From 4247ec7a3fac08773ca81be800c1204ada6a49e2 Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Thu, 12 Nov 2020 15:23:19 -0800 Subject: [PATCH 01/11] Migrate to CanvasKit 0.19.0 --- .../src/engine/canvaskit/canvaskit_api.dart | 61 ++++++++++++------- .../src/engine/canvaskit/color_filter.dart | 12 ++-- .../lib/src/engine/canvaskit/fonts.dart | 11 +--- .../lib/src/engine/canvaskit/image.dart | 4 +- .../src/engine/canvaskit/image_filter.dart | 2 +- .../src/engine/canvaskit/initialization.dart | 2 +- .../lib/src/engine/canvaskit/mask_filter.dart | 2 +- .../lib/src/engine/canvaskit/shader.dart | 8 +-- .../lib/src/engine/canvaskit/vertices.dart | 2 +- .../test/canvaskit/canvaskit_api_test.dart | 42 ++++++------- 10 files changed, 80 insertions(+), 66 deletions(-) diff --git a/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart b/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart index 40ee255c71a1f..693f101ffb356 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart @@ -45,14 +45,13 @@ class CanvasKit { external SkFontWeightEnum get FontWeight; external SkFontSlantEnum get FontSlant; external SkAnimatedImage MakeAnimatedImageFromEncoded(Uint8List imageData); - external SkShaderNamespace get SkShader; - external SkMaskFilter MakeBlurMaskFilter( - SkBlurStyle blurStyle, double sigma, bool respectCTM); - external SkColorFilterNamespace get SkColorFilter; - external SkImageFilterNamespace get SkImageFilter; + external SkShaderNamespace get Shader; + external SkMaskFilterNamespace get MaskFilter; + external SkColorFilterNamespace get ColorFilter; + external SkImageFilterNamespace get ImageFilter; external SkPath MakePathFromOp(SkPath path1, SkPath path2, SkPathOp pathOp); external SkTonalColors computeTonalColors(SkTonalColors inTonalColors); - external SkVertices MakeSkVertices( + external SkVertices MakeVertices( SkVertexMode mode, List positions, List? textureCoordinates, @@ -68,7 +67,7 @@ class CanvasKit { int width, int height, ); - external Uint8List getSkDataBytes( + external Uint8List getDataBytes( SkData skData, ); @@ -83,7 +82,7 @@ class CanvasKit { external SkTextBaselineEnum get TextBaseline; external SkPlaceholderAlignmentEnum get PlaceholderAlignment; - external SkFontMgrNamespace get SkFontMgr; + external SkFontMgrNamespace get FontMgr; external TypefaceFontProviderNamespace get TypefaceFontProvider; external int GetWebGLContext( html.CanvasElement canvas, SkWebGLContextOptions options); @@ -92,7 +91,7 @@ class CanvasKit { SkGrContext grContext, int width, int height, - SkColorSpace colorSpace, + ColorSpace colorSpace, ); external SkSurface MakeSWCanvasSurface(html.CanvasElement canvas); external void setCurrentContext(int glContext); @@ -122,11 +121,11 @@ class CanvasKitInitPromise { external void then(CanvasKitInitCallback callback); } -@JS('window.flutterCanvasKit.SkColorSpace.SRGB') -external SkColorSpace get SkColorSpaceSRGB; +@JS('window.flutterCanvasKit.ColorSpace.SRGB') +external ColorSpace get SkColorSpaceSRGB; @JS() -class SkColorSpace {} +class ColorSpace {} @JS() @anonymous @@ -138,7 +137,7 @@ class SkWebGLContextOptions { }); } -@JS() +@JS('window.flutterCanvasKit.Surface') class SkSurface { external SkCanvas getCanvas(); external void flush(); @@ -157,12 +156,13 @@ class SkGrContext { } @JS() +@anonymous class SkFontSlantEnum { external SkFontSlant get Upright; external SkFontSlant get Italic; } -@JS() +@JS('window.flutterCanvasKit.FontSlant') class SkFontSlant { external int get value; } @@ -177,6 +177,7 @@ SkFontSlant toSkFontSlant(ui.FontStyle style) { } @JS() +@anonymous class SkFontWeightEnum { external SkFontWeight get Thin; external SkFontWeight get ExtraLight; @@ -763,11 +764,17 @@ class SkShader { external void delete(); } +@JS() +class SkMaskFilterNamespace { + external SkMaskFilter MakeBlur( + SkBlurStyle blurStyle, double sigma, bool respectCTM); +} + // This needs to be bound to top-level because SkPaint is initialized // with `new`. Also in Dart you can't write this: // // external SkPaint SkPaint(); -@JS('window.flutterCanvasKit.SkPaint') +@JS('window.flutterCanvasKit.Paint') class SkPaint { // TODO(yjbanov): implement invertColors, see paint.cc external SkPaint(); @@ -1028,7 +1035,7 @@ List encodeRawColorList(Int32List rawColors) { return toSkFloatColorList(colors); } -@JS('window.flutterCanvasKit.SkPath') +@JS('window.flutterCanvasKit.Path') class SkPath { external SkPath([SkPath? other]); external void setFillType(SkFillType fillType); @@ -1166,7 +1173,7 @@ class SkPath { external void delete(); } -@JS('window.flutterCanvasKit.SkContourMeasureIter') +@JS('window.flutterCanvasKit.ContourMeasureIter') class SkContourMeasureIter { external SkContourMeasureIter(SkPath path, bool forceClosed, double resScale); external SkContourMeasure? next(); @@ -1280,7 +1287,7 @@ Uint16List toUint16List(List ints) { return result; } -@JS('window.flutterCanvasKit.SkPictureRecorder') +@JS('window.flutterCanvasKit.PictureRecorder') class SkPictureRecorder { external SkPictureRecorder(); external SkCanvas beginRecording(Float32List bounds); @@ -1630,11 +1637,22 @@ class SkFontFeature { external set value(int? value); } +@JS() +@anonymous +class SkTypeface {} + +@JS('window.flutterCanvasKit.Font') +class SkFont { + external SkFont(SkTypeface typeface); + external Uint8List getGlyphIDs(String text); +} + @JS() @anonymous class SkFontMgr { external String? getFamilyName(int fontId); external void delete(); + external SkTypeface MakeTypefaceFromData(Uint8List font); } @JS('window.flutterCanvasKit.TypefaceFontProvider') @@ -1703,6 +1721,7 @@ class SkTonalColors { class SkFontMgrNamespace { // TODO(yjbanov): can this be made non-null? It returns null in our unit-tests right now. external SkFontMgr? FromData(List fonts); + external SkFontMgr RefDefault(); } @JS() @@ -1810,11 +1829,11 @@ class SkImageInfo { required int width, required int height, SkAlphaType alphaType, - SkColorSpace colorSpace, + ColorSpace colorSpace, SkColorType colorType, }); external SkAlphaType get alphaType; - external SkColorSpace get colorSpace; + external ColorSpace get colorSpace; external SkColorType get colorType; external int get height; external bool get isEmpty; @@ -1822,7 +1841,7 @@ class SkImageInfo { external Float32List get bounds; external int get width; external SkImageInfo makeAlphaType(SkAlphaType alphaType); - external SkImageInfo makeColorSpace(SkColorSpace colorSpace); + external SkImageInfo makeColorSpace(ColorSpace colorSpace); external SkImageInfo makeColorType(SkColorType colorType); external SkImageInfo makeWH(int width, int height); } diff --git a/lib/web_ui/lib/src/engine/canvaskit/color_filter.dart b/lib/web_ui/lib/src/engine/canvaskit/color_filter.dart index 86bb58cbcde98..0876d7118ba5b 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/color_filter.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/color_filter.dart @@ -54,7 +54,7 @@ abstract class CkColorFilter implements _CkManagedSkImageFilterConvertible canvasKit.SkImageFilter.MakeColorFilter(_initRawColorFilter(), null); + SkImageFilter _initRawImageFilter() => canvasKit.ImageFilter.MakeColorFilter(_initRawColorFilter(), null); /// Called by [ManagedSkiaObject.createDefault] and /// [ManagedSkiaObject.resurrect] to create a new [SKColorFilter], when this @@ -72,7 +72,7 @@ class _CkBlendModeColorFilter extends CkColorFilter { @override SkColorFilter _initRawColorFilter() { - return canvasKit.SkColorFilter.MakeBlend( + return canvasKit.ColorFilter.MakeBlend( toSharedSkColor1(color), toSkBlendMode(blendMode), ); @@ -104,12 +104,12 @@ class _CkMatrixColorFilter extends CkColorFilter { assert(this.matrix.length == 20, 'Color Matrix must have 20 entries.'); final List matrix = this.matrix; if (matrix is Float32List) - return canvasKit.SkColorFilter.MakeMatrix(matrix); + return canvasKit.ColorFilter.MakeMatrix(matrix); final Float32List float32Matrix = Float32List(20); for (int i = 0; i < 20; i++) { float32Matrix[i] = matrix[i]; } - return canvasKit.SkColorFilter.MakeMatrix(float32Matrix); + return canvasKit.ColorFilter.MakeMatrix(float32Matrix); } @override @@ -129,7 +129,7 @@ class _CkMatrixColorFilter extends CkColorFilter { class _CkLinearToSrgbGammaColorFilter extends CkColorFilter { const _CkLinearToSrgbGammaColorFilter(); @override - SkColorFilter _initRawColorFilter() => canvasKit.SkColorFilter.MakeLinearToSRGBGamma(); + SkColorFilter _initRawColorFilter() => canvasKit.ColorFilter.MakeLinearToSRGBGamma(); @override bool operator ==(Object other) => runtimeType == other.runtimeType; @@ -141,7 +141,7 @@ class _CkLinearToSrgbGammaColorFilter extends CkColorFilter { class _CkSrgbToLinearGammaColorFilter extends CkColorFilter { const _CkSrgbToLinearGammaColorFilter(); @override - SkColorFilter _initRawColorFilter() => canvasKit.SkColorFilter.MakeSRGBToLinearGamma(); + SkColorFilter _initRawColorFilter() => canvasKit.ColorFilter.MakeSRGBToLinearGamma(); @override bool operator ==(Object other) => runtimeType == other.runtimeType; diff --git a/lib/web_ui/lib/src/engine/canvaskit/fonts.dart b/lib/web_ui/lib/src/engine/canvaskit/fonts.dart index e8812a3b94e58..ba08f1a50e6a2 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/fonts.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/fonts.dart @@ -121,9 +121,7 @@ class SkiaFontCollection { Future<_RegisteredFont?> _registerFont(String url, String family) async { ByteBuffer buffer; try { - buffer = await html.window - .fetch(url) - .then(_getArrayBuffer); + buffer = await html.window.fetch(url).then(_getArrayBuffer); } catch (e) { html.window.console.warn('Failed to load font $family at $url'); html.window.console.warn(e); @@ -143,7 +141,7 @@ class SkiaFontCollection { } String? _readActualFamilyName(Uint8List bytes) { - final SkFontMgr tmpFontMgr = canvasKit.SkFontMgr.FromData([bytes])!; + final SkFontMgr tmpFontMgr = canvasKit.FontMgr.FromData([bytes])!; String? actualFamily = tmpFontMgr.getFamilyName(0); tmpFontMgr.delete(); return actualFamily; @@ -171,8 +169,5 @@ class _RegisteredFont { /// The font family that was parsed from the font's bytes. final String actualFamily; - _RegisteredFont(this.bytes, this.flutterFamily, this.actualFamily) - : assert(bytes != null), // ignore: unnecessary_null_comparison - assert(flutterFamily != null), // ignore: unnecessary_null_comparison - assert(actualFamily != null); // ignore: unnecessary_null_comparison + _RegisteredFont(this.bytes, this.flutterFamily, this.actualFamily); } diff --git a/lib/web_ui/lib/src/engine/canvaskit/image.dart b/lib/web_ui/lib/src/engine/canvaskit/image.dart index 0f1f2d98ff055..9edc0961ce409 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/image.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/image.dart @@ -126,7 +126,7 @@ class CkAnimatedImage implements ui.Image { // Defaults to PNG 100%. final SkData skData = _skAnimatedImage.encodeToData(); // Make a copy that we can return. - bytes = Uint8List.fromList(canvasKit.getSkDataBytes(skData)); + bytes = Uint8List.fromList(canvasKit.getDataBytes(skData)); } final ByteData data = bytes.buffer.asByteData(0, bytes.length); @@ -207,7 +207,7 @@ class CkImage implements ui.Image { } else { final SkData skData = skImage.encodeToData(); //defaults to PNG 100% // make a copy that we can return - bytes = Uint8List.fromList(canvasKit.getSkDataBytes(skData)); + bytes = Uint8List.fromList(canvasKit.getDataBytes(skData)); } final ByteData data = bytes.buffer.asByteData(0, bytes.length); diff --git a/lib/web_ui/lib/src/engine/canvaskit/image_filter.dart b/lib/web_ui/lib/src/engine/canvaskit/image_filter.dart index 5562e84c65f69..5400b2ab2d61e 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/image_filter.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/image_filter.dart @@ -73,7 +73,7 @@ class _CkBlurImageFilter extends CkImageFilter { @override SkImageFilter _initSkiaObject() { - return canvasKit.SkImageFilter.MakeBlur( + return canvasKit.ImageFilter.MakeBlur( sigmaX, sigmaY, canvasKit.TileMode.Clamp, diff --git a/lib/web_ui/lib/src/engine/canvaskit/initialization.dart b/lib/web_ui/lib/src/engine/canvaskit/initialization.dart index 131eef0bcea67..d29758b75042a 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/initialization.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/initialization.dart @@ -52,7 +52,7 @@ const bool canvasKitForceCpuOnly = /// NPM, update this URL to `https://unpkg.com/canvaskit-wasm@0.34.0/bin/`. const String canvasKitBaseUrl = String.fromEnvironment( 'FLUTTER_WEB_CANVASKIT_URL', - defaultValue: 'https://unpkg.com/canvaskit-wasm@0.18.1/bin/', + defaultValue: 'https://unpkg.com/canvaskit-wasm@0.19.0/bin/', ); /// Initialize CanvasKit. diff --git a/lib/web_ui/lib/src/engine/canvaskit/mask_filter.dart b/lib/web_ui/lib/src/engine/canvaskit/mask_filter.dart index 8c120d1520035..749c98b94a8ff 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/mask_filter.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/mask_filter.dart @@ -21,7 +21,7 @@ class CkMaskFilter extends ManagedSkiaObject { SkMaskFilter resurrect() => _initSkiaObject(); SkMaskFilter _initSkiaObject() { - return canvasKit.MakeBlurMaskFilter( + return canvasKit.MaskFilter.MakeBlur( toSkBlurStyle(_blurStyle), _sigma, true, diff --git a/lib/web_ui/lib/src/engine/canvaskit/shader.dart b/lib/web_ui/lib/src/engine/canvaskit/shader.dart index ea251bfd7b5e4..36c372d71122b 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/shader.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/shader.dart @@ -35,7 +35,7 @@ class CkGradientSweep extends CkShader implements ui.Gradient { @override SkShader createDefault() { - return canvasKit.SkShader.MakeSweepGradient( + return canvasKit.Shader.MakeSweepGradient( center.dx, center.dy, toSkFloatColorList(colors), @@ -83,7 +83,7 @@ class CkGradientLinear extends CkShader implements ui.Gradient { SkShader createDefault() { assert(useCanvasKit); - return canvasKit.SkShader.MakeLinearGradient( + return canvasKit.Shader.MakeLinearGradient( toSkPoint(from), toSkPoint(to), toSkFloatColorList(colors), @@ -111,7 +111,7 @@ class CkGradientRadial extends CkShader implements ui.Gradient { SkShader createDefault() { assert(useCanvasKit); - return canvasKit.SkShader.MakeRadialGradient( + return canvasKit.Shader.MakeRadialGradient( toSkPoint(center), radius, toSkFloatColorList(colors), @@ -142,7 +142,7 @@ class CkGradientConical extends CkShader implements ui.Gradient { @override SkShader createDefault() { assert(useCanvasKit); - return canvasKit.SkShader.MakeTwoPointConicalGradient( + return canvasKit.Shader.MakeTwoPointConicalGradient( toSkPoint(focal), focalRadius, toSkPoint(center), diff --git a/lib/web_ui/lib/src/engine/canvaskit/vertices.dart b/lib/web_ui/lib/src/engine/canvaskit/vertices.dart index ec13910db9c39..153060a310b56 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/vertices.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/vertices.dart @@ -80,7 +80,7 @@ class CkVertices extends ManagedSkiaObject implements ui.Vertices { @override SkVertices createDefault() { - return canvasKit.MakeSkVertices( + return canvasKit.MakeVertices( _mode, _positions, _textureCoordinates, diff --git a/lib/web_ui/test/canvaskit/canvaskit_api_test.dart b/lib/web_ui/test/canvaskit/canvaskit_api_test.dart index b1d56509d4b2d..452cffd495b00 100644 --- a/lib/web_ui/test/canvaskit/canvaskit_api_test.dart +++ b/lib/web_ui/test/canvaskit/canvaskit_api_test.dart @@ -334,7 +334,7 @@ void _shaderTests() { test('MakeRadialGradient', () { expect( - canvasKit.SkShader.MakeRadialGradient( + canvasKit.Shader.MakeRadialGradient( Float32List.fromList([1, 1]), 10.0, [ @@ -351,7 +351,7 @@ void _shaderTests() { test('MakeTwoPointConicalGradient', () { expect( - canvasKit.SkShader.MakeTwoPointConicalGradient( + canvasKit.Shader.MakeTwoPointConicalGradient( Float32List.fromList([1, 1]), 10.0, Float32List.fromList([1, 1]), @@ -370,7 +370,7 @@ void _shaderTests() { } SkShader _makeTestShader() { - return canvasKit.SkShader.MakeLinearGradient( + return canvasKit.Shader.MakeLinearGradient( Float32List.fromList([0, 0]), Float32List.fromList([1, 1]), [ @@ -392,15 +392,15 @@ void _paintTests() { paint.setAntiAlias(true); paint.setColorInt(0x00FFCCAA); paint.setShader(_makeTestShader()); - paint.setMaskFilter(canvasKit.MakeBlurMaskFilter( + paint.setMaskFilter(canvasKit.MaskFilter.MakeBlur( canvasKit.BlurStyle.Outer, 2.0, true, )); paint.setFilterQuality(canvasKit.FilterQuality.High); - paint.setColorFilter(canvasKit.SkColorFilter.MakeLinearToSRGBGamma()); + paint.setColorFilter(canvasKit.ColorFilter.MakeLinearToSRGBGamma()); paint.setStrokeMiter(1.4); - paint.setImageFilter(canvasKit.SkImageFilter.MakeBlur( + paint.setImageFilter(canvasKit.ImageFilter.MakeBlur( 1, 2, canvasKit.TileMode.Repeat, @@ -410,9 +410,9 @@ void _paintTests() { } void _maskFilterTests() { - test('MakeBlurMaskFilter', () { + test('MaskFilter.MakeBlur', () { expect( - canvasKit.MakeBlurMaskFilter( + canvasKit.MaskFilter.MakeBlur( canvasKit.BlurStyle.Outer, 5.0, false, @@ -424,7 +424,7 @@ void _maskFilterTests() { void _colorFilterTests() { test('MakeBlend', () { expect( - canvasKit.SkColorFilter.MakeBlend( + canvasKit.ColorFilter.MakeBlend( Float32List.fromList([0, 0, 0, 1]), canvasKit.BlendMode.SrcATop, ), @@ -434,7 +434,7 @@ void _colorFilterTests() { test('MakeMatrix', () { expect( - canvasKit.SkColorFilter.MakeMatrix( + canvasKit.ColorFilter.MakeMatrix( Float32List(20), ), isNotNull, @@ -443,14 +443,14 @@ void _colorFilterTests() { test('MakeSRGBToLinearGamma', () { expect( - canvasKit.SkColorFilter.MakeSRGBToLinearGamma(), + canvasKit.ColorFilter.MakeSRGBToLinearGamma(), isNotNull, ); }); test('MakeLinearToSRGBGamma', () { expect( - canvasKit.SkColorFilter.MakeLinearToSRGBGamma(), + canvasKit.ColorFilter.MakeLinearToSRGBGamma(), isNotNull, ); }); @@ -459,14 +459,14 @@ void _colorFilterTests() { void _imageFilterTests() { test('MakeBlur', () { expect( - canvasKit.SkImageFilter.MakeBlur(1, 2, canvasKit.TileMode.Repeat, null), + canvasKit.ImageFilter.MakeBlur(1, 2, canvasKit.TileMode.Repeat, null), isNotNull, ); }); test('MakeMatrixTransform', () { expect( - canvasKit.SkImageFilter.MakeMatrixTransform( + canvasKit.ImageFilter.MakeMatrixTransform( toSkMatrixFromFloat32(Matrix4.identity().storage), canvasKit.FilterQuality.Medium, null, @@ -477,8 +477,8 @@ void _imageFilterTests() { test('MakeColorFilter', () { expect( - canvasKit.SkImageFilter.MakeColorFilter( - canvasKit.SkColorFilter.MakeLinearToSRGBGamma(), + canvasKit.ImageFilter.MakeColorFilter( + canvasKit.ColorFilter.MakeLinearToSRGBGamma(), null, ), isNotNull, @@ -487,9 +487,9 @@ void _imageFilterTests() { test('MakeCompose', () { expect( - canvasKit.SkImageFilter.MakeCompose( - canvasKit.SkImageFilter.MakeBlur(1, 2, canvasKit.TileMode.Repeat, null), - canvasKit.SkImageFilter.MakeBlur(1, 2, canvasKit.TileMode.Repeat, null), + canvasKit.ImageFilter.MakeCompose( + canvasKit.ImageFilter.MakeBlur(1, 2, canvasKit.TileMode.Repeat, null), + canvasKit.ImageFilter.MakeBlur(1, 2, canvasKit.TileMode.Repeat, null), ), isNotNull, ); @@ -860,7 +860,7 @@ void _pathTests() { } SkVertices _testVertices() { - return canvasKit.MakeSkVertices( + return canvasKit.MakeVertices( canvasKit.VertexMode.Triangles, [ Float32List.fromList([0, 0]), @@ -930,7 +930,7 @@ void _canvasTests() { canvas.saveLayer( SkPaint(), toSkRect(ui.Rect.fromLTRB(0, 0, 100, 100)), - canvasKit.SkImageFilter.MakeBlur(1, 2, canvasKit.TileMode.Repeat, null), + canvasKit.ImageFilter.MakeBlur(1, 2, canvasKit.TileMode.Repeat, null), 0, ); }); From c06960c01d6c4b0f7240286c3abb7630a70d411c Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Thu, 12 Nov 2020 15:18:43 -0800 Subject: [PATCH 02/11] Detect unsupported glyphs --- lib/web_ui/lib/src/engine.dart | 1 + .../src/engine/canvaskit/font_fallbacks.dart | 461 ++++++++++++++++++ .../lib/src/engine/canvaskit/fonts.dart | 18 +- lib/web_ui/lib/src/engine/canvaskit/text.dart | 95 ++-- 4 files changed, 542 insertions(+), 33 deletions(-) create mode 100644 lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart diff --git a/lib/web_ui/lib/src/engine.dart b/lib/web_ui/lib/src/engine.dart index e9684afea6bc9..64a1bd299a941 100644 --- a/lib/web_ui/lib/src/engine.dart +++ b/lib/web_ui/lib/src/engine.dart @@ -32,6 +32,7 @@ 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'; diff --git a/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart b/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart new file mode 100644 index 0000000000000..2255cad792a09 --- /dev/null +++ b/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart @@ -0,0 +1,461 @@ +// 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; + +void _findFontsForMissingCodeunit(int codeunit) { + _ensureNotoFontTreeCreated(); + List families = _lookupNotoFontsForCodeunit(codeunit); + print('Fonts which match $codeunit: $families'); +} + +void _ensureNotoFontTreeCreated() { + if (_notoTreeRoot != null) { + return; + } + + for (_NotoFont font in _notoFonts) { + for (_UnicodeRange range in font.unicodeRanges) { + _insertNotoFontRange(range, font); + } + } +} + +List _lookupNotoFontsForCodeunit(int codeunit) { + List lookupHelper(_NotoTreeNode node) { + if (node.range.contains(codeunit)) { + return node.fonts.map((_NotoFont f) => f.name).toList(); + } + if (node.range.start > codeunit) { + if (node.left != null) { + return lookupHelper(node.left!); + } else { + print('Could not find font to match $codeunit'); + return []; + } + } else { + if (node.right != null) { + return lookupHelper(node.right!); + } else { + print('Could not find font to match $codeunit'); + return []; + } + } + } + + return lookupHelper(_notoTreeRoot!); +} + +class _NotoFont { + final String name; + final List<_UnicodeRange> unicodeRanges; + + const _NotoFont(this.name, this.unicodeRanges); + + String get googleFontsCssUrl => + 'https://fonts.googleapis.com/css2?family=${name.replaceAll(' ', '+')}'; +} + +class _UnicodeRange { + final int start; + final int end; + + const _UnicodeRange(this.start, this.end); + + bool contains(int codeUnit) { + return start <= codeUnit && codeUnit <= end; + } + + bool operator ==(dynamic other) { + if (other is! _UnicodeRange) { + return false; + } + _UnicodeRange range = other; + return range.start == start && range.end == end; + } +} + +const List<_NotoFont> _notoFonts = <_NotoFont>[ + _NotoFont('Noto Naskh Arabic UI', <_UnicodeRange>[ + _UnicodeRange(1536, 1791), + _UnicodeRange(8204, 8206), + _UnicodeRange(8208, 8209), + _UnicodeRange(8271, 8271), + _UnicodeRange(11841, 11841), + _UnicodeRange(64336, 65023), + _UnicodeRange(65132, 65276), + ]), + _NotoFont('Noto Sans Armenian', <_UnicodeRange>[ + _UnicodeRange(1328, 1424), + _UnicodeRange(64275, 64279), + ]), + _NotoFont('Noto Sans Bengali UI', <_UnicodeRange>[ + _UnicodeRange(2404, 2405), + _UnicodeRange(2433, 2555), + _UnicodeRange(8204, 8205), + _UnicodeRange(8377, 8377), + _UnicodeRange(9676, 9676), + ]), + _NotoFont('Noto Sans Myanmar UI', <_UnicodeRange>[ + _UnicodeRange(4096, 4255), + _UnicodeRange(8204, 8205), + _UnicodeRange(9676, 9676), + ]), + _NotoFont('Noto Sans HK', <_UnicodeRange>[ + _UnicodeRange(12288, 12351), + _UnicodeRange(12549, 12585), + _UnicodeRange(19968, 40959), + ]), + _NotoFont('Noto Sans SC', <_UnicodeRange>[ + _UnicodeRange(12288, 12591), + _UnicodeRange(12800, 13311), + _UnicodeRange(19968, 40959), + _UnicodeRange(65072, 65135), + _UnicodeRange(65280, 65519), + ]), + _NotoFont('Noto Sans TC', <_UnicodeRange>[ + _UnicodeRange(12288, 12351), + _UnicodeRange(12549, 12585), + _UnicodeRange(19968, 40959), + ]), + _NotoFont('Noto Sans Egyptian Hieroglyphs', <_UnicodeRange>[ + _UnicodeRange(77824, 78894), + ]), + _NotoFont('Noto Sans Ethiopic', <_UnicodeRange>[ + _UnicodeRange(4608, 5017), + _UnicodeRange(11648, 11742), + _UnicodeRange(43777, 43822), + ]), + _NotoFont('Noto Sans Georgian', <_UnicodeRange>[ + _UnicodeRange(1417, 1417), + _UnicodeRange(4256, 4351), + _UnicodeRange(11520, 11567), + ]), + _NotoFont('Noto Sans Gujarati UI', <_UnicodeRange>[ + _UnicodeRange(2404, 2405), + _UnicodeRange(2688, 2815), + _UnicodeRange(8204, 8205), + _UnicodeRange(8377, 8377), + _UnicodeRange(9676, 9676), + _UnicodeRange(43056, 43065), + ]), + _NotoFont('Noto Sans Gurmukhi UI', <_UnicodeRange>[ + _UnicodeRange(2404, 2405), + _UnicodeRange(2561, 2677), + _UnicodeRange(8204, 8205), + _UnicodeRange(8377, 8377), + _UnicodeRange(9676, 9676), + _UnicodeRange(9772, 9772), + _UnicodeRange(43056, 43065), + ]), + _NotoFont('Noto Sans Hebrew', <_UnicodeRange>[ + _UnicodeRange(1424, 1535), + _UnicodeRange(8362, 8362), + _UnicodeRange(9676, 9676), + _UnicodeRange(64285, 64335), + ]), + _NotoFont('Noto Sans Devanagari UI', <_UnicodeRange>[ + _UnicodeRange(2304, 2431), + _UnicodeRange(7376, 7414), + _UnicodeRange(7416, 7417), + _UnicodeRange(8204, 9205), + _UnicodeRange(8360, 8360), + _UnicodeRange(8377, 8377), + _UnicodeRange(9676, 9676), + _UnicodeRange(43056, 43065), + _UnicodeRange(43232, 43259), + ]), + _NotoFont('Noto Sans JP', <_UnicodeRange>[ + _UnicodeRange(12288, 12543), + _UnicodeRange(19968, 40959), + _UnicodeRange(65280, 65519), + ]), + _NotoFont('Noto Sans Kannada UI', <_UnicodeRange>[ + _UnicodeRange(2404, 2405), + _UnicodeRange(3202, 3314), + _UnicodeRange(8204, 8205), + _UnicodeRange(8377, 8377), + _UnicodeRange(9676, 9676), + ]), + _NotoFont('Noto Sans Khmer UI', <_UnicodeRange>[ + _UnicodeRange(6016, 6143), + _UnicodeRange(8204, 8204), + _UnicodeRange(9676, 9676), + ]), + _NotoFont('Noto Sans KR', <_UnicodeRange>[ + _UnicodeRange(12593, 12686), + _UnicodeRange(12800, 12828), + _UnicodeRange(12896, 12923), + _UnicodeRange(44032, 55215), + ]), + _NotoFont('Noto Sans Lao UI', <_UnicodeRange>[ + _UnicodeRange(3713, 3807), + _UnicodeRange(9676, 9676), + ]), + _NotoFont('Noto Sans Malayalam UI', <_UnicodeRange>[ + _UnicodeRange(775, 775), + _UnicodeRange(803, 803), + _UnicodeRange(2404, 2405), + _UnicodeRange(3330, 3455), + _UnicodeRange(8204, 8205), + _UnicodeRange(8377, 8377), + _UnicodeRange(9676, 9676), + ]), + _NotoFont('Noto Sans Sinhala', <_UnicodeRange>[ + _UnicodeRange(2404, 2405), + _UnicodeRange(3458, 3572), + _UnicodeRange(8204, 8205), + _UnicodeRange(9676, 9676), + ]), + _NotoFont('Noto Sans Tamil UI', <_UnicodeRange>[ + _UnicodeRange(2404, 2405), + _UnicodeRange(2946, 3066), + _UnicodeRange(8204, 8205), + _UnicodeRange(8377, 8377), + _UnicodeRange(9676, 9676), + ]), + _NotoFont('Noto Sans Telugu UI', <_UnicodeRange>[ + _UnicodeRange(2385, 2386), + _UnicodeRange(2404, 2405), + _UnicodeRange(3072, 3199), + _UnicodeRange(7386, 7386), + _UnicodeRange(8204, 8205), + _UnicodeRange(9676, 9676), + ]), + _NotoFont('Noto Sans Thai UI', <_UnicodeRange>[ + _UnicodeRange(3585, 3675), + _UnicodeRange(8204, 8205), + _UnicodeRange(9676, 9676), + ]), + _NotoFont('Noto Sans', <_UnicodeRange>[ + _UnicodeRange(0, 255), + _UnicodeRange(305, 305), + _UnicodeRange(338, 339), + _UnicodeRange(699, 700), + _UnicodeRange(710, 710), + _UnicodeRange(730, 730), + _UnicodeRange(732, 732), + _UnicodeRange(8192, 8303), + _UnicodeRange(8308, 8308), + _UnicodeRange(8364, 8364), + _UnicodeRange(8482, 8482), + _UnicodeRange(8593, 8593), + _UnicodeRange(8595, 8595), + _UnicodeRange(8722, 8722), + _UnicodeRange(8725, 8725), + _UnicodeRange(65279, 65279), + _UnicodeRange(65533, 65533), + _UnicodeRange(1024, 1119), + _UnicodeRange(1168, 1169), + _UnicodeRange(1200, 1201), + _UnicodeRange(8470, 8470), + _UnicodeRange(1120, 1327), + _UnicodeRange(7296, 7304), + _UnicodeRange(8372, 8372), + _UnicodeRange(11744, 11775), + _UnicodeRange(42560, 42655), + _UnicodeRange(65070, 65071), + _UnicodeRange(880, 1023), + _UnicodeRange(7936, 8191), + _UnicodeRange(256, 591), + _UnicodeRange(601, 601), + _UnicodeRange(7680, 7935), + _UnicodeRange(8224, 8224), + _UnicodeRange(8352, 8363), + _UnicodeRange(8365, 8399), + _UnicodeRange(8467, 8467), + _UnicodeRange(11360, 11391), + _UnicodeRange(42784, 43007), + _UnicodeRange(258, 259), + _UnicodeRange(272, 273), + _UnicodeRange(296, 297), + _UnicodeRange(360, 361), + _UnicodeRange(416, 417), + _UnicodeRange(431, 432), + _UnicodeRange(7840, 7929), + _UnicodeRange(8363, 8363), + ]), +// Noto Sans Symbols +// Noto Color Emoji Compat +]; + +/// A node in a red-black tree for Noto Fonts. +class _NotoTreeNode { + _NotoTreeNode? parent; + _NotoTreeNode? left; + _NotoTreeNode? right; + + /// If `true`, then this node is black. Otherwise it is red. + bool isBlack = false; + bool get isRed => !isBlack; + + final _UnicodeRange range; + final List<_NotoFont> fonts; + + _NotoTreeNode(this.range) : this.fonts = <_NotoFont>[]; +} + +/// Associates [range] with [font] in the Noto Font tree. +void _insertNotoFontRange(_UnicodeRange range, _NotoFont font) { + _NotoTreeNode? newNode = + _insertNotoFontRangeHelper(_notoTreeRoot, range, font); + if (newNode != null) { + _repairNotoFontTree(newNode); + + // Make sure the root node is correctly set. + _NotoTreeNode newRoot = newNode; + while (newRoot.parent != null) { + newRoot = newRoot.parent!; + } + + _notoTreeRoot = newRoot; + } +} + +/// Recurses the font tree and associates [range] with [font]. +/// +/// If a new node is created, it is returned so we can repair the tree. +_NotoTreeNode? _insertNotoFontRangeHelper( + _NotoTreeNode? root, _UnicodeRange range, _NotoFont font) { + if (root != null) { + if (root.range == range) { + // The root node range is the same as the range we're inserting. + root.fonts.add(font); + return null; + } + if (range.start < root.range.start) { + if (root.left != null) { + return _insertNotoFontRangeHelper(root.left, range, font); + } else { + _NotoTreeNode newNode = _NotoTreeNode(range); + newNode.fonts.add(font); + newNode.parent = root; + root.left = newNode; + return newNode; + } + } else { + if (root.right != null) { + return _insertNotoFontRangeHelper(root.right, range, font); + } else { + _NotoTreeNode newNode = _NotoTreeNode(range); + newNode.fonts.add(font); + newNode.parent = root; + root.right = newNode; + return newNode; + } + } + } else { + // If [root] is null, then the root of the entire Noto Font tree is null. + assert(_notoTreeRoot == null); + _NotoTreeNode newRoot = _NotoTreeNode(range); + newRoot.fonts.add(font); + _notoTreeRoot = newRoot; + return newRoot; + } +} + +void _repairNotoFontTree(_NotoTreeNode node) { + if (node.parent == null) { + // This is the root node. The root node must be black. + node.isBlack = true; + return; + } else if (node.parent!.isBlack) { + // Do nothing. + return; + } + + // If we've reached here, then (1) node's parent is non-null and (2) node's + // parent is red, which means that node's parent is not the root node and + // therefore (3) node's grandparent is not null; + + _NotoTreeNode parent = node.parent!; + _NotoTreeNode grandparent = parent.parent!; + + _NotoTreeNode? uncle; + if (parent == grandparent.left) { + uncle = grandparent.right; + } else { + uncle = grandparent.left; + } + if (uncle != null && uncle.isRed) { + parent.isBlack = true; + uncle.isBlack = true; + _repairNotoFontTree(grandparent); + return; + } + + // If we've reached here, then the parent is red and the uncle is black + // (note: null leaves are considered black). We must re-balance the tree + // by rotating such that the node is in the grandparent position. + + void rotateLeft(_NotoTreeNode node) { + // We will only ever call this on nodes which have a right child. + _NotoTreeNode newNode = node.right!; + _NotoTreeNode? parent = node.parent; + + node.right = newNode.left; + newNode.left = node; + node.parent = newNode; + if (node.right != null) { + node.right!.parent = node; + } + + if (parent != null) { + if (node == parent.left) { + parent.left = newNode; + } else { + parent.right = newNode; + } + } + + newNode.parent = parent; + } + + void rotateRight(_NotoTreeNode node) { + // We will only ever call this on nodes which have a left child. + _NotoTreeNode newNode = node.left!; + _NotoTreeNode? parent = node.parent; + + node.left = newNode.right; + newNode.right = node; + node.parent = newNode; + + if (node.left != null) { + node.left!.parent = node; + } + + if (parent != null) { + if (node == parent.left) { + parent.left = newNode; + } else { + parent.right = newNode; + } + } + + newNode.parent = parent; + } + + if (node == parent.right && parent == grandparent.left) { + rotateLeft(parent); + node = node.left!; + } else if (node == parent.left && parent == grandparent.right) { + rotateRight(parent); + node = node.right!; + } + + parent = node.parent!; + grandparent = parent.parent!; + + if (node == parent.left) { + rotateRight(grandparent); + } else { + rotateLeft(grandparent); + } + + parent.isBlack = true; + grandparent.isBlack = false; +} + +_NotoTreeNode? _notoTreeRoot; diff --git a/lib/web_ui/lib/src/engine/canvaskit/fonts.dart b/lib/web_ui/lib/src/engine/canvaskit/fonts.dart index 2a5589d61ba80..97096aeb01556 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/fonts.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/fonts.dart @@ -24,13 +24,22 @@ class SkiaFontCollection { final Set registeredFamilies = {}; + final Map> familyToTypefaceMap = + >{}; + + final List globalFontFallbacks = []; + Future ensureFontsLoaded() async { await _loadFonts(); fontProvider = canvasKit.TypefaceFontProvider.Make(); + familyToTypefaceMap.clear(); for (var font in _registeredFonts) { fontProvider.registerFont(font.bytes, font.flutterFamily); + familyToTypefaceMap + .putIfAbsent(font.flutterFamily, () => []) + .add(font.typeface); } } @@ -169,5 +178,12 @@ class _RegisteredFont { /// The font family that was parsed from the font's bytes. final String actualFamily; - _RegisteredFont(this.bytes, this.flutterFamily, this.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) + : this.typeface = + canvasKit.FontMgr.RefDefault().MakeTypefaceFromData(bytes); } diff --git a/lib/web_ui/lib/src/engine/canvaskit/text.dart b/lib/web_ui/lib/src/engine/canvaskit/text.dart index 36f609f64d83a..1d327eab21bd3 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/text.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/text.dart @@ -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 = _getActualFontFamilies(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 fontFamilies = [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 = + _getActualFontFamilies(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 fontFamilies = [fontFamily]; - if (fontFamilyFallback != null && - !fontFamilyFallback.every((font) => fontFamily == font)) { - fontFamilies.addAll(fontFamilyFallback); - } - - properties.fontFamilies = fontFamilies; + properties.fontFamilies = + _getActualFontFamilies(fontFamily, fontFamilyFallback); if (fontWeight != null || fontStyle != null) { properties.fontStyle = toSkFontStyle(fontWeight, fontStyle); @@ -564,7 +538,6 @@ class CkParagraph extends ManagedSkiaObject @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 @@ -660,8 +633,52 @@ 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 _verifyFontsSupportText(String text) { + CkTextStyle style = _peekStyle(); + List fontFamilies = + _getActualFontFamilies(style.fontFamily, style.fontFamilyFallback); + List typefaces = []; + for (var font in fontFamilies) { + List? typefacesForFamily = + skiaFontCollection.familyToTypefaceMap[font]; + if (typefacesForFamily != null) { + typefaces.addAll(typefacesForFamily); + } + } + List codeUnits = text.codeUnits; + List codeUnitsSupported = List.filled(codeUnits.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(codeUnits[i]); + } + } + + if (codeUnitsSupported.any((x) => !x)) { + print('Some code units are not supported by any loaded font!!!!!'); + List missingCodeUnits = []; + for (int i = 0; i < codeUnitsSupported.length; i++) { + if (!codeUnitsSupported[i]) { + missingCodeUnits.add(codeUnits[i]); + print('We do not support the Unicode code point: ${codeUnits[i]}'); + _findFontsForMissingCodeunit(codeUnits[i]); + } + } + } + } + + /// 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) { + _verifyFontsSupportText(text); _commands.add(_ParagraphCommand.addText(text)); _paragraphBuilder.addText(text); } @@ -747,3 +764,17 @@ enum _ParagraphCommandType { pushStyle, addPlaceholder, } + +List _getActualFontFamilies(String? fontFamily, + [List? fontFamilyFallback]) { + if (fontFamily == null || + !skiaFontCollection.registeredFamilies.contains(fontFamily)) { + fontFamily = 'Roboto'; + } + List fontFamilies = [fontFamily]; + if (fontFamilyFallback != null && + !fontFamilyFallback.every((font) => fontFamily == font)) { + fontFamilies.addAll(fontFamilyFallback); + } + return fontFamilies; +} From 56675a0e65231afb055aa090e80cb3b54da688dc Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Wed, 2 Dec 2020 10:49:26 -0800 Subject: [PATCH 03/11] WIP on downloading Noto automatically --- .../src/engine/canvaskit/font_fallbacks.dart | 222 ++++++++++++++---- lib/web_ui/lib/src/engine/canvaskit/text.dart | 2 +- 2 files changed, 174 insertions(+), 50 deletions(-) diff --git a/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart b/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart index 2255cad792a09..f3e8b8ed278ac 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart @@ -5,10 +5,93 @@ // @dart = 2.12 part of engine; -void _findFontsForMissingCodeunit(int codeunit) { +Future _findFontsForMissingCodeunits(List codeunits) async { _ensureNotoFontTreeCreated(); - List families = _lookupNotoFontsForCodeunit(codeunit); - print('Fonts which match $codeunit: $families'); + Set<_NotoFont> fonts = <_NotoFont>{}; + for (int codeunit in codeunits) { + fonts.addAll(_lookupNotoFontsForCodeunit(codeunit)); + } + fonts = _findMinimumFontsForCodeunits(codeunits, fonts); + for (_NotoFont font in fonts) { + String googleFontCss = await html.window.fetch(font.googleFontsCssUrl).then( + (dynamic response) => + response.text().then((dynamic x) => x as String)); + print(googleFontCss); + } + print('Fonts which match missing code units: ${fonts.map((f) => f.name)}'); +} + +_NotoFont _makeResolvedNotoFontFromCss(String css, String family) { + List<_ResolvedNotoSubset> subsets = <_ResolvedNotoSubset>[]; + for (String line in LineSplitter.split(css)) { + } +} + +/// 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( + List codeunits, Set<_NotoFont> fonts) { + List unmatchedCodeunits = List.from(codeunits); + Set<_NotoFont> minimumFonts = <_NotoFont>{}; + List<_NotoFont> bestFonts = <_NotoFont>[]; + int maxCodeunitsCovered = 0; + + 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))) { + String language = html.window.navigator.language; + 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() { @@ -18,29 +101,29 @@ void _ensureNotoFontTreeCreated() { for (_NotoFont font in _notoFonts) { for (_UnicodeRange range in font.unicodeRanges) { - _insertNotoFontRange(range, font); + _notoTreeRoot = _insertNotoFontRange(range, font, _notoTreeRoot); } } } -List _lookupNotoFontsForCodeunit(int codeunit) { - List lookupHelper(_NotoTreeNode node) { +List<_NotoFont> _lookupNotoFontsForCodeunit(int codeunit) { + List<_NotoFont> lookupHelper(_NotoTreeNode<_NotoFont> node) { if (node.range.contains(codeunit)) { - return node.fonts.map((_NotoFont f) => f.name).toList(); + return node.fonts.toList(); } if (node.range.start > codeunit) { if (node.left != null) { return lookupHelper(node.left!); } else { print('Could not find font to match $codeunit'); - return []; + return <_NotoFont>[]; } } else { if (node.right != null) { return lookupHelper(node.right!); } else { print('Could not find font to match $codeunit'); - return []; + return <_NotoFont>[]; } } } @@ -54,6 +137,15 @@ class _NotoFont { const _NotoFont(this.name, this.unicodeRanges); + bool matchesCodeunit(int codeunit) { + for (_UnicodeRange range in unicodeRanges) { + if (range.contains(codeunit)) { + return true; + } + } + return false; + } + String get googleFontsCssUrl => 'https://fonts.googleapis.com/css2?family=${name.replaceAll(' ', '+')}'; } @@ -77,7 +169,58 @@ class _UnicodeRange { } } +class _ResolvedNotoFont { + final String name; + final List<_ResolvedNotoSubset> subsets; + + const _ResolvedNotoFont(this.name, this.subsets); +} + +class _ResolvedNotoSubset { + final String url; + final List<_UnicodeRange> ranges; + + const _ResolvedNotoSubset(this.url, this.ranges); +} + +const _NotoFont _notoSansSC = _NotoFont('Noto Sans SC', <_UnicodeRange>[ + _UnicodeRange(12288, 12591), + _UnicodeRange(12800, 13311), + _UnicodeRange(19968, 40959), + _UnicodeRange(65072, 65135), + _UnicodeRange(65280, 65519), +]); + +const _NotoFont _notoSansTC = _NotoFont('Noto Sans TC', <_UnicodeRange>[ + _UnicodeRange(12288, 12351), + _UnicodeRange(12549, 12585), + _UnicodeRange(19968, 40959), +]); + +const _NotoFont _notoSansHK = _NotoFont('Noto Sans HK', <_UnicodeRange>[ + _UnicodeRange(12288, 12351), + _UnicodeRange(12549, 12585), + _UnicodeRange(19968, 40959), +]); + +const _NotoFont _notoSansJP = _NotoFont('Noto Sans JP', <_UnicodeRange>[ + _UnicodeRange(12288, 12543), + _UnicodeRange(19968, 40959), + _UnicodeRange(65280, 65519), +]); + +const List<_NotoFont> _cjkFonts = <_NotoFont>[ + _notoSansSC, + _notoSansTC, + _notoSansHK, + _notoSansJP, +]; + const List<_NotoFont> _notoFonts = <_NotoFont>[ + _notoSansSC, + _notoSansTC, + _notoSansHK, + _notoSansJP, _NotoFont('Noto Naskh Arabic UI', <_UnicodeRange>[ _UnicodeRange(1536, 1791), _UnicodeRange(8204, 8206), @@ -103,23 +246,6 @@ const List<_NotoFont> _notoFonts = <_NotoFont>[ _UnicodeRange(8204, 8205), _UnicodeRange(9676, 9676), ]), - _NotoFont('Noto Sans HK', <_UnicodeRange>[ - _UnicodeRange(12288, 12351), - _UnicodeRange(12549, 12585), - _UnicodeRange(19968, 40959), - ]), - _NotoFont('Noto Sans SC', <_UnicodeRange>[ - _UnicodeRange(12288, 12591), - _UnicodeRange(12800, 13311), - _UnicodeRange(19968, 40959), - _UnicodeRange(65072, 65135), - _UnicodeRange(65280, 65519), - ]), - _NotoFont('Noto Sans TC', <_UnicodeRange>[ - _UnicodeRange(12288, 12351), - _UnicodeRange(12549, 12585), - _UnicodeRange(19968, 40959), - ]), _NotoFont('Noto Sans Egyptian Hieroglyphs', <_UnicodeRange>[ _UnicodeRange(77824, 78894), ]), @@ -167,11 +293,6 @@ const List<_NotoFont> _notoFonts = <_NotoFont>[ _UnicodeRange(43056, 43065), _UnicodeRange(43232, 43259), ]), - _NotoFont('Noto Sans JP', <_UnicodeRange>[ - _UnicodeRange(12288, 12543), - _UnicodeRange(19968, 40959), - _UnicodeRange(65280, 65519), - ]), _NotoFont('Noto Sans Kannada UI', <_UnicodeRange>[ _UnicodeRange(2404, 2405), _UnicodeRange(3202, 3314), @@ -282,43 +403,46 @@ const List<_NotoFont> _notoFonts = <_NotoFont>[ ]; /// A node in a red-black tree for Noto Fonts. -class _NotoTreeNode { - _NotoTreeNode? parent; - _NotoTreeNode? left; - _NotoTreeNode? right; +class _NotoTreeNode { + _NotoTreeNode? parent; + _NotoTreeNode? left; + _NotoTreeNode? right; /// If `true`, then this node is black. Otherwise it is red. bool isBlack = false; bool get isRed => !isBlack; final _UnicodeRange range; - final List<_NotoFont> fonts; + final List fonts; - _NotoTreeNode(this.range) : this.fonts = <_NotoFont>[]; + _NotoTreeNode(this.range) : this.fonts = []; } /// Associates [range] with [font] in the Noto Font tree. -void _insertNotoFontRange(_UnicodeRange range, _NotoFont font) { - _NotoTreeNode? newNode = - _insertNotoFontRangeHelper(_notoTreeRoot, range, font); +/// +/// Returns the root node. +_NotoTreeNode? _insertNotoFontRange( + _UnicodeRange range, T font, _NotoTreeNode? root) { + _NotoTreeNode? newNode = _insertNotoFontRangeHelper(root, range, font); if (newNode != null) { _repairNotoFontTree(newNode); // Make sure the root node is correctly set. - _NotoTreeNode newRoot = newNode; + _NotoTreeNode newRoot = newNode; while (newRoot.parent != null) { newRoot = newRoot.parent!; } - _notoTreeRoot = newRoot; + return newRoot; } + return root; } /// Recurses the font tree and associates [range] with [font]. /// /// If a new node is created, it is returned so we can repair the tree. -_NotoTreeNode? _insertNotoFontRangeHelper( - _NotoTreeNode? root, _UnicodeRange range, _NotoFont font) { +_NotoTreeNode? _insertNotoFontRangeHelper( + _NotoTreeNode? root, _UnicodeRange range, T font) { if (root != null) { if (root.range == range) { // The root node range is the same as the range we're inserting. @@ -327,9 +451,9 @@ _NotoTreeNode? _insertNotoFontRangeHelper( } if (range.start < root.range.start) { if (root.left != null) { - return _insertNotoFontRangeHelper(root.left, range, font); + return _insertNotoFontRangeHelper(root.left, range, font); } else { - _NotoTreeNode newNode = _NotoTreeNode(range); + _NotoTreeNode newNode = _NotoTreeNode(range); newNode.fonts.add(font); newNode.parent = root; root.left = newNode; @@ -339,7 +463,7 @@ _NotoTreeNode? _insertNotoFontRangeHelper( if (root.right != null) { return _insertNotoFontRangeHelper(root.right, range, font); } else { - _NotoTreeNode newNode = _NotoTreeNode(range); + _NotoTreeNode newNode = _NotoTreeNode(range); newNode.fonts.add(font); newNode.parent = root; root.right = newNode; @@ -349,7 +473,7 @@ _NotoTreeNode? _insertNotoFontRangeHelper( } else { // If [root] is null, then the root of the entire Noto Font tree is null. assert(_notoTreeRoot == null); - _NotoTreeNode newRoot = _NotoTreeNode(range); + _NotoTreeNode newRoot = _NotoTreeNode(range); newRoot.fonts.add(font); _notoTreeRoot = newRoot; return newRoot; @@ -458,4 +582,4 @@ void _repairNotoFontTree(_NotoTreeNode node) { grandparent.isBlack = false; } -_NotoTreeNode? _notoTreeRoot; +_NotoTreeNode<_NotoFont>? _notoTreeRoot; diff --git a/lib/web_ui/lib/src/engine/canvaskit/text.dart b/lib/web_ui/lib/src/engine/canvaskit/text.dart index 1d327eab21bd3..26b1197040a1e 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/text.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/text.dart @@ -665,9 +665,9 @@ class CkParagraphBuilder implements ui.ParagraphBuilder { if (!codeUnitsSupported[i]) { missingCodeUnits.add(codeUnits[i]); print('We do not support the Unicode code point: ${codeUnits[i]}'); - _findFontsForMissingCodeunit(codeUnits[i]); } } + _findFontsForMissingCodeunits(missingCodeUnits); } } From 46d0256e79ab900d4f28541e34831d407ac594c5 Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Tue, 15 Dec 2020 15:23:19 -0800 Subject: [PATCH 04/11] Download and use the Noto fonts as fallbacks --- .../src/engine/canvaskit/font_fallbacks.dart | 133 ++++++++++++++++-- .../lib/src/engine/canvaskit/fonts.dart | 11 ++ .../src/engine/canvaskit/initialization.dart | 16 ++- lib/web_ui/lib/src/engine/canvaskit/text.dart | 9 +- 4 files changed, 145 insertions(+), 24 deletions(-) diff --git a/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart b/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart index f3e8b8ed278ac..1ffa859c2f1d2 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart @@ -13,17 +13,96 @@ Future _findFontsForMissingCodeunits(List codeunits) async { } fonts = _findMinimumFontsForCodeunits(codeunits, fonts); for (_NotoFont font in fonts) { - String googleFontCss = await html.window.fetch(font.googleFontsCssUrl).then( - (dynamic response) => - response.text().then((dynamic x) => x as String)); - print(googleFontCss); + if (_resolvedNotoFonts[font] == null) { + String googleFontCss = await html.window + .fetch(font.googleFontsCssUrl) + .then((dynamic response) => + response.text().then((dynamic x) => x as String)); + final _ResolvedNotoFont resolvedFont = + _makeResolvedNotoFontFromCss(googleFontCss, font.name); + _registerResolvedFont(font, resolvedFont); + } + } + + Set<_ResolvedNotoSubset> resolvedFonts = <_ResolvedNotoSubset>{}; + for (int codeunit in codeunits) { + resolvedFonts.addAll(_lookupResolvedFontsForCodeunit(codeunit)); + } + + for (_ResolvedNotoSubset resolvedFont in resolvedFonts) { + skiaFontCollection.registerFallbackFont( + resolvedFont.url, resolvedFont.name); } - print('Fonts which match missing code units: ${fonts.map((f) => f.name)}'); + await skiaFontCollection.ensureFontsLoaded(); + // TODO(hterkelsen): This doesn't always cause us to re-render the paragraph + // with the new fonts. + EnginePlatformDispatcher.instance.invokeOnMetricsChanged(); } -_NotoFont _makeResolvedNotoFontFromCss(String css, String family) { +_ResolvedNotoFont _makeResolvedNotoFontFromCss(String css, String name) { List<_ResolvedNotoSubset> subsets = <_ResolvedNotoSubset>[]; - for (String line in LineSplitter.split(css)) { + bool resolvingFontFace = false; + String? fontFaceUrl; + List<_UnicodeRange>? 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) { + throw new Exception('Unable to resolve Noto font URL: $line'); + } + int urlEnd = line.indexOf(')'); + fontFaceUrl = line.substring(urlStart + 4, urlEnd); + } else if (line.startsWith(' unicode-range:')) { + fontFaceUnicodeRanges = <_UnicodeRange>[]; + String rangeString = line.substring(17, line.length - 1); + List rawRanges = rangeString.split(', '); + for (final String rawRange in rawRanges) { + List 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(_UnicodeRange(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(_UnicodeRange(startValue, endValue)); + } + } + } else if (line == '}') { + subsets.add( + _ResolvedNotoSubset(fontFaceUrl!, name, fontFaceUnicodeRanges!)); + resolvingFontFace = false; + } else { + continue; + } + } + } + + return _ResolvedNotoFont(name, subsets); +} + +void _registerResolvedFont(_NotoFont font, _ResolvedNotoFont resolvedFont) { + _resolvedNotoFonts[font] = resolvedFont; + + for (_ResolvedNotoSubset subset in resolvedFont.subsets) { + for (_UnicodeRange range in subset.ranges) { + _resolvedNotoTreeRoot = + _insertNotoFontRange(range, subset, _resolvedNotoTreeRoot); + } } } @@ -115,14 +194,12 @@ List<_NotoFont> _lookupNotoFontsForCodeunit(int codeunit) { if (node.left != null) { return lookupHelper(node.left!); } else { - print('Could not find font to match $codeunit'); return <_NotoFont>[]; } } else { if (node.right != null) { return lookupHelper(node.right!); } else { - print('Could not find font to match $codeunit'); return <_NotoFont>[]; } } @@ -131,6 +208,30 @@ List<_NotoFont> _lookupNotoFontsForCodeunit(int codeunit) { return lookupHelper(_notoTreeRoot!); } +List<_ResolvedNotoSubset> _lookupResolvedFontsForCodeunit(int codeunit) { + List<_ResolvedNotoSubset> lookupHelper( + _NotoTreeNode<_ResolvedNotoSubset> node) { + if (node.range.contains(codeunit)) { + return node.fonts.toList(); + } + if (node.range.start > codeunit) { + if (node.left != null) { + return lookupHelper(node.left!); + } else { + return <_ResolvedNotoSubset>[]; + } + } else { + if (node.right != null) { + return lookupHelper(node.right!); + } else { + return <_ResolvedNotoSubset>[]; + } + } + } + + return lookupHelper(_resolvedNotoTreeRoot!); +} + class _NotoFont { final String name; final List<_UnicodeRange> unicodeRanges; @@ -178,9 +279,10 @@ class _ResolvedNotoFont { class _ResolvedNotoSubset { final String url; + final String name; final List<_UnicodeRange> ranges; - const _ResolvedNotoSubset(this.url, this.ranges); + const _ResolvedNotoSubset(this.url, this.name, this.ranges); } const _NotoFont _notoSansSC = _NotoFont('Noto Sans SC', <_UnicodeRange>[ @@ -471,11 +573,9 @@ _NotoTreeNode? _insertNotoFontRangeHelper( } } } else { - // If [root] is null, then the root of the entire Noto Font tree is null. - assert(_notoTreeRoot == null); + // If [root] is null, then the tree is empty. Create a new root. _NotoTreeNode newRoot = _NotoTreeNode(range); newRoot.fonts.add(font); - _notoTreeRoot = newRoot; return newRoot; } } @@ -582,4 +682,11 @@ void _repairNotoFontTree(_NotoTreeNode node) { grandparent.isBlack = false; } +/// The root of the unresolved Noto font Red-Black Tree. _NotoTreeNode<_NotoFont>? _notoTreeRoot; + +/// The root of the resolved Noto font Red-Black Tree. +_NotoTreeNode<_ResolvedNotoSubset>? _resolvedNotoTreeRoot; + +Map<_NotoFont, _ResolvedNotoFont> _resolvedNotoFonts = + <_NotoFont, _ResolvedNotoFont>{}; diff --git a/lib/web_ui/lib/src/engine/canvaskit/fonts.dart b/lib/web_ui/lib/src/engine/canvaskit/fonts.dart index 97096aeb01556..6a6c84c8322eb 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/fonts.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/fonts.dart @@ -29,6 +29,8 @@ class SkiaFontCollection { final List globalFontFallbacks = []; + final Map _fontFallbackCounts = {}; + Future ensureFontsLoaded() async { await _loadFonts(); @@ -149,6 +151,15 @@ class SkiaFontCollection { return _RegisteredFont(bytes, family, actualFamily); } + void registerFallbackFont(String url, String family) { + _fontFallbackCounts.putIfAbsent(family, () => 0); + int fontFallbackTag = _fontFallbackCounts[family]!; + _fontFallbackCounts[family] = _fontFallbackCounts[family]! + 1; + String countedFamily = '$family $fontFallbackTag'; + _unloadedFonts.add(_registerFont(url, countedFamily)); + globalFontFallbacks.add(countedFamily); + } + String? _readActualFamilyName(Uint8List bytes) { final SkFontMgr tmpFontMgr = canvasKit.FontMgr.FromData([bytes])!; String? actualFamily = tmpFontMgr.getFamilyName(0); diff --git a/lib/web_ui/lib/src/engine/canvaskit/initialization.dart b/lib/web_ui/lib/src/engine/canvaskit/initialization.dart index 20a9ecd952c2e..fe31d83c49107 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/initialization.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/initialization.dart @@ -11,8 +11,7 @@ part of engine; external String? get requestedRendererType; /// Whether to use CanvasKit as the rendering backend. -bool get useCanvasKit => - _autoDetect ? _detectRenderer() : _useSkia; +bool get useCanvasKit => _autoDetect ? _detectRenderer() : _useSkia; /// Returns true if CanvasKit is used. /// @@ -42,8 +41,9 @@ const bool _useSkia = bool.fromEnvironment('FLUTTER_WEB_USE_SKIA', defaultValue: false); // If set to true, forces CPU-only rendering (i.e. no WebGL). -const bool canvasKitForceCpuOnly = - bool.fromEnvironment('FLUTTER_WEB_CANVASKIT_FORCE_CPU_ONLY', defaultValue: false); +const bool canvasKitForceCpuOnly = bool.fromEnvironment( + 'FLUTTER_WEB_CANVASKIT_FORCE_CPU_ONLY', + defaultValue: false); /// The URL to use when downloading the CanvasKit script and associated wasm. /// @@ -52,7 +52,7 @@ const bool canvasKitForceCpuOnly = /// NPM, update this URL to `https://unpkg.com/canvaskit-wasm@0.34.0/bin/`. const String canvasKitBaseUrl = String.fromEnvironment( 'FLUTTER_WEB_CANVASKIT_URL', - defaultValue: 'https://unpkg.com/canvaskit-wasm@0.19.0/bin/', + defaultValue: 'https://unpkg.com/canvaskit-wasm@0.20.0/bin/', ); /// Initialize CanvasKit. @@ -63,8 +63,10 @@ Future initializeCanvasKit() { late StreamSubscription loadSubscription; loadSubscription = domRenderer.canvasKitScript!.onLoad.listen((_) { loadSubscription.cancel(); - final CanvasKitInitPromise canvasKitInitPromise = CanvasKitInit(CanvasKitInitOptions( - locateFile: js.allowInterop((String file, String unusedBase) => canvasKitBaseUrl + file), + final CanvasKitInitPromise canvasKitInitPromise = + CanvasKitInit(CanvasKitInitOptions( + locateFile: js.allowInterop( + (String file, String unusedBase) => canvasKitBaseUrl + file), )); canvasKitInitPromise.then(js.allowInterop((CanvasKit ck) { canvasKit = ck; diff --git a/lib/web_ui/lib/src/engine/canvaskit/text.dart b/lib/web_ui/lib/src/engine/canvaskit/text.dart index c067f21b588ce..6116610285fda 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/text.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/text.dart @@ -659,12 +659,10 @@ class CkParagraphBuilder implements ui.ParagraphBuilder { } if (codeUnitsSupported.any((x) => !x)) { - print('Some code units are not supported by any loaded font!!!!!'); List missingCodeUnits = []; for (int i = 0; i < codeUnitsSupported.length; i++) { if (!codeUnitsSupported[i]) { missingCodeUnits.add(codeUnits[i]); - print('We do not support the Unicode code point: ${codeUnits[i]}'); } } _findFontsForMissingCodeunits(missingCodeUnits); @@ -729,8 +727,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 { @@ -785,5 +785,6 @@ List _getActualFontFamilies(String? fontFamily, !fontFamilyFallback.every((font) => fontFamily == font)) { fontFamilies.addAll(fontFamilyFallback); } + fontFamilies.addAll(skiaFontCollection.globalFontFallbacks); return fontFamilies; } From 92637112451fda499d72fa70ded13f8102f1663c Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Tue, 15 Dec 2020 17:00:45 -0800 Subject: [PATCH 05/11] Send a message to the framework letting it know that system fonts changed --- lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart b/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart index 1ffa859c2f1d2..7b756f5416c4b 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart @@ -34,9 +34,7 @@ Future _findFontsForMissingCodeunits(List codeunits) async { resolvedFont.url, resolvedFont.name); } await skiaFontCollection.ensureFontsLoaded(); - // TODO(hterkelsen): This doesn't always cause us to re-render the paragraph - // with the new fonts. - EnginePlatformDispatcher.instance.invokeOnMetricsChanged(); + sendFontChangeMessage(); } _ResolvedNotoFont _makeResolvedNotoFontFromCss(String css, String name) { From 9f7c158f244b67c17cbed82f1c8befc49a4054eb Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Thu, 17 Dec 2020 17:31:09 -0800 Subject: [PATCH 06/11] Respond to comments --- .../src/engine/canvaskit/font_fallbacks.dart | 325 ++++++++++++++---- .../src/engine/canvaskit/initialization.dart | 5 +- lib/web_ui/lib/src/engine/canvaskit/text.dart | 37 +- 3 files changed, 281 insertions(+), 86 deletions(-) diff --git a/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart b/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart index 7b756f5416c4b..6a86f53971142 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart @@ -5,13 +5,33 @@ // @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 _downloadedSymbolsAndEmoji = false; + +final Set codeUnitsWithNoKnownFont = {}; + Future _findFontsForMissingCodeunits(List 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 coveredCodeUnits = {}; + Set missingCodeUnits = {}; for (int codeunit in codeunits) { - fonts.addAll(_lookupNotoFontsForCodeunit(codeunit)); + List<_NotoFont> fontsForUnit = _lookupNotoFontsForCodeunit(codeunit); + fonts.addAll(fontsForUnit); + if (fontsForUnit.isNotEmpty) { + coveredCodeUnits.add(codeunit); + } else { + missingCodeUnits.add(codeunit); + } } - fonts = _findMinimumFontsForCodeunits(codeunits, fonts); + fonts = _findMinimumFontsForCodeunits(coveredCodeUnits, fonts); for (_NotoFont font in fonts) { if (_resolvedNotoFonts[font] == null) { String googleFontCss = await html.window @@ -25,7 +45,7 @@ Future _findFontsForMissingCodeunits(List codeunits) async { } Set<_ResolvedNotoSubset> resolvedFonts = <_ResolvedNotoSubset>{}; - for (int codeunit in codeunits) { + for (int codeunit in coveredCodeUnits) { resolvedFonts.addAll(_lookupResolvedFontsForCodeunit(codeunit)); } @@ -33,10 +53,49 @@ Future _findFontsForMissingCodeunits(List codeunits) async { skiaFontCollection.registerFallbackFont( resolvedFont.url, resolvedFont.name); } + + if (missingCodeUnits.isNotEmpty) { + if (!_downloadedSymbolsAndEmoji) { + await _registerSymbolsAndEmoji(); + } else { + html.window.console + .log('Could not find a Noto font to display all missing characters. ' + 'Please add a font asset for the missing characters.'); + codeUnitsWithNoKnownFont.addAll(missingCodeUnits); + } + } await skiaFontCollection.ensureFontsLoaded(); sendFontChangeMessage(); } +/// 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; @@ -98,10 +157,53 @@ void _registerResolvedFont(_NotoFont font, _ResolvedNotoFont resolvedFont) { for (_ResolvedNotoSubset subset in resolvedFont.subsets) { for (_UnicodeRange range in subset.ranges) { - _resolvedNotoTreeRoot = - _insertNotoFontRange(range, subset, _resolvedNotoTreeRoot); + _resolvedNotoTreeRoot = _insertNotoFontRange<_ResolvedNotoSubset>( + range, subset, _resolvedNotoTreeRoot); } } + + assert( + _verifyNotoTree(_resolvedNotoTreeRoot), + 'Resolved Noto tree is invalid: ' + '${_verifyNotoSubtree(_resolvedNotoTreeRoot).reason}'); +} + +/// 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 _registerSymbolsAndEmoji() async { + 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 html.window.fetch(symbolsUrl).then( + (dynamic response) => + response.text().then((dynamic x) => x as String)); + String emojiCss = await html.window.fetch(emojiUrl).then((dynamic response) => + response.text().then((dynamic x) => x as String)); + + String extractUrlFromCss(String css) { + for (final String line in LineSplitter.split(css)) { + if (line.startsWith(' src:')) { + int urlStart = line.indexOf('url('); + if (urlStart == -1) { + throw new Exception('Unable to resolve Noto font URL: $line'); + } + int urlEnd = line.indexOf(')'); + return line.substring(urlStart + 4, urlEnd); + } + } + throw Exception('Unable to determine URL for Noto font'); + } + + String symbolsFontUrl = extractUrlFromCss(symbolsCss); + String emojiFontUrl = extractUrlFromCss(emojiCss); + + skiaFontCollection.registerFallbackFont(symbolsFontUrl, 'Noto Sans Symbols'); + skiaFontCollection.registerFallbackFont( + emojiFontUrl, 'Noto Color Emoji Compat'); + _downloadedSymbolsAndEmoji = true; } /// Finds the minimum set of fonts which covers all of the [codeunits]. @@ -111,12 +213,16 @@ void _registerResolvedFont(_NotoFont font, _ResolvedNotoFont resolvedFont) { /// fonts match the same number of codeunits, we choose one based on the user's /// locale. Set<_NotoFont> _findMinimumFontsForCodeunits( - List codeunits, Set<_NotoFont> fonts) { + Iterable codeunits, Set<_NotoFont> fonts) { List unmatchedCodeunits = List.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; @@ -139,7 +245,6 @@ Set<_NotoFont> _findMinimumFontsForCodeunits( _NotoFont bestFont = bestFonts.first; if (bestFonts.length > 1) { if (bestFonts.every((font) => _cjkFonts.contains(font))) { - String language = html.window.navigator.language; if (language == 'zh-Hans' || language == 'zh-CN' || language == 'zh-SG' || @@ -178,27 +283,33 @@ void _ensureNotoFontTreeCreated() { for (_NotoFont font in _notoFonts) { for (_UnicodeRange range in font.unicodeRanges) { - _notoTreeRoot = _insertNotoFontRange(range, font, _notoTreeRoot); + _notoTreeRoot = + _insertNotoFontRange<_NotoFont>(range, font, _notoTreeRoot); } } + + assert( + _verifyNotoTree(_notoTreeRoot), + 'The Noto font tree is invalid: ' + '${_verifyNotoSubtree(_notoTreeRoot).reason}'); } List<_NotoFont> _lookupNotoFontsForCodeunit(int codeunit) { List<_NotoFont> lookupHelper(_NotoTreeNode<_NotoFont> node) { if (node.range.contains(codeunit)) { - return node.fonts.toList(); + return node.fonts; } if (node.range.start > codeunit) { if (node.left != null) { return lookupHelper(node.left!); } else { - return <_NotoFont>[]; + return const <_NotoFont>[]; } } else { if (node.right != null) { return lookupHelper(node.right!); } else { - return <_NotoFont>[]; + return const <_NotoFont>[]; } } } @@ -210,19 +321,19 @@ List<_ResolvedNotoSubset> _lookupResolvedFontsForCodeunit(int codeunit) { List<_ResolvedNotoSubset> lookupHelper( _NotoTreeNode<_ResolvedNotoSubset> node) { if (node.range.contains(codeunit)) { - return node.fonts.toList(); + return node.fonts; } if (node.range.start > codeunit) { if (node.left != null) { return lookupHelper(node.left!); } else { - return <_ResolvedNotoSubset>[]; + return const <_ResolvedNotoSubset>[]; } } else { if (node.right != null) { return lookupHelper(node.right!); } else { - return <_ResolvedNotoSubset>[]; + return const <_ResolvedNotoSubset>[]; } } } @@ -259,6 +370,7 @@ class _UnicodeRange { return start <= codeUnit && codeUnit <= end; } + @override bool operator ==(dynamic other) { if (other is! _UnicodeRange) { return false; @@ -266,6 +378,9 @@ class _UnicodeRange { _UnicodeRange range = other; return range.start == start && range.end == end; } + + @override + int get hashCode => ui.hashValues(start, end); } class _ResolvedNotoFont { @@ -498,10 +613,10 @@ const List<_NotoFont> _notoFonts = <_NotoFont>[ _UnicodeRange(7840, 7929), _UnicodeRange(8363, 8363), ]), -// Noto Sans Symbols -// Noto Color Emoji Compat ]; +// TODO(hterkelsen): Add unit tests for the Red-Black tree code. + /// A node in a red-black tree for Noto Fonts. class _NotoTreeNode { _NotoTreeNode? parent; @@ -521,7 +636,7 @@ class _NotoTreeNode { /// Associates [range] with [font] in the Noto Font tree. /// /// Returns the root node. -_NotoTreeNode? _insertNotoFontRange( +_NotoTreeNode _insertNotoFontRange( _UnicodeRange range, T font, _NotoTreeNode? root) { _NotoTreeNode? newNode = _insertNotoFontRangeHelper(root, range, font); if (newNode != null) { @@ -535,7 +650,7 @@ _NotoTreeNode? _insertNotoFontRange( return newRoot; } - return root; + return root!; } /// Recurses the font tree and associates [range] with [font]. @@ -550,6 +665,8 @@ _NotoTreeNode? _insertNotoFontRangeHelper( return null; } if (range.start < root.range.start) { + assert(range.end < root.range.start, + 'Overlapping Unicode range in Noto Tree'); if (root.left != null) { return _insertNotoFontRangeHelper(root.left, range, font); } else { @@ -560,8 +677,10 @@ _NotoTreeNode? _insertNotoFontRangeHelper( return newNode; } } else { + assert(root.range.end < range.start, + 'Overlapping Unicode range in Noto Tree'); if (root.right != null) { - return _insertNotoFontRangeHelper(root.right, range, font); + return _insertNotoFontRangeHelper(root.right, range, font); } else { _NotoTreeNode newNode = _NotoTreeNode(range); newNode.fonts.add(font); @@ -578,6 +697,53 @@ _NotoTreeNode? _insertNotoFontRangeHelper( } } +void _rotateLeft(_NotoTreeNode node) { + // We will only ever call this on nodes which have a right child. + _NotoTreeNode newNode = node.right!; + _NotoTreeNode? parent = node.parent; + + node.right = newNode.left; + newNode.left = node; + node.parent = newNode; + if (node.right != null) { + node.right!.parent = node; + } + + if (parent != null) { + if (node == parent.left) { + parent.left = newNode; + } else { + parent.right = newNode; + } + } + + newNode.parent = parent; +} + +void _rotateRight(_NotoTreeNode node) { + // We will only ever call this on nodes which have a left child. + _NotoTreeNode newNode = node.left!; + _NotoTreeNode? parent = node.parent; + + node.left = newNode.right; + newNode.right = node; + node.parent = newNode; + + if (node.left != null) { + node.left!.parent = node; + } + + if (parent != null) { + if (node == parent.left) { + parent.left = newNode; + } else { + parent.right = newNode; + } + } + + newNode.parent = parent; +} + void _repairNotoFontTree(_NotoTreeNode node) { if (node.parent == null) { // This is the root node. The root node must be black. @@ -604,66 +770,16 @@ void _repairNotoFontTree(_NotoTreeNode node) { if (uncle != null && uncle.isRed) { parent.isBlack = true; uncle.isBlack = true; + grandparent.isBlack = false; _repairNotoFontTree(grandparent); return; } - // If we've reached here, then the parent is red and the uncle is black - // (note: null leaves are considered black). We must re-balance the tree - // by rotating such that the node is in the grandparent position. - - void rotateLeft(_NotoTreeNode node) { - // We will only ever call this on nodes which have a right child. - _NotoTreeNode newNode = node.right!; - _NotoTreeNode? parent = node.parent; - - node.right = newNode.left; - newNode.left = node; - node.parent = newNode; - if (node.right != null) { - node.right!.parent = node; - } - - if (parent != null) { - if (node == parent.left) { - parent.left = newNode; - } else { - parent.right = newNode; - } - } - - newNode.parent = parent; - } - - void rotateRight(_NotoTreeNode node) { - // We will only ever call this on nodes which have a left child. - _NotoTreeNode newNode = node.left!; - _NotoTreeNode? parent = node.parent; - - node.left = newNode.right; - newNode.right = node; - node.parent = newNode; - - if (node.left != null) { - node.left!.parent = node; - } - - if (parent != null) { - if (node == parent.left) { - parent.left = newNode; - } else { - parent.right = newNode; - } - } - - newNode.parent = parent; - } - if (node == parent.right && parent == grandparent.left) { - rotateLeft(parent); + _rotateLeft(parent); node = node.left!; } else if (node == parent.left && parent == grandparent.right) { - rotateRight(parent); + _rotateRight(parent); node = node.right!; } @@ -671,15 +787,76 @@ void _repairNotoFontTree(_NotoTreeNode node) { grandparent = parent.parent!; if (node == parent.left) { - rotateRight(grandparent); + _rotateRight(grandparent); } else { - rotateLeft(grandparent); + _rotateLeft(grandparent); } parent.isBlack = true; grandparent.isBlack = false; } +bool _verifyNotoTree(_NotoTreeNode? root) { + _VerifyNotoTreeResult result = _verifyNotoSubtree(root); + return result.isValid; +} + +_VerifyNotoTreeResult _verifyNotoSubtree(_NotoTreeNode? node) { + if (node == null) { + // Leaves of the tree are represented as null nodes. Leaf nodes are black. + return _VerifyNotoTreeResult(true, 1); + } + if (node.parent == null) { + // This is the root node of the tree. The root node must be black. + if (!node.isBlack) { + return _VerifyNotoTreeResult(false, 0, 'Root node is red'); + } + } + + int blackNodesOnPath = 0; + if (node.isRed) { + // Both of a red tree node's children must be black. + if ((node.left != null && !node.left!.isBlack) || + (node.right != null && !node.right!.isBlack)) { + return _VerifyNotoTreeResult(false, -1, 'Red node has a red child'); + } + } else { + blackNodesOnPath = 1; + } + + _VerifyNotoTreeResult leftResult = _verifyNotoSubtree(node.left); + _VerifyNotoTreeResult rightResult = _verifyNotoSubtree(node.right); + + if (!leftResult.isValid) { + return leftResult; + } else if (!rightResult.isValid) { + return rightResult; + } else if (leftResult.blackNodesOnPath != rightResult.blackNodesOnPath) { + return _VerifyNotoTreeResult( + false, + -1, + "The number of black nodes on the path from " + "root to leaf isn't the same for all leaves"); + } + + return _VerifyNotoTreeResult( + true, blackNodesOnPath + leftResult.blackNodesOnPath); +} + +class _VerifyNotoTreeResult { + /// Whether or not the tree conforms to the Red-Black tree invariants. + final bool isValid; + + /// The number of black nodes on the path from the node to the root. + final int blackNodesOnPath; + + /// A human-readable reason why the tree is invalid. + final String? reason; + + const _VerifyNotoTreeResult(this.isValid, this.blackNodesOnPath, + [this.reason]); +} + /// The root of the unresolved Noto font Red-Black Tree. _NotoTreeNode<_NotoFont>? _notoTreeRoot; diff --git a/lib/web_ui/lib/src/engine/canvaskit/initialization.dart b/lib/web_ui/lib/src/engine/canvaskit/initialization.dart index fe31d83c49107..b62eb96acebbe 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/initialization.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/initialization.dart @@ -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); diff --git a/lib/web_ui/lib/src/engine/canvaskit/text.dart b/lib/web_ui/lib/src/engine/canvaskit/text.dart index 6116610285fda..79339e4e527dd 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/text.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/text.dart @@ -62,7 +62,7 @@ class CkParagraphStyle implements ui.ParagraphStyle { skTextStyle.fontSize = fontSize; } - skTextStyle.fontFamilies = _getActualFontFamilies(fontFamily); + skTextStyle.fontFamilies = _getEffectiveFontFamilies(fontFamily); return skTextStyle; } @@ -71,7 +71,7 @@ class CkParagraphStyle implements ui.ParagraphStyle { EngineStrutStyle style = value as EngineStrutStyle; final SkStrutStyleProperties skStrutStyle = SkStrutStyleProperties(); skStrutStyle.fontFamilies = - _getActualFontFamilies(style._fontFamily, style._fontFamilyFallback); + _getEffectiveFontFamilies(style._fontFamily, style._fontFamilyFallback); if (style._fontSize != null) { skStrutStyle.fontSize = style._fontSize; @@ -264,7 +264,7 @@ class CkTextStyle implements ui.TextStyle { } properties.fontFamilies = - _getActualFontFamilies(fontFamily, fontFamilyFallback); + _getEffectiveFontFamilies(fontFamily, fontFamilyFallback); if (fontWeight != null || fontStyle != null) { properties.fontStyle = toSkFontStyle(fontWeight, fontStyle); @@ -635,10 +635,24 @@ class CkParagraphBuilder implements ui.ParagraphBuilder { /// Determines if the given [text] contains any code points which are not /// supported by the current set of fonts. - void _verifyFontsSupportText(String text) { + 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 fontFamilies = - _getActualFontFamilies(style.fontFamily, style.fontFamilyFallback); + _getEffectiveFontFamilies(style.fontFamily, style.fontFamilyFallback); List typefaces = []; for (var font in fontFamilies) { List? typefacesForFamily = @@ -647,14 +661,15 @@ class CkParagraphBuilder implements ui.ParagraphBuilder { typefaces.addAll(typefacesForFamily); } } - List codeUnits = text.codeUnits; - List codeUnitsSupported = List.filled(codeUnits.length, false); + // List codeUnits = text.codeUnits; + List codeUnitsSupported = List.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(codeUnits[i]); + codeUnitsSupported[i] |= + glyphs[i] != 0 || _isControlCode(text.codeUnitAt(i)); } } @@ -662,7 +677,7 @@ class CkParagraphBuilder implements ui.ParagraphBuilder { List missingCodeUnits = []; for (int i = 0; i < codeUnitsSupported.length; i++) { if (!codeUnitsSupported[i]) { - missingCodeUnits.add(codeUnits[i]); + missingCodeUnits.add(text.codeUnitAt(i)); } } _findFontsForMissingCodeunits(missingCodeUnits); @@ -676,7 +691,7 @@ class CkParagraphBuilder implements ui.ParagraphBuilder { @override void addText(String text) { - _verifyFontsSupportText(text); + _ensureFontsSupportText(text); _commands.add(_ParagraphCommand.addText(text)); _paragraphBuilder.addText(text); } @@ -774,7 +789,7 @@ enum _ParagraphCommandType { addPlaceholder, } -List _getActualFontFamilies(String? fontFamily, +List _getEffectiveFontFamilies(String? fontFamily, [List? fontFamilyFallback]) { if (fontFamily == null || !skiaFontCollection.registeredFamilies.contains(fontFamily)) { From fec943b38fb0f5f56a53b36d4a2984921662ca8e Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Thu, 17 Dec 2020 17:41:12 -0800 Subject: [PATCH 07/11] Update licenses --- ci/licenses_golden/licenses_flutter | 1 + 1 file changed, 1 insertion(+) diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 07da1f547b35a..816a7f1c99122 100755 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -436,6 +436,7 @@ 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 From a4d132c639168e9b92d76467b9fe1dd4aa84ac4b Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Mon, 28 Dec 2020 17:06:40 -0800 Subject: [PATCH 08/11] Fix tests --- .../src/engine/canvaskit/canvaskit_api.dart | 27 ++++++++++++------- .../lib/src/engine/canvaskit/image.dart | 2 +- lib/web_ui/lib/src/engine/canvaskit/path.dart | 4 +-- .../test/canvaskit/canvaskit_api_test.dart | 4 +-- 4 files changed, 22 insertions(+), 15 deletions(-) diff --git a/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart b/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart index f4bf266ee1252..ddb8e6e5efacb 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart @@ -49,7 +49,7 @@ class CanvasKit { external SkMaskFilterNamespace get MaskFilter; external SkColorFilterNamespace get ColorFilter; external SkImageFilterNamespace get ImageFilter; - external SkPath MakePathFromOp(SkPath path1, SkPath path2, SkPathOp pathOp); + external SkPathNamespace get Path; external SkTonalColors computeTonalColors(SkTonalColors inTonalColors); external SkVertices MakeVertices( SkVertexMode mode, @@ -96,10 +96,6 @@ class CanvasKit { external SkSurface MakeSWCanvasSurface(html.CanvasElement canvas); external void setCurrentContext(int glContext); - /// Creates an [SkPath] using commands obtained from [SkPath.toCmds]. - // TODO(yjbanov): switch to CanvasKit.Path.MakeFromCmds when it's available. - external SkPath MakePathFromCmds(List pathCommands); - /// Creates an image from decoded pixels represented as a list of bytes. /// /// The pixel data must match the [width], [height], [alphaType], [colorType], @@ -723,7 +719,7 @@ class SkImage { SkTileMode tileModeY, Float32List? matrix, // 3x3 matrix ); - external Uint8List readPixels(SkImageInfo imageInfo, int srcX, int srcY); + external Uint8List readPixels(int srcX, int srcY, SkImageInfo imageInfo); external SkData encodeToData(); external bool isAliasOf(SkImage other); external bool isDeleted(); @@ -783,7 +779,7 @@ class SkShader { @JS() class SkMaskFilterNamespace { external SkMaskFilter MakeBlur( - SkBlurStyle blurStyle, double sigma, bool respectCTM); + SkBlurStyle blurStyle, double sigma, bool respectCTM); } // This needs to be bound to top-level because SkPaint is initialized @@ -864,6 +860,14 @@ class SkImageFilter { external void delete(); } +@JS() +class SkPathNamespace { + external SkPath MakeFromOp(SkPath path1, SkPath path2, SkPathOp pathOp); + + /// Creates an [SkPath] using commands obtained from [SkPath.toCmds]. + external SkPath MakeFromCmds(List pathCommands); +} + // Mappings from SkMatrix-index to input-index. const List _skMatrixIndexToMatrix4Index = [ 0, 4, 12, // Row 1 @@ -1183,7 +1187,8 @@ class SkPath { /// Serializes the path into a list of commands. /// - /// The list can be used to create a new [SkPath] using [CanvasKit.MakePathFromCmds]. + /// The list can be used to create a new [SkPath] using + /// [CanvasKit.Path.MakeFromCmds]. external List toCmds(); external void delete(); @@ -1786,7 +1791,8 @@ abstract class Collector { /// Uses timers to delete objects in batches and outside the animation frame. class ProductionCollector implements Collector { ProductionCollector() { - _skObjectFinalizationRegistry = SkObjectFinalizationRegistry(js.allowInterop((SkDeletable deletable) { + _skObjectFinalizationRegistry = + SkObjectFinalizationRegistry(js.allowInterop((SkDeletable deletable) { // This is called when GC decides to collect the wrapper object and // notify us, which may happen after the object is already deleted // explicitly, e.g. when its ref count drops to zero. When that happens @@ -1967,7 +1973,8 @@ bool browserSupportsFinalizationRegistry = /// Sets the value of [browserSupportsFinalizationRegistry] to its true value. void debugResetBrowserSupportsFinalizationRegistry() { - browserSupportsFinalizationRegistry = _finalizationRegistryConstructor != null; + browserSupportsFinalizationRegistry = + _finalizationRegistryConstructor != null; } @JS() diff --git a/lib/web_ui/lib/src/engine/canvaskit/image.dart b/lib/web_ui/lib/src/engine/canvaskit/image.dart index 3617924da5540..3e7875b4b77be 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/image.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/image.dart @@ -300,7 +300,7 @@ class CkImage implements ui.Image, StackTraceDebugger { width: skImage.width(), height: skImage.height(), ); - bytes = skImage.readPixels(imageInfo, 0, 0); + bytes = skImage.readPixels(0, 0, imageInfo); } else { final SkData skData = skImage.encodeToData(); //defaults to PNG 100% // make a copy that we can return diff --git a/lib/web_ui/lib/src/engine/canvaskit/path.dart b/lib/web_ui/lib/src/engine/canvaskit/path.dart index 12626d5ec354e..e8518dc6e6828 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/path.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/path.dart @@ -265,7 +265,7 @@ class CkPath extends ManagedSkiaObject implements ui.Path { ) { final CkPath path1 = uiPath1 as CkPath; final CkPath path2 = uiPath2 as CkPath; - final SkPath newPath = canvasKit.MakePathFromOp( + final SkPath newPath = canvasKit.Path.MakeFromOp( path1.skiaObject, path2.skiaObject, toSkPathOp(operation), @@ -320,7 +320,7 @@ class CkPath extends ManagedSkiaObject implements ui.Path { @override SkPath resurrect() { - final SkPath path = canvasKit.MakePathFromCmds(_cachedCommands!); + final SkPath path = canvasKit.Path.MakeFromCmds(_cachedCommands!); path.setFillType(toSkFillType(_fillType)); return path; } diff --git a/lib/web_ui/test/canvaskit/canvaskit_api_test.dart b/lib/web_ui/test/canvaskit/canvaskit_api_test.dart index b70182ac85b1d..1fdf26f09699c 100644 --- a/lib/web_ui/test/canvaskit/canvaskit_api_test.dart +++ b/lib/web_ui/test/canvaskit/canvaskit_api_test.dart @@ -836,7 +836,7 @@ void _pathTests() { expect(measure2, isNull); }); - test('SkPath.toCmds and CanvasKit.MakePathFromCmds', () { + test('SkPath.toCmds and CanvasKit.Path.MakeFromCmds', () { const ui.Rect rect = ui.Rect.fromLTRB(0, 0, 10, 10); final SkPath path = SkPath(); path.addRect(toSkRect(rect)); @@ -848,7 +848,7 @@ void _pathTests() { [5], // close ]); - final SkPath copy = canvasKit.MakePathFromCmds(path.toCmds()); + final SkPath copy = canvasKit.Path.MakeFromCmds(path.toCmds()); expect(fromSkRect(copy.getBounds()), rect); }); } From a719068edc9b53f6bf33d1c536286d42b5b49a36 Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Tue, 29 Dec 2020 11:37:59 -0800 Subject: [PATCH 09/11] Fix drawPoints --- .../lib/src/engine/canvaskit/canvas.dart | 51 +++++++++++-------- .../src/engine/canvaskit/canvaskit_api.dart | 2 +- 2 files changed, 31 insertions(+), 22 deletions(-) diff --git a/lib/web_ui/lib/src/engine/canvaskit/canvas.dart b/lib/web_ui/lib/src/engine/canvaskit/canvas.dart index 4333a15c80631..5c372178b7489 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/canvas.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/canvas.dart @@ -176,7 +176,8 @@ class CkCanvas { void drawPoints(CkPaint paint, ui.PointMode pointMode, Float32List points) { skCanvas.drawPoints( toSkPointMode(pointMode), - points, + // TODO(hterkelsen): Don't convert this to 2d after we move to CK 0.21. + rawPointsToSkPoints2d(points), paint.skiaObject, ); } @@ -192,10 +193,10 @@ class CkCanvas { skCanvas.drawRect(toSkRect(rect), paint.skiaObject); } - void drawShadow(CkPath path, ui.Color color, double elevation, - bool transparentOccluder) { - drawSkShadow(skCanvas, path, color, elevation, - transparentOccluder, ui.window.devicePixelRatio); + void drawShadow( + CkPath path, ui.Color color, double elevation, bool transparentOccluder) { + drawSkShadow(skCanvas, path, color, elevation, transparentOccluder, + ui.window.devicePixelRatio); } void drawVertices( @@ -237,7 +238,8 @@ class CkCanvas { } void saveLayerWithFilter(ui.Rect bounds, ui.ImageFilter filter) { - final _CkManagedSkImageFilterConvertible convertible = filter as _CkManagedSkImageFilterConvertible; + final _CkManagedSkImageFilterConvertible convertible = + filter as _CkManagedSkImageFilterConvertible; return skCanvas.saveLayer( null, toSkRect(bounds), @@ -267,8 +269,8 @@ class CkCanvas { class RecordingCkCanvas extends CkCanvas { RecordingCkCanvas(SkCanvas skCanvas, ui.Rect bounds) - : pictureSnapshot = CkPictureSnapshot(bounds), - super(skCanvas); + : pictureSnapshot = CkPictureSnapshot(bounds), + super(skCanvas); @override final CkPictureSnapshot pictureSnapshot; @@ -310,7 +312,8 @@ class RecordingCkCanvas extends CkCanvas { CkPaint paint, ) { super.drawArc(oval, startAngle, sweepAngle, useCenter, paint); - _addCommand(CkDrawArcCommand(oval, startAngle, sweepAngle, useCenter, paint)); + _addCommand( + CkDrawArcCommand(oval, startAngle, sweepAngle, useCenter, paint)); } @override @@ -323,7 +326,8 @@ class RecordingCkCanvas extends CkCanvas { ui.BlendMode blendMode, ) { super.drawAtlasRaw(paint, atlas, rstTransforms, rects, colors, blendMode); - _addCommand(CkDrawAtlasCommand(paint, atlas, rstTransforms, rects, colors, blendMode)); + _addCommand(CkDrawAtlasCommand( + paint, atlas, rstTransforms, rects, colors, blendMode)); } @override @@ -418,10 +422,11 @@ class RecordingCkCanvas extends CkCanvas { } @override - void drawShadow(CkPath path, ui.Color color, double elevation, - bool transparentOccluder) { + void drawShadow( + CkPath path, ui.Color color, double elevation, bool transparentOccluder) { super.drawShadow(path, color, elevation, transparentOccluder); - _addCommand(CkDrawShadowCommand(path, color, elevation, transparentOccluder)); + _addCommand( + CkDrawShadowCommand(path, color, elevation, transparentOccluder)); } @override @@ -627,7 +632,7 @@ class CkTransformCommand extends CkPaintCommand { @override void apply(SkCanvas canvas) { canvas.concat(toSkMatrixFromFloat32(matrix4)); - } + } } class CkSkewCommand extends CkPaintCommand { @@ -660,7 +665,8 @@ class CkClipRectCommand extends CkPaintCommand { } class CkDrawArcCommand extends CkPaintCommand { - CkDrawArcCommand(this.oval, this.startAngle, this.sweepAngle, this.useCenter, this.paint); + CkDrawArcCommand( + this.oval, this.startAngle, this.sweepAngle, this.useCenter, this.paint); final ui.Rect oval; final double startAngle; @@ -682,7 +688,8 @@ class CkDrawArcCommand extends CkPaintCommand { } class CkDrawAtlasCommand extends CkPaintCommand { - CkDrawAtlasCommand(this.paint, this.atlas, this.rstTransforms, this.rects, this.colors, this.blendMode); + CkDrawAtlasCommand(this.paint, this.atlas, this.rstTransforms, this.rects, + this.colors, this.blendMode); final CkPaint paint; final CkImage atlas; @@ -807,7 +814,8 @@ class CkDrawPointsCommand extends CkPaintCommand { void apply(SkCanvas canvas) { canvas.drawPoints( toSkPointMode(pointMode), - points, + // TODO(hterkelsen): Don't convert this to 2d after we move to CK 0.21. + rawPointsToSkPoints2d(points), paint.skiaObject, ); } @@ -924,7 +932,7 @@ class CkDrawImageCommand extends CkPaintCommand { final CkPaint paint; CkDrawImageCommand(CkImage image, this.offset, this.paint) - : this.image = image.clone(); + : this.image = image.clone(); @override void apply(SkCanvas canvas) { @@ -949,7 +957,7 @@ class CkDrawImageRectCommand extends CkPaintCommand { final CkPaint paint; CkDrawImageRectCommand(CkImage image, this.src, this.dst, this.paint) - : this.image = image.clone(); + : this.image = image.clone(); @override void apply(SkCanvas canvas) { @@ -970,7 +978,7 @@ class CkDrawImageRectCommand extends CkPaintCommand { class CkDrawImageNineCommand extends CkPaintCommand { CkDrawImageNineCommand(CkImage image, this.center, this.dst, this.paint) - : this.image = image.clone(); + : this.image = image.clone(); final CkImage image; final ui.Rect center; @@ -1061,7 +1069,8 @@ class CkSaveLayerWithFilterCommand extends CkPaintCommand { @override void apply(SkCanvas canvas) { - final _CkManagedSkImageFilterConvertible convertible = filter as _CkManagedSkImageFilterConvertible; + final _CkManagedSkImageFilterConvertible convertible = + filter as _CkManagedSkImageFilterConvertible; return canvas.saveLayer( null, toSkRect(bounds), diff --git a/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart b/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart index ddb8e6e5efacb..eca86072b0d96 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart @@ -1409,7 +1409,7 @@ class SkCanvas { ); external void drawPoints( SkPointMode pointMode, - Float32List points, + List points, SkPaint paint, ); external void drawRRect( From 18a0914a2876ca64fe6a6ddfa7398ef20c80704a Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Tue, 29 Dec 2020 12:16:46 -0800 Subject: [PATCH 10/11] Fix API test --- lib/web_ui/test/canvaskit/canvaskit_api_test.dart | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/web_ui/test/canvaskit/canvaskit_api_test.dart b/lib/web_ui/test/canvaskit/canvaskit_api_test.dart index 1fdf26f09699c..b9dc86f9f8622 100644 --- a/lib/web_ui/test/canvaskit/canvaskit_api_test.dart +++ b/lib/web_ui/test/canvaskit/canvaskit_api_test.dart @@ -1055,7 +1055,14 @@ void _canvasTests() { test('drawPoints', () { canvas.drawPoints( canvasKit.PointMode.Lines, - Float32List.fromList([0, 0, 10, 10, 0, 10]), + [ + Float32List.fromList([0, 0]), + Float32List.fromList([ + 10, + 10, + ]), + Float32List.fromList([0, 10]), + ], SkPaint(), ); }); From 4aa97c51b5d62bacf56b47f7275799cf1f19fdee Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Tue, 29 Dec 2020 12:18:52 -0800 Subject: [PATCH 11/11] Update formatting --- lib/web_ui/test/canvaskit/canvaskit_api_test.dart | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/web_ui/test/canvaskit/canvaskit_api_test.dart b/lib/web_ui/test/canvaskit/canvaskit_api_test.dart index b9dc86f9f8622..46743857615e7 100644 --- a/lib/web_ui/test/canvaskit/canvaskit_api_test.dart +++ b/lib/web_ui/test/canvaskit/canvaskit_api_test.dart @@ -1057,10 +1057,7 @@ void _canvasTests() { canvasKit.PointMode.Lines, [ Float32List.fromList([0, 0]), - Float32List.fromList([ - 10, - 10, - ]), + Float32List.fromList([10, 10]), Float32List.fromList([0, 10]), ], SkPaint(),