From 680ef20110d369bdffa2695c224ba005868cc54d Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Wed, 30 Dec 2020 16:20:18 -0800 Subject: [PATCH 01/14] Revert "Revert "[CanvasKit] Automatically fall back to Noto fonts (#23096)" (#23357)" This reverts commit f9f4d0168dc6e03324844db942a4bca717ebb2e0. --- ci/licenses_golden/licenses_flutter | 1 + lib/web_ui/lib/src/engine.dart | 1 + .../lib/src/engine/canvaskit/canvas.dart | 51 +- .../src/engine/canvaskit/canvaskit_api.dart | 29 +- .../src/engine/canvaskit/font_fallbacks.dart | 867 ++++++++++++++++++ .../lib/src/engine/canvaskit/fonts.dart | 29 +- .../lib/src/engine/canvaskit/image.dart | 2 +- .../src/engine/canvaskit/initialization.dart | 21 +- lib/web_ui/lib/src/engine/canvaskit/path.dart | 4 +- lib/web_ui/lib/src/engine/canvaskit/text.dart | 115 ++- .../test/canvaskit/canvaskit_api_test.dart | 10 +- 11 files changed, 1049 insertions(+), 81 deletions(-) create mode 100644 lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 177517471df03..1be248830ed8c 100755 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -1209,6 +1209,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 diff --git a/lib/web_ui/lib/src/engine.dart b/lib/web_ui/lib/src/engine.dart index 42040eb1afa56..1069a34fe6fba 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/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 f4bf266ee1252..eca86072b0d96 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(); @@ -1404,7 +1409,7 @@ class SkCanvas { ); external void drawPoints( SkPointMode pointMode, - Float32List points, + List points, SkPaint paint, ); external void drawRRect( @@ -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/font_fallbacks.dart b/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart new file mode 100644 index 0000000000000..6a86f53971142 --- /dev/null +++ b/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart @@ -0,0 +1,867 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// @dart = 2.12 +part of engine; + +/// Whether or not "Noto Sans Symbols" and "Noto Color Emoji" fonts have been +/// downloaded. We download these as fallbacks when no other font covers the +/// given code units. +bool _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) { + List<_NotoFont> fontsForUnit = _lookupNotoFontsForCodeunit(codeunit); + fonts.addAll(fontsForUnit); + if (fontsForUnit.isNotEmpty) { + coveredCodeUnits.add(codeunit); + } else { + missingCodeUnits.add(codeunit); + } + } + fonts = _findMinimumFontsForCodeunits(coveredCodeUnits, fonts); + for (_NotoFont font in fonts) { + 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 coveredCodeUnits) { + resolvedFonts.addAll(_lookupResolvedFontsForCodeunit(codeunit)); + } + + for (_ResolvedNotoSubset resolvedFont in resolvedFonts) { + 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; + 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<_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]. +/// +/// Since set cover is NP-complete, we approximate using a greedy algorithm +/// which finds the font which covers the most codeunits. If multiple CJK +/// fonts match the same number of codeunits, we choose one based on the user's +/// locale. +Set<_NotoFont> _findMinimumFontsForCodeunits( + Iterable 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; + for (int codeunit in unmatchedCodeunits) { + if (font.matchesCodeunit(codeunit)) { + codeunitsCovered++; + } + } + if (codeunitsCovered > maxCodeunitsCovered) { + bestFonts.clear(); + bestFonts.add(font); + maxCodeunitsCovered = codeunitsCovered; + } else if (codeunitsCovered == maxCodeunitsCovered) { + bestFonts.add(font); + } + } + assert(bestFonts.isNotEmpty); + // If the list of best fonts are all CJK fonts, choose the best one based + // on locale. Otherwise just choose the first font. + _NotoFont bestFont = bestFonts.first; + if (bestFonts.length > 1) { + if (bestFonts.every((font) => _cjkFonts.contains(font))) { + if (language == 'zh-Hans' || + language == 'zh-CN' || + language == 'zh-SG' || + language == 'zh-MY') { + if (bestFonts.contains(_notoSansSC)) { + bestFont = _notoSansSC; + } + } else if (language == 'zh-Hant' || + language == 'zh-TW' || + language == 'zh-MO') { + if (bestFonts.contains(_notoSansTC)) { + bestFont = _notoSansTC; + } + } else if (language == 'zh-HK') { + if (bestFonts.contains(_notoSansHK)) { + bestFont = _notoSansHK; + } + } else if (language == 'ja') { + if (bestFonts.contains(_notoSansJP)) { + bestFont = _notoSansJP; + } + } + } + } + unmatchedCodeunits + .removeWhere((codeunit) => bestFont.matchesCodeunit(codeunit)); + minimumFonts.add(bestFont); + } + return minimumFonts; +} + +void _ensureNotoFontTreeCreated() { + if (_notoTreeRoot != null) { + return; + } + + for (_NotoFont font in _notoFonts) { + for (_UnicodeRange range in font.unicodeRanges) { + _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; + } + if (node.range.start > codeunit) { + if (node.left != null) { + return lookupHelper(node.left!); + } else { + return const <_NotoFont>[]; + } + } else { + if (node.right != null) { + return lookupHelper(node.right!); + } else { + return const <_NotoFont>[]; + } + } + } + + return lookupHelper(_notoTreeRoot!); +} + +List<_ResolvedNotoSubset> _lookupResolvedFontsForCodeunit(int codeunit) { + List<_ResolvedNotoSubset> lookupHelper( + _NotoTreeNode<_ResolvedNotoSubset> node) { + if (node.range.contains(codeunit)) { + return node.fonts; + } + if (node.range.start > codeunit) { + if (node.left != null) { + return lookupHelper(node.left!); + } else { + return const <_ResolvedNotoSubset>[]; + } + } else { + if (node.right != null) { + return lookupHelper(node.right!); + } else { + return const <_ResolvedNotoSubset>[]; + } + } + } + + return lookupHelper(_resolvedNotoTreeRoot!); +} + +class _NotoFont { + final String name; + final List<_UnicodeRange> unicodeRanges; + + 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(' ', '+')}'; +} + +class _UnicodeRange { + final int start; + final int end; + + const _UnicodeRange(this.start, this.end); + + bool contains(int codeUnit) { + return start <= codeUnit && codeUnit <= end; + } + + @override + bool operator ==(dynamic other) { + if (other is! _UnicodeRange) { + return false; + } + _UnicodeRange range = other; + return range.start == start && range.end == end; + } + + @override + int get hashCode => ui.hashValues(start, end); +} + +class _ResolvedNotoFont { + final String name; + final List<_ResolvedNotoSubset> subsets; + + const _ResolvedNotoFont(this.name, this.subsets); +} + +class _ResolvedNotoSubset { + final String url; + final String name; + final List<_UnicodeRange> ranges; + + const _ResolvedNotoSubset(this.url, this.name, 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), + _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 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 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), + ]), +]; + +// 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; + _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 fonts; + + _NotoTreeNode(this.range) : this.fonts = []; +} + +/// Associates [range] with [font] in the Noto Font tree. +/// +/// 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; + while (newRoot.parent != null) { + newRoot = newRoot.parent!; + } + + 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, T 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) { + assert(range.end < root.range.start, + 'Overlapping Unicode range in Noto Tree'); + 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 { + assert(root.range.end < range.start, + 'Overlapping Unicode range in Noto Tree'); + 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 tree is empty. Create a new root. + _NotoTreeNode newRoot = _NotoTreeNode(range); + newRoot.fonts.add(font); + return newRoot; + } +} + +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. + 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; + grandparent.isBlack = false; + _repairNotoFontTree(grandparent); + return; + } + + 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; +} + +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; + +/// 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 2a5589d61ba80..6a6c84c8322eb 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/fonts.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/fonts.dart @@ -24,13 +24,24 @@ class SkiaFontCollection { final Set registeredFamilies = {}; + final Map> familyToTypefaceMap = + >{}; + + final List globalFontFallbacks = []; + + final Map _fontFallbackCounts = {}; + 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); } } @@ -140,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); @@ -169,5 +189,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/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/initialization.dart b/lib/web_ui/lib/src/engine/canvaskit/initialization.dart index 20a9ecd952c2e..b62eb96acebbe 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. /// @@ -41,9 +40,13 @@ 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). -const bool canvasKitForceCpuOnly = - bool.fromEnvironment('FLUTTER_WEB_CANVASKIT_FORCE_CPU_ONLY', defaultValue: false); +/// 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); /// The URL to use when downloading the CanvasKit script and associated wasm. /// @@ -52,7 +55,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 +66,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/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/lib/src/engine/canvaskit/text.dart b/lib/web_ui/lib/src/engine/canvaskit/text.dart index cb4773abdaec2..79339e4e527dd 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 = _getEffectiveFontFamilies(fontFamily); return skTextStyle; } @@ -74,20 +70,8 @@ class CkParagraphStyle implements ui.ParagraphStyle { static SkStrutStyleProperties toSkStrutStyleProperties(ui.StrutStyle value) { EngineStrutStyle style = value as EngineStrutStyle; final SkStrutStyleProperties skStrutStyle = SkStrutStyleProperties(); - if (style._fontFamily != null) { - String fontFamily = style._fontFamily!; - if (!skiaFontCollection.registeredFamilies.contains(fontFamily)) { - fontFamily = 'Roboto'; - } - final List 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 = + _getEffectiveFontFamilies(style._fontFamily, style._fontFamilyFallback); if (style._fontSize != null) { skStrutStyle.fontSize = style._fontSize; @@ -279,18 +263,8 @@ class CkTextStyle implements ui.TextStyle { properties.locale = locale.toLanguageTag(); } - if (fontFamily == null || - !skiaFontCollection.registeredFamilies.contains(fontFamily)) { - fontFamily = 'Roboto'; - } - - List fontFamilies = [fontFamily]; - if (fontFamilyFallback != null && - !fontFamilyFallback.every((font) => fontFamily == font)) { - fontFamilies.addAll(fontFamilyFallback); - } - - properties.fontFamilies = fontFamilies; + properties.fontFamilies = + _getEffectiveFontFamilies(fontFamily, fontFamilyFallback); if (fontWeight != null || fontStyle != null) { properties.fontStyle = toSkFontStyle(fontWeight, fontStyle); @@ -564,7 +538,6 @@ class CkParagraph extends ManagedSkiaObject @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,65 @@ class CkParagraphBuilder implements ui.ParagraphBuilder { return properties; } + /// Determines if the given [text] contains any code points which are not + /// supported by the current set of fonts. + void _ensureFontsSupportText(String text) { + // TODO(hterkelsen): Make this faster for the common case where the text + // is supported by the given fonts. + + // If the text is ASCII, then skip this check. + bool isAscii = true; + for (int i = 0; i < text.length; i++) { + if (text.codeUnitAt(i) >= 160) { + isAscii = false; + break; + } + } + if (isAscii) { + return; + } + CkTextStyle style = _peekStyle(); + List fontFamilies = + _getEffectiveFontFamilies(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(text.length, false); + for (SkTypeface typeface in typefaces) { + SkFont font = SkFont(typeface); + Uint8List glyphs = font.getGlyphIDs(text); + assert(glyphs.length == codeUnitsSupported.length); + for (int i = 0; i < glyphs.length; i++) { + codeUnitsSupported[i] |= + glyphs[i] != 0 || _isControlCode(text.codeUnitAt(i)); + } + } + + if (codeUnitsSupported.any((x) => !x)) { + List missingCodeUnits = []; + for (int i = 0; i < codeUnitsSupported.length; i++) { + if (!codeUnitsSupported[i]) { + missingCodeUnits.add(text.codeUnitAt(i)); + } + } + _findFontsForMissingCodeunits(missingCodeUnits); + } + } + + /// Returns [true] if [codepoint] is a Unicode control code. + bool _isControlCode(int codepoint) { + return codepoint < 32 || (codepoint > 127 && codepoint < 160); + } + @override void addText(String text) { + _ensureFontsSupportText(text); _commands.add(_ParagraphCommand.addText(text)); _paragraphBuilder.addText(text); } @@ -712,8 +742,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 { @@ -756,3 +788,18 @@ enum _ParagraphCommandType { pushStyle, addPlaceholder, } + +List _getEffectiveFontFamilies(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); + } + fontFamilies.addAll(skiaFontCollection.globalFontFallbacks); + return fontFamilies; +} diff --git a/lib/web_ui/test/canvaskit/canvaskit_api_test.dart b/lib/web_ui/test/canvaskit/canvaskit_api_test.dart index b70182ac85b1d..46743857615e7 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); }); } @@ -1055,7 +1055,11 @@ 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 99a2b279f1a161c68f389c474d2227f4e28cec87 Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Mon, 11 Jan 2021 09:30:04 -0800 Subject: [PATCH 02/14] WIP --- lib/web_ui/lib/src/engine.dart | 1 + .../src/engine/canvaskit/font_fallbacks.dart | 91 +++------------ .../src/engine/canvaskit/interval_tree.dart | 109 ++++++++++++++++++ 3 files changed, 124 insertions(+), 77 deletions(-) create mode 100644 lib/web_ui/lib/src/engine/canvaskit/interval_tree.dart diff --git a/lib/web_ui/lib/src/engine.dart b/lib/web_ui/lib/src/engine.dart index 1069a34fe6fba..760e3c8d5d0d9 100644 --- a/lib/web_ui/lib/src/engine.dart +++ b/lib/web_ui/lib/src/engine.dart @@ -36,6 +36,7 @@ part 'engine/canvaskit/font_fallbacks.dart'; part 'engine/canvaskit/image.dart'; part 'engine/canvaskit/image_filter.dart'; part 'engine/canvaskit/initialization.dart'; +part 'engine/canvaskit/interval_tree.dart'; part 'engine/canvaskit/layer.dart'; part 'engine/canvaskit/layer_scene_builder.dart'; part 'engine/canvaskit/layer_tree.dart'; 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 6a86f53971142..89c37a5bc3696 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart @@ -23,7 +23,7 @@ Future _findFontsForMissingCodeunits(List codeunits) async { Set coveredCodeUnits = {}; Set missingCodeUnits = {}; for (int codeunit in codeunits) { - List<_NotoFont> fontsForUnit = _lookupNotoFontsForCodeunit(codeunit); + List<_NotoFont> fontsForUnit = _notoTree!.intersections(codeunit); fonts.addAll(fontsForUnit); if (fontsForUnit.isNotEmpty) { coveredCodeUnits.add(codeunit); @@ -277,68 +277,20 @@ Set<_NotoFont> _findMinimumFontsForCodeunits( } void _ensureNotoFontTreeCreated() { - if (_notoTreeRoot != null) { + if (_notoTree != null) { return; } + Map<_NotoFont, List<_UnicodeRange>> ranges = + <_NotoFont, List<_UnicodeRange>>{}; + for (_NotoFont font in _notoFonts) { for (_UnicodeRange range in font.unicodeRanges) { - _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; - } - if (node.range.start > codeunit) { - if (node.left != null) { - return lookupHelper(node.left!); - } else { - return const <_NotoFont>[]; - } - } else { - if (node.right != null) { - return lookupHelper(node.right!); - } else { - return const <_NotoFont>[]; - } - } - } - - return lookupHelper(_notoTreeRoot!); -} - -List<_ResolvedNotoSubset> _lookupResolvedFontsForCodeunit(int codeunit) { - List<_ResolvedNotoSubset> lookupHelper( - _NotoTreeNode<_ResolvedNotoSubset> node) { - if (node.range.contains(codeunit)) { - return node.fonts; - } - if (node.range.start > codeunit) { - if (node.left != null) { - return lookupHelper(node.left!); - } else { - return const <_ResolvedNotoSubset>[]; - } - } else { - if (node.right != null) { - return lookupHelper(node.right!); - } else { - return const <_ResolvedNotoSubset>[]; - } + ranges.putIfAbsent(font, () => <_UnicodeRange>[]).add(range); } } - return lookupHelper(_resolvedNotoTreeRoot!); + _notoTree = _IntervalTree<_NotoFont>.createFromRanges(ranges); } class _NotoFont { @@ -381,6 +333,9 @@ class _UnicodeRange { @override int get hashCode => ui.hashValues(start, end); + + @override + String toString() => '[$start, $end]'; } class _ResolvedNotoFont { @@ -615,24 +570,6 @@ const List<_NotoFont> _notoFonts = <_NotoFont>[ ]), ]; -// 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; - _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 fonts; - - _NotoTreeNode(this.range) : this.fonts = []; -} - /// Associates [range] with [font] in the Noto Font tree. /// /// Returns the root node. @@ -666,7 +603,7 @@ _NotoTreeNode? _insertNotoFontRangeHelper( } if (range.start < root.range.start) { assert(range.end < root.range.start, - 'Overlapping Unicode range in Noto Tree'); + 'Overlapping Unicode range in Noto Tree. Root: ${root.range}, Range: $range'); if (root.left != null) { return _insertNotoFontRangeHelper(root.left, range, font); } else { @@ -678,7 +615,7 @@ _NotoTreeNode? _insertNotoFontRangeHelper( } } else { assert(root.range.end < range.start, - 'Overlapping Unicode range in Noto Tree'); + 'Overlapping Unicode range in Noto Tree. Root: ${root.range}, Range: $range'); if (root.right != null) { return _insertNotoFontRangeHelper(root.right, range, font); } else { @@ -857,8 +794,8 @@ class _VerifyNotoTreeResult { [this.reason]); } -/// The root of the unresolved Noto font Red-Black Tree. -_NotoTreeNode<_NotoFont>? _notoTreeRoot; +/// The Noto font interval tree. +_IntervalTree<_NotoFont>? _notoTree; /// The root of the resolved Noto font Red-Black Tree. _NotoTreeNode<_ResolvedNotoSubset>? _resolvedNotoTreeRoot; diff --git a/lib/web_ui/lib/src/engine/canvaskit/interval_tree.dart b/lib/web_ui/lib/src/engine/canvaskit/interval_tree.dart new file mode 100644 index 0000000000000..cb97adc560365 --- /dev/null +++ b/lib/web_ui/lib/src/engine/canvaskit/interval_tree.dart @@ -0,0 +1,109 @@ +// 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; + +/// Associates a [T] with one or more [_UnicodeRange]s. +class _IntervalTree { + final _IntervalTreeNode root; + + _IntervalTree._(this.root); + + factory _IntervalTree.createFromRanges( + Map> rangesMap) { + // Get a list of all the ranges ordered by start index. + List<_Interval> intervals = <_Interval>[]; + for (T key in rangesMap.keys) { + for (_UnicodeRange range in rangesMap[key]!) { + intervals.add(_Interval(key, range.start, range.end)); + } + } + intervals.sort((_Interval a, _Interval b) => a.start - b.start); + + // Make a balanced binary search tree from the sorted intervals. + _IntervalTreeNode? _makeBalancedTree(List<_Interval> intervals) { + if (intervals.length == 0) { + return null; + } + if (intervals.length == 1) { + return _IntervalTreeNode(intervals.single); + } + int mid = intervals.length ~/ 2; + _IntervalTreeNode root = _IntervalTreeNode(intervals[mid]); + root.left = _makeBalancedTree(intervals.sublist(0, mid)); + root.right = _makeBalancedTree(intervals.sublist(mid + 1)); + } + + void _computeHigh(_IntervalTreeNode root) { + if (root.left == null && root.right == null) { + root._cachedHigh = root.interval.end; + } else if (root.left == null) { + _computeHigh(root.right!); + root._cachedHigh = math.max(root.interval.end, root.right!.high); + } else if (root.right == null) { + _computeHigh(root.left!); + root._cachedHigh = math.max(root.interval.end, root.left!.high); + } else { + _computeHigh(root.right!); + _computeHigh(root.left!); + root._cachedHigh = math.max( + root.interval.end, math.max(root.left!.high, root.right!.high)); + } + } + + _IntervalTreeNode root = _makeBalancedTree(intervals)!; + _computeHigh(root); + + return _IntervalTree._(root); + } + + /// Returns the list of objects which have been associated with intervals that + /// intersect with [x]. + List intersections(int x) { + List results = []; + root.searchForPoint(x, results); + return results; + } +} + +class _Interval { + final T value; + final int start; + final int end; + + bool contains(int x) { + return start <= x && x <= end; + } + + const _Interval(this.value, this.start, this.end); +} + +class _IntervalTreeNode { + final _Interval interval; + + int get low => interval.start; + int get high => _cachedHigh ?? interval.end; + int? _cachedHigh; + + _IntervalTreeNode? left; + _IntervalTreeNode? right; + + _IntervalTreeNode(this.interval); + + // Searches the tree rooted at this node for all T containing [x]. + void searchForPoint(int x, List result) { + if (x > high) { + return; + } + left?.searchForPoint(x, result); + if (interval.contains(x)) { + result.add(interval.value); + } + if (x < low) { + return; + } + right?.searchForPoint(x, result); + } +} From 6a4d8a1b5bcae202c995ac500cd60ddc332b31bb Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Fri, 15 Jan 2021 15:00:28 -0800 Subject: [PATCH 03/14] Use an Interval Tree to store the unicode ranges for the Noto Fonts --- .../src/engine/canvaskit/font_fallbacks.dart | 369 +++++------------- .../lib/src/engine/canvaskit/fonts.dart | 57 ++- .../src/engine/canvaskit/interval_tree.dart | 2 + lib/web_ui/lib/src/engine/canvaskit/text.dart | 11 +- 4 files changed, 138 insertions(+), 301 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 89c37a5bc3696..267caba79497a 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart @@ -8,12 +8,13 @@ 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; +bool _registeredSymbolsAndEmoji = 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))) { @@ -31,41 +32,36 @@ Future _findFontsForMissingCodeunits(List codeunits) async { missingCodeUnits.add(codeunit); } } + fonts = _findMinimumFontsForCodeunits(coveredCodeUnits, fonts); + for (_NotoFont font in fonts) { - 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); - } + await font.ensureResolved(); } Set<_ResolvedNotoSubset> resolvedFonts = <_ResolvedNotoSubset>{}; for (int codeunit in coveredCodeUnits) { - resolvedFonts.addAll(_lookupResolvedFontsForCodeunit(codeunit)); + for (_NotoFont font in fonts) { + resolvedFonts.addAll(font.resolvedFont!.tree.intersections(codeunit)); + } } for (_ResolvedNotoSubset resolvedFont in resolvedFonts) { - skiaFontCollection.registerFallbackFont( - resolvedFont.url, resolvedFont.name); + _notoDownloadQueue.add(resolvedFont); } - if (missingCodeUnits.isNotEmpty) { - if (!_downloadedSymbolsAndEmoji) { - await _registerSymbolsAndEmoji(); + if (missingCodeUnits.isNotEmpty && !_notoDownloadQueue.isPending) { + if (!_registeredSymbolsAndEmoji) { + _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); + if (!_notoDownloadQueue.isPending) { + 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. @@ -149,29 +145,28 @@ _ResolvedNotoFont _makeResolvedNotoFontFromCss(String css, String name) { } } - return _ResolvedNotoFont(name, subsets); -} - -void _registerResolvedFont(_NotoFont font, _ResolvedNotoFont resolvedFont) { - _resolvedNotoFonts[font] = resolvedFont; - - for (_ResolvedNotoSubset subset in resolvedFont.subsets) { + Map<_ResolvedNotoSubset, List<_UnicodeRange>> rangesMap = + <_ResolvedNotoSubset, List<_UnicodeRange>>{}; + for (_ResolvedNotoSubset subset in subsets) { for (_UnicodeRange range in subset.ranges) { - _resolvedNotoTreeRoot = _insertNotoFontRange<_ResolvedNotoSubset>( - range, subset, _resolvedNotoTreeRoot); + rangesMap.putIfAbsent(subset, () => <_UnicodeRange>[]).add(range); } } - assert( - _verifyNotoTree(_resolvedNotoTreeRoot), - 'Resolved Noto tree is invalid: ' - '${_verifyNotoSubtree(_resolvedNotoTreeRoot).reason}'); + _IntervalTree<_ResolvedNotoSubset> tree = + _IntervalTree<_ResolvedNotoSubset>.createFromRanges(rangesMap); + + return _ResolvedNotoFont(name, subsets, tree); } /// In the case where none of the known Noto Fonts cover a set of code units, /// try the Symbols and Emoji fonts. We don't know the exact range of code units /// that are covered by these fonts, so we download them and hope for the best. Future _registerSymbolsAndEmoji() async { + if (_registeredSymbolsAndEmoji) { + return; + } + _registeredSymbolsAndEmoji = true; const String symbolsUrl = 'https://fonts.googleapis.com/css2?family=Noto+Sans+Symbols'; const String emojiUrl = @@ -200,10 +195,10 @@ Future _registerSymbolsAndEmoji() async { String symbolsFontUrl = extractUrlFromCss(symbolsCss); String emojiFontUrl = extractUrlFromCss(emojiCss); - skiaFontCollection.registerFallbackFont(symbolsFontUrl, 'Noto Sans Symbols'); - skiaFontCollection.registerFallbackFont( - emojiFontUrl, 'Noto Color Emoji Compat'); - _downloadedSymbolsAndEmoji = true; + _notoDownloadQueue.add(_ResolvedNotoSubset( + symbolsFontUrl, 'Noto Sans Symbols', const <_UnicodeRange>[])); + _notoDownloadQueue.add(_ResolvedNotoSubset( + emojiFontUrl, 'Noto Color Emoji Compat', const <_UnicodeRange>[])); } /// Finds the minimum set of fonts which covers all of the [codeunits]. @@ -297,7 +292,11 @@ class _NotoFont { final String name; final List<_UnicodeRange> unicodeRanges; - const _NotoFont(this.name, this.unicodeRanges); + Completer? _decodingCompleter; + + _ResolvedNotoFont? resolvedFont; + + _NotoFont(this.name, this.unicodeRanges); bool matchesCodeunit(int codeunit) { for (_UnicodeRange range in unicodeRanges) { @@ -310,6 +309,23 @@ class _NotoFont { String get googleFontsCssUrl => 'https://fonts.googleapis.com/css2?family=${name.replaceAll(' ', '+')}'; + + Future ensureResolved() async { + if (resolvedFont == null) { + if (_decodingCompleter == null) { + _decodingCompleter = Completer(); + String googleFontCss = await html.window.fetch(googleFontsCssUrl).then( + (dynamic response) => + response.text().then((dynamic x) => x as String)); + final _ResolvedNotoFont googleFont = + _makeResolvedNotoFontFromCss(googleFontCss, name); + resolvedFont = googleFont; + _decodingCompleter!.complete(); + } else { + await _decodingCompleter!.future; + } + } + } } class _UnicodeRange { @@ -341,19 +357,20 @@ class _UnicodeRange { class _ResolvedNotoFont { final String name; final List<_ResolvedNotoSubset> subsets; + final _IntervalTree<_ResolvedNotoSubset> tree; - const _ResolvedNotoFont(this.name, this.subsets); + const _ResolvedNotoFont(this.name, this.subsets, this.tree); } class _ResolvedNotoSubset { final String url; - final String name; + final String family; final List<_UnicodeRange> ranges; - const _ResolvedNotoSubset(this.url, this.name, this.ranges); + _ResolvedNotoSubset(this.url, this.family, this.ranges); } -const _NotoFont _notoSansSC = _NotoFont('Noto Sans SC', <_UnicodeRange>[ +_NotoFont _notoSansSC = _NotoFont('Noto Sans SC', <_UnicodeRange>[ _UnicodeRange(12288, 12591), _UnicodeRange(12800, 13311), _UnicodeRange(19968, 40959), @@ -361,32 +378,32 @@ const _NotoFont _notoSansSC = _NotoFont('Noto Sans SC', <_UnicodeRange>[ _UnicodeRange(65280, 65519), ]); -const _NotoFont _notoSansTC = _NotoFont('Noto Sans TC', <_UnicodeRange>[ +_NotoFont _notoSansTC = _NotoFont('Noto Sans TC', <_UnicodeRange>[ _UnicodeRange(12288, 12351), _UnicodeRange(12549, 12585), _UnicodeRange(19968, 40959), ]); -const _NotoFont _notoSansHK = _NotoFont('Noto Sans HK', <_UnicodeRange>[ +_NotoFont _notoSansHK = _NotoFont('Noto Sans HK', <_UnicodeRange>[ _UnicodeRange(12288, 12351), _UnicodeRange(12549, 12585), _UnicodeRange(19968, 40959), ]); -const _NotoFont _notoSansJP = _NotoFont('Noto Sans JP', <_UnicodeRange>[ +_NotoFont _notoSansJP = _NotoFont('Noto Sans JP', <_UnicodeRange>[ _UnicodeRange(12288, 12543), _UnicodeRange(19968, 40959), _UnicodeRange(65280, 65519), ]); -const List<_NotoFont> _cjkFonts = <_NotoFont>[ +List<_NotoFont> _cjkFonts = <_NotoFont>[ _notoSansSC, _notoSansTC, _notoSansHK, _notoSansJP, ]; -const List<_NotoFont> _notoFonts = <_NotoFont>[ +List<_NotoFont> _notoFonts = <_NotoFont>[ _notoSansSC, _notoSansTC, _notoSansHK, @@ -570,235 +587,63 @@ const List<_NotoFont> _notoFonts = <_NotoFont>[ ]), ]; -/// Associates [range] with [font] in the Noto Font tree. -/// -/// 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; - while (newRoot.parent != null) { - newRoot = newRoot.parent!; - } +class _FallbackFontDownloadQueue { + final Set<_ResolvedNotoSubset> downloadedSubsets = <_ResolvedNotoSubset>{}; + final Set<_ResolvedNotoSubset> pendingSubsets = <_ResolvedNotoSubset>{}; - return newRoot; - } - return root!; -} + bool get isPending => pendingSubsets.isNotEmpty; -/// 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, T 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) { - assert(range.end < root.range.start, - 'Overlapping Unicode range in Noto Tree. Root: ${root.range}, Range: $range'); - 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 { - assert(root.range.end < range.start, - 'Overlapping Unicode range in Noto Tree. Root: ${root.range}, Range: $range'); - 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; - } + void add(_ResolvedNotoSubset subset) { + if (downloadedSubsets.contains(subset) || pendingSubsets.contains(subset)) { + return; } - } else { - // If [root] is null, then the tree is empty. Create a new root. - _NotoTreeNode newRoot = _NotoTreeNode(range); - newRoot.fonts.add(font); - return newRoot; - } -} - -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; + bool firstInBatch = pendingSubsets.isEmpty; + pendingSubsets.add(subset); + if (firstInBatch) { + Timer.run(startDownloads); } } - 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. - 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; - grandparent.isBlack = false; - _repairNotoFontTree(grandparent); - return; - } - - 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; -} + Future startDownloads() async { + List> downloads = []; + for (_ResolvedNotoSubset subset in pendingSubsets) { + downloads.add(Future(() async { + ByteBuffer buffer; + try { + buffer = await html.window.fetch(subset.url).then( + (dynamic fetchResult) => fetchResult + .arrayBuffer() + .then((dynamic x) => x as ByteBuffer)); + } catch (e) { + html.window.console + .warn('Failed to load font ${subset.family} at ${subset.url}'); + html.window.console.warn(e); + return; + } -bool _verifyNotoTree(_NotoTreeNode? root) { - _VerifyNotoTreeResult result = _verifyNotoSubtree(root); - return result.isValid; -} + final Uint8List bytes = buffer.asUint8List(); + skiaFontCollection.registerFallbackFont(subset.family, bytes); -_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'); + pendingSubsets.remove(subset); + downloadedSubsets.add(subset); + if (pendingSubsets.isEmpty) { + await skiaFontCollection.ensureFontsLoaded(); + sendFontChangeMessage(); + } + })); } - } - 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'); + await Future.wait(downloads); + if (pendingSubsets.isNotEmpty) { + await startDownloads(); } - } 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 Noto font interval tree. _IntervalTree<_NotoFont>? _notoTree; -/// The root of the resolved Noto font Red-Black Tree. -_NotoTreeNode<_ResolvedNotoSubset>? _resolvedNotoTreeRoot; - Map<_NotoFont, _ResolvedNotoFont> _resolvedNotoFonts = <_NotoFont, _ResolvedNotoFont>{}; + +_FallbackFontDownloadQueue _notoDownloadQueue = _FallbackFontDownloadQueue(); diff --git a/lib/web_ui/lib/src/engine/canvaskit/fonts.dart b/lib/web_ui/lib/src/engine/canvaskit/fonts.dart index 6a6c84c8322eb..9f6074c7b1bc7 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/fonts.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/fonts.dart @@ -22,8 +22,6 @@ class SkiaFontCollection { /// Fonts which have been registered and loaded. final List<_RegisteredFont> _registeredFonts = <_RegisteredFont>[]; - final Set registeredFamilies = {}; - final Map> familyToTypefaceMap = >{}; @@ -34,13 +32,17 @@ class SkiaFontCollection { Future ensureFontsLoaded() async { await _loadFonts(); + if (fontProvider != null) { + fontProvider!.delete(); + fontProvider = null; + } fontProvider = canvasKit.TypefaceFontProvider.Make(); familyToTypefaceMap.clear(); for (var font in _registeredFonts) { - fontProvider.registerFont(font.bytes, font.flutterFamily); + fontProvider!.registerFont(font.bytes, font.family); familyToTypefaceMap - .putIfAbsent(font.flutterFamily, () => []) + .putIfAbsent(font.family, () => []) .add(font.typeface); } } @@ -62,24 +64,16 @@ class SkiaFontCollection { } Future loadFontFromList(Uint8List list, {String? fontFamily}) async { - String? actualFamily = _readActualFamilyName(list); - - if (actualFamily == null) { + if (fontFamily == null) { + fontFamily = _readActualFamilyName(list); if (fontFamily == null) { html.window.console .warn('Failed to read font family name. Aborting font load.'); return; } - actualFamily = fontFamily; - } - - if (fontFamily == null) { - fontFamily = actualFamily; } - registeredFamilies.add(fontFamily); - - _registeredFonts.add(_RegisteredFont(list, fontFamily, actualFamily)); + _registeredFonts.add(_RegisteredFont(list, fontFamily)); await ensureFontsLoaded(); } @@ -105,12 +99,16 @@ class SkiaFontCollection { 'There was a problem trying to load FontManifest.json'); } + bool registeredRoboto = false; + for (Map fontFamily in fontManifest.cast>()) { final String family = fontFamily['family']!; final List fontAssets = fontFamily['fonts']; - registeredFamilies.add(family); + if (family == 'Roboto') { + registeredRoboto = true; + } for (dynamic fontAssetItem in fontAssets) { final Map fontAsset = fontAssetItem; @@ -123,7 +121,7 @@ class SkiaFontCollection { /// We need a default fallback font for CanvasKit, in order to /// avoid crashing while laying out text with an unregistered font. We chose /// Roboto to match Android. - if (!registeredFamilies.contains('Roboto')) { + if (!registeredRoboto) { // Download Roboto and add it to the font buffers. _unloadedFonts.add(_registerFont(_robotoUrl, 'Roboto')); } @@ -140,23 +138,15 @@ class SkiaFontCollection { } final Uint8List bytes = buffer.asUint8List(); - String? actualFamily = _readActualFamilyName(bytes); - - if (actualFamily == null) { - html.window.console.warn('Failed to determine the actual name of the ' - 'font $family at $url. Defaulting to $family.'); - actualFamily = family; - } - - return _RegisteredFont(bytes, family, actualFamily); + return _RegisteredFont(bytes, family); } - void registerFallbackFont(String url, String family) { + void registerFallbackFont(String family, Uint8List bytes) { _fontFallbackCounts.putIfAbsent(family, () => 0); int fontFallbackTag = _fontFallbackCounts[family]!; _fontFallbackCounts[family] = _fontFallbackCounts[family]! + 1; String countedFamily = '$family $fontFallbackTag'; - _unloadedFonts.add(_registerFont(url, countedFamily)); + _registeredFonts.add(_RegisteredFont(bytes, countedFamily)); globalFontFallbacks.add(countedFamily); } @@ -175,26 +165,23 @@ class SkiaFontCollection { } SkFontMgr? skFontMgr; - late TypefaceFontProvider fontProvider; + TypefaceFontProvider? fontProvider; } /// Represents a font that has been registered. class _RegisteredFont { - /// The font family that the font was declared to have by Flutter. - final String flutterFamily; + /// The font family name for this font. + final String family; /// The byte data for this font. final Uint8List bytes; - /// The font family that was parsed from the font's bytes. - final String actualFamily; - /// The [SkTypeface] created from this font's [bytes]. /// /// This is used to determine which code points are supported by this font. final SkTypeface typeface; - _RegisteredFont(this.bytes, this.flutterFamily, this.actualFamily) + _RegisteredFont(this.bytes, this.family) : this.typeface = canvasKit.FontMgr.RefDefault().MakeTypefaceFromData(bytes); } diff --git a/lib/web_ui/lib/src/engine/canvaskit/interval_tree.dart b/lib/web_ui/lib/src/engine/canvaskit/interval_tree.dart index cb97adc560365..b5c60a80a732a 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/interval_tree.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/interval_tree.dart @@ -5,6 +5,7 @@ // @dart = 2.12 part of engine; +// TODO(hterkelsen): Tests. /// Associates a [T] with one or more [_UnicodeRange]s. class _IntervalTree { final _IntervalTreeNode root; @@ -34,6 +35,7 @@ class _IntervalTree { _IntervalTreeNode root = _IntervalTreeNode(intervals[mid]); root.left = _makeBalancedTree(intervals.sublist(0, mid)); root.right = _makeBalancedTree(intervals.sublist(mid + 1)); + return root; } void _computeHigh(_IntervalTreeNode root) { diff --git a/lib/web_ui/lib/src/engine/canvaskit/text.dart b/lib/web_ui/lib/src/engine/canvaskit/text.dart index 79339e4e527dd..82abedbb047c7 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/text.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/text.dart @@ -680,6 +680,8 @@ class CkParagraphBuilder implements ui.ParagraphBuilder { missingCodeUnits.add(text.codeUnitAt(i)); } } + print("FOUND MISSING CODE UNITS IN STRING: $text"); + print("FONT FAMILIES: $fontFamilies"); _findFontsForMissingCodeunits(missingCodeUnits); } } @@ -791,11 +793,12 @@ enum _ParagraphCommandType { List _getEffectiveFontFamilies(String? fontFamily, [List? fontFamilyFallback]) { - if (fontFamily == null || - !skiaFontCollection.registeredFamilies.contains(fontFamily)) { - fontFamily = 'Roboto'; + List fontFamilies = []; + if (fontFamily == null) { + fontFamilies.add('Roboto'); + } else { + fontFamilies.add(fontFamily); } - List fontFamilies = [fontFamily]; if (fontFamilyFallback != null && !fontFamilyFallback.every((font) => fontFamily == font)) { fontFamilies.addAll(fontFamilyFallback); From 28e5935ffd9f83e87f1d9139cce13e682709836d Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Fri, 15 Jan 2021 15:27:02 -0800 Subject: [PATCH 04/14] 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 158c5409b26c2..2ceab31f112f0 100755 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -442,6 +442,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/fonts.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/image.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/image_filter.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/initialization.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/interval_tree.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/layer.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/layer_scene_builder.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/layer_tree.dart From 0da7ca34b17db0e80f51969b9929b45972d9971b Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Fri, 15 Jan 2021 16:02:08 -0800 Subject: [PATCH 05/14] Remove debug print statements --- lib/web_ui/lib/src/engine/canvaskit/text.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/web_ui/lib/src/engine/canvaskit/text.dart b/lib/web_ui/lib/src/engine/canvaskit/text.dart index 82abedbb047c7..2efdf70c5fe6f 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/text.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/text.dart @@ -680,8 +680,6 @@ class CkParagraphBuilder implements ui.ParagraphBuilder { missingCodeUnits.add(text.codeUnitAt(i)); } } - print("FOUND MISSING CODE UNITS IN STRING: $text"); - print("FONT FAMILIES: $fontFamilies"); _findFontsForMissingCodeunits(missingCodeUnits); } } From 2b8dad55499451bf2c22a35c999cdd11b37a06ba Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Tue, 19 Jan 2021 09:31:33 -0800 Subject: [PATCH 06/14] Respond to comments --- .../lib/src/engine/canvaskit/interval_tree.dart | 13 ++++++------- lib/web_ui/lib/src/engine/canvaskit/text.dart | 1 - 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/lib/web_ui/lib/src/engine/canvaskit/interval_tree.dart b/lib/web_ui/lib/src/engine/canvaskit/interval_tree.dart index b5c60a80a732a..0c7f472098866 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/interval_tree.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/interval_tree.dart @@ -40,17 +40,17 @@ class _IntervalTree { void _computeHigh(_IntervalTreeNode root) { if (root.left == null && root.right == null) { - root._cachedHigh = root.interval.end; + root.high = root.interval.end; } else if (root.left == null) { _computeHigh(root.right!); - root._cachedHigh = math.max(root.interval.end, root.right!.high); + root.high = math.max(root.interval.end, root.right!.high); } else if (root.right == null) { _computeHigh(root.left!); - root._cachedHigh = math.max(root.interval.end, root.left!.high); + root.high = math.max(root.interval.end, root.left!.high); } else { _computeHigh(root.right!); _computeHigh(root.left!); - root._cachedHigh = math.max( + root.high = math.max( root.interval.end, math.max(root.left!.high, root.right!.high)); } } @@ -86,13 +86,12 @@ class _IntervalTreeNode { final _Interval interval; int get low => interval.start; - int get high => _cachedHigh ?? interval.end; - int? _cachedHigh; + int high; _IntervalTreeNode? left; _IntervalTreeNode? right; - _IntervalTreeNode(this.interval); + _IntervalTreeNode(this.interval) : high = interval.end; // Searches the tree rooted at this node for all T containing [x]. void searchForPoint(int x, List result) { diff --git a/lib/web_ui/lib/src/engine/canvaskit/text.dart b/lib/web_ui/lib/src/engine/canvaskit/text.dart index 2efdf70c5fe6f..9789ad0a590d5 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/text.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/text.dart @@ -661,7 +661,6 @@ class CkParagraphBuilder implements ui.ParagraphBuilder { typefaces.addAll(typefacesForFamily); } } - // List codeUnits = text.codeUnits; List codeUnitsSupported = List.filled(text.length, false); for (SkTypeface typeface in typefaces) { SkFont font = SkFont(typeface); From 935e8652cb635c85033f3a3455e30fdefca95d9a Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Tue, 19 Jan 2021 09:57:34 -0800 Subject: [PATCH 07/14] Fix analysis error --- lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart | 3 --- 1 file changed, 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 267caba79497a..3e3135b3325b1 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart @@ -643,7 +643,4 @@ class _FallbackFontDownloadQueue { /// The Noto font interval tree. _IntervalTree<_NotoFont>? _notoTree; -Map<_NotoFont, _ResolvedNotoFont> _resolvedNotoFonts = - <_NotoFont, _ResolvedNotoFont>{}; - _FallbackFontDownloadQueue _notoDownloadQueue = _FallbackFontDownloadQueue(); From af1d96b139bd3a3761245749141abff948782ba4 Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Wed, 20 Jan 2021 15:07:55 -0800 Subject: [PATCH 08/14] Add tests --- lib/web_ui/dev/test_runner.dart | 15 +- .../src/engine/canvaskit/font_fallbacks.dart | 454 +++++++++--------- .../src/engine/canvaskit/interval_tree.dart | 93 ++-- lib/web_ui/lib/src/engine/util.dart | 84 ++-- .../test/canvaskit/fallback_fonts_test.dart | 127 +++++ .../test/canvaskit/interval_tree_test.dart | 85 ++++ 6 files changed, 544 insertions(+), 314 deletions(-) create mode 100644 lib/web_ui/test/canvaskit/fallback_fonts_test.dart create mode 100644 lib/web_ui/test/canvaskit/interval_tree_test.dart diff --git a/lib/web_ui/dev/test_runner.dart b/lib/web_ui/dev/test_runner.dart index 8c261151bddf4..31d413df7e00c 100644 --- a/lib/web_ui/dev/test_runner.dart +++ b/lib/web_ui/dev/test_runner.dart @@ -170,7 +170,7 @@ class TestCommand extends Command with ArgUtils { Future runIntegrationTests() async { // Parse additional arguments specific for integration testing. IntegrationTestsArgumentParser.instance.parseOptions(argResults); - if(!_testPreparationReady) { + if (!_testPreparationReady) { await _prepare(); } return IntegrationTestsManager( @@ -458,9 +458,12 @@ class TestCommand extends Command with ArgUtils { } // All files under test/golden_tests are considered golden tests. - final bool isUnderGoldenTestsDirectory = path.split(testFilePath.relativeToWebUi).contains('golden_tests'); + final bool isUnderGoldenTestsDirectory = + path.split(testFilePath.relativeToWebUi).contains('golden_tests'); // Any file whose name ends with "_golden_test.dart" is run as a golden test. - final bool isGoldenTestFile = path.basename(testFilePath.relativeToWebUi).endsWith('_golden_test.dart'); + final bool isGoldenTestFile = path + .basename(testFilePath.relativeToWebUi) + .endsWith('_golden_test.dart'); if (isUnderGoldenTestsDirectory || isGoldenTestFile) { screenshotTestFiles.add(testFilePath); } else { @@ -698,7 +701,11 @@ class TestCommand extends Command with ArgUtils { } } -const List _kTestFonts = ['ahem.ttf', 'Roboto-Regular.ttf']; +const List _kTestFonts = [ + 'ahem.ttf', + 'Roboto-Regular.ttf', + 'NotoNaskhArabic-Regular.ttf', +]; void _copyTestFontsIntoWebUi() { final String fontsPath = path.join( 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 3e3135b3325b1..b7f94f850f65f 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart @@ -47,14 +47,14 @@ Future _findFontsForMissingCodeunits(List codeunits) async { } for (_ResolvedNotoSubset resolvedFont in resolvedFonts) { - _notoDownloadQueue.add(resolvedFont); + notoDownloadQueue.add(resolvedFont); } - if (missingCodeUnits.isNotEmpty && !_notoDownloadQueue.isPending) { + if (missingCodeUnits.isNotEmpty && !notoDownloadQueue.isPending) { if (!_registeredSymbolsAndEmoji) { _registerSymbolsAndEmoji(); } else { - if (!_notoDownloadQueue.isPending) { + if (!notoDownloadQueue.isPending) { html.window.console.log( 'Could not find a Noto font to display all missing characters. ' 'Please add a font asset for the missing characters.'); @@ -96,7 +96,7 @@ _ResolvedNotoFont _makeResolvedNotoFontFromCss(String css, String name) { List<_ResolvedNotoSubset> subsets = <_ResolvedNotoSubset>[]; bool resolvingFontFace = false; String? fontFaceUrl; - List<_UnicodeRange>? fontFaceUnicodeRanges; + List? fontFaceUnicodeRanges; for (final String line in LineSplitter.split(css)) { // Search for the beginning of a @font-face. if (!resolvingFontFace) { @@ -115,7 +115,7 @@ _ResolvedNotoFont _makeResolvedNotoFontFromCss(String css, String name) { int urlEnd = line.indexOf(')'); fontFaceUrl = line.substring(urlStart + 4, urlEnd); } else if (line.startsWith(' unicode-range:')) { - fontFaceUnicodeRanges = <_UnicodeRange>[]; + fontFaceUnicodeRanges = []; String rangeString = line.substring(17, line.length - 1); List rawRanges = rangeString.split(', '); for (final String rawRange in rawRanges) { @@ -124,7 +124,7 @@ _ResolvedNotoFont _makeResolvedNotoFontFromCss(String css, String name) { String singleRange = startEnd.single; assert(singleRange.startsWith('U+')); int rangeValue = int.parse(singleRange.substring(2), radix: 16); - fontFaceUnicodeRanges.add(_UnicodeRange(rangeValue, rangeValue)); + fontFaceUnicodeRanges.add(CodeunitRange(rangeValue, rangeValue)); } else { assert(startEnd.length == 2); String startRange = startEnd[0]; @@ -132,7 +132,7 @@ _ResolvedNotoFont _makeResolvedNotoFontFromCss(String css, String name) { 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)); + fontFaceUnicodeRanges.add(CodeunitRange(startValue, endValue)); } } } else if (line == '}') { @@ -145,16 +145,16 @@ _ResolvedNotoFont _makeResolvedNotoFontFromCss(String css, String name) { } } - Map<_ResolvedNotoSubset, List<_UnicodeRange>> rangesMap = - <_ResolvedNotoSubset, List<_UnicodeRange>>{}; + Map<_ResolvedNotoSubset, List> rangesMap = + <_ResolvedNotoSubset, List>{}; for (_ResolvedNotoSubset subset in subsets) { - for (_UnicodeRange range in subset.ranges) { - rangesMap.putIfAbsent(subset, () => <_UnicodeRange>[]).add(range); + for (CodeunitRange range in subset.ranges) { + rangesMap.putIfAbsent(subset, () => []).add(range); } } - _IntervalTree<_ResolvedNotoSubset> tree = - _IntervalTree<_ResolvedNotoSubset>.createFromRanges(rangesMap); + IntervalTree<_ResolvedNotoSubset> tree = + IntervalTree<_ResolvedNotoSubset>.createFromRanges(rangesMap); return _ResolvedNotoFont(name, subsets, tree); } @@ -172,11 +172,10 @@ Future _registerSymbolsAndEmoji() async { 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 symbolsCss = + await notoDownloadQueue.downloader.downloadAsString(symbolsUrl); + String emojiCss = + await notoDownloadQueue.downloader.downloadAsString(emojiUrl); String extractUrlFromCss(String css) { for (final String line in LineSplitter.split(css)) { @@ -195,10 +194,10 @@ Future _registerSymbolsAndEmoji() async { String symbolsFontUrl = extractUrlFromCss(symbolsCss); String emojiFontUrl = extractUrlFromCss(emojiCss); - _notoDownloadQueue.add(_ResolvedNotoSubset( - symbolsFontUrl, 'Noto Sans Symbols', const <_UnicodeRange>[])); - _notoDownloadQueue.add(_ResolvedNotoSubset( - emojiFontUrl, 'Noto Color Emoji Compat', const <_UnicodeRange>[])); + notoDownloadQueue.add(_ResolvedNotoSubset( + symbolsFontUrl, 'Noto Sans Symbols', const [])); + notoDownloadQueue.add(_ResolvedNotoSubset( + emojiFontUrl, 'Noto Color Emoji Compat', const [])); } /// Finds the minimum set of fonts which covers all of the [codeunits]. @@ -276,21 +275,21 @@ void _ensureNotoFontTreeCreated() { return; } - Map<_NotoFont, List<_UnicodeRange>> ranges = - <_NotoFont, List<_UnicodeRange>>{}; + Map<_NotoFont, List> ranges = + <_NotoFont, List>{}; for (_NotoFont font in _notoFonts) { - for (_UnicodeRange range in font.unicodeRanges) { - ranges.putIfAbsent(font, () => <_UnicodeRange>[]).add(range); + for (CodeunitRange range in font.unicodeRanges) { + ranges.putIfAbsent(font, () => []).add(range); } } - _notoTree = _IntervalTree<_NotoFont>.createFromRanges(ranges); + _notoTree = IntervalTree<_NotoFont>.createFromRanges(ranges); } class _NotoFont { final String name; - final List<_UnicodeRange> unicodeRanges; + final List unicodeRanges; Completer? _decodingCompleter; @@ -299,7 +298,7 @@ class _NotoFont { _NotoFont(this.name, this.unicodeRanges); bool matchesCodeunit(int codeunit) { - for (_UnicodeRange range in unicodeRanges) { + for (CodeunitRange range in unicodeRanges) { if (range.contains(codeunit)) { return true; } @@ -314,9 +313,8 @@ class _NotoFont { if (resolvedFont == null) { if (_decodingCompleter == null) { _decodingCompleter = Completer(); - String googleFontCss = await html.window.fetch(googleFontsCssUrl).then( - (dynamic response) => - response.text().then((dynamic x) => x as String)); + String googleFontCss = await notoDownloadQueue.downloader + .downloadAsString(googleFontsCssUrl); final _ResolvedNotoFont googleFont = _makeResolvedNotoFontFromCss(googleFontCss, name); resolvedFont = googleFont; @@ -328,11 +326,11 @@ class _NotoFont { } } -class _UnicodeRange { +class CodeunitRange { final int start; final int end; - const _UnicodeRange(this.start, this.end); + const CodeunitRange(this.start, this.end); bool contains(int codeUnit) { return start <= codeUnit && codeUnit <= end; @@ -340,10 +338,10 @@ class _UnicodeRange { @override bool operator ==(dynamic other) { - if (other is! _UnicodeRange) { + if (other is! CodeunitRange) { return false; } - _UnicodeRange range = other; + CodeunitRange range = other; return range.start == start && range.end == end; } @@ -357,7 +355,7 @@ class _UnicodeRange { class _ResolvedNotoFont { final String name; final List<_ResolvedNotoSubset> subsets; - final _IntervalTree<_ResolvedNotoSubset> tree; + final IntervalTree<_ResolvedNotoSubset> tree; const _ResolvedNotoFont(this.name, this.subsets, this.tree); } @@ -365,35 +363,35 @@ class _ResolvedNotoFont { class _ResolvedNotoSubset { final String url; final String family; - final List<_UnicodeRange> ranges; + final List ranges; _ResolvedNotoSubset(this.url, this.family, this.ranges); } -_NotoFont _notoSansSC = _NotoFont('Noto Sans SC', <_UnicodeRange>[ - _UnicodeRange(12288, 12591), - _UnicodeRange(12800, 13311), - _UnicodeRange(19968, 40959), - _UnicodeRange(65072, 65135), - _UnicodeRange(65280, 65519), +_NotoFont _notoSansSC = _NotoFont('Noto Sans SC', [ + CodeunitRange(12288, 12591), + CodeunitRange(12800, 13311), + CodeunitRange(19968, 40959), + CodeunitRange(65072, 65135), + CodeunitRange(65280, 65519), ]); -_NotoFont _notoSansTC = _NotoFont('Noto Sans TC', <_UnicodeRange>[ - _UnicodeRange(12288, 12351), - _UnicodeRange(12549, 12585), - _UnicodeRange(19968, 40959), +_NotoFont _notoSansTC = _NotoFont('Noto Sans TC', [ + CodeunitRange(12288, 12351), + CodeunitRange(12549, 12585), + CodeunitRange(19968, 40959), ]); -_NotoFont _notoSansHK = _NotoFont('Noto Sans HK', <_UnicodeRange>[ - _UnicodeRange(12288, 12351), - _UnicodeRange(12549, 12585), - _UnicodeRange(19968, 40959), +_NotoFont _notoSansHK = _NotoFont('Noto Sans HK', [ + CodeunitRange(12288, 12351), + CodeunitRange(12549, 12585), + CodeunitRange(19968, 40959), ]); -_NotoFont _notoSansJP = _NotoFont('Noto Sans JP', <_UnicodeRange>[ - _UnicodeRange(12288, 12543), - _UnicodeRange(19968, 40959), - _UnicodeRange(65280, 65519), +_NotoFont _notoSansJP = _NotoFont('Noto Sans JP', [ + CodeunitRange(12288, 12543), + CodeunitRange(19968, 40959), + CodeunitRange(65280, 65519), ]); List<_NotoFont> _cjkFonts = <_NotoFont>[ @@ -408,186 +406,188 @@ List<_NotoFont> _notoFonts = <_NotoFont>[ _notoSansTC, _notoSansHK, _notoSansJP, - _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 Naskh Arabic UI', [ + CodeunitRange(1536, 1791), + CodeunitRange(8204, 8206), + CodeunitRange(8208, 8209), + CodeunitRange(8271, 8271), + CodeunitRange(11841, 11841), + CodeunitRange(64336, 65023), + CodeunitRange(65132, 65276), ]), - _NotoFont('Noto Sans Armenian', <_UnicodeRange>[ - _UnicodeRange(1328, 1424), - _UnicodeRange(64275, 64279), + _NotoFont('Noto Sans Armenian', [ + CodeunitRange(1328, 1424), + CodeunitRange(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 Bengali UI', [ + CodeunitRange(2404, 2405), + CodeunitRange(2433, 2555), + CodeunitRange(8204, 8205), + CodeunitRange(8377, 8377), + CodeunitRange(9676, 9676), ]), - _NotoFont('Noto Sans Myanmar UI', <_UnicodeRange>[ - _UnicodeRange(4096, 4255), - _UnicodeRange(8204, 8205), - _UnicodeRange(9676, 9676), + _NotoFont('Noto Sans Myanmar UI', [ + CodeunitRange(4096, 4255), + CodeunitRange(8204, 8205), + CodeunitRange(9676, 9676), ]), - _NotoFont('Noto Sans Egyptian Hieroglyphs', <_UnicodeRange>[ - _UnicodeRange(77824, 78894), + _NotoFont('Noto Sans Egyptian Hieroglyphs', [ + CodeunitRange(77824, 78894), ]), - _NotoFont('Noto Sans Ethiopic', <_UnicodeRange>[ - _UnicodeRange(4608, 5017), - _UnicodeRange(11648, 11742), - _UnicodeRange(43777, 43822), + _NotoFont('Noto Sans Ethiopic', [ + CodeunitRange(4608, 5017), + CodeunitRange(11648, 11742), + CodeunitRange(43777, 43822), ]), - _NotoFont('Noto Sans Georgian', <_UnicodeRange>[ - _UnicodeRange(1417, 1417), - _UnicodeRange(4256, 4351), - _UnicodeRange(11520, 11567), + _NotoFont('Noto Sans Georgian', [ + CodeunitRange(1417, 1417), + CodeunitRange(4256, 4351), + CodeunitRange(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 Gujarati UI', [ + CodeunitRange(2404, 2405), + CodeunitRange(2688, 2815), + CodeunitRange(8204, 8205), + CodeunitRange(8377, 8377), + CodeunitRange(9676, 9676), + CodeunitRange(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 Gurmukhi UI', [ + CodeunitRange(2404, 2405), + CodeunitRange(2561, 2677), + CodeunitRange(8204, 8205), + CodeunitRange(8377, 8377), + CodeunitRange(9676, 9676), + CodeunitRange(9772, 9772), + CodeunitRange(43056, 43065), ]), - _NotoFont('Noto Sans Hebrew', <_UnicodeRange>[ - _UnicodeRange(1424, 1535), - _UnicodeRange(8362, 8362), - _UnicodeRange(9676, 9676), - _UnicodeRange(64285, 64335), + _NotoFont('Noto Sans Hebrew', [ + CodeunitRange(1424, 1535), + CodeunitRange(8362, 8362), + CodeunitRange(9676, 9676), + CodeunitRange(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 Devanagari UI', [ + CodeunitRange(2304, 2431), + CodeunitRange(7376, 7414), + CodeunitRange(7416, 7417), + CodeunitRange(8204, 9205), + CodeunitRange(8360, 8360), + CodeunitRange(8377, 8377), + CodeunitRange(9676, 9676), + CodeunitRange(43056, 43065), + CodeunitRange(43232, 43259), ]), - _NotoFont('Noto Sans Kannada UI', <_UnicodeRange>[ - _UnicodeRange(2404, 2405), - _UnicodeRange(3202, 3314), - _UnicodeRange(8204, 8205), - _UnicodeRange(8377, 8377), - _UnicodeRange(9676, 9676), + _NotoFont('Noto Sans Kannada UI', [ + CodeunitRange(2404, 2405), + CodeunitRange(3202, 3314), + CodeunitRange(8204, 8205), + CodeunitRange(8377, 8377), + CodeunitRange(9676, 9676), ]), - _NotoFont('Noto Sans Khmer UI', <_UnicodeRange>[ - _UnicodeRange(6016, 6143), - _UnicodeRange(8204, 8204), - _UnicodeRange(9676, 9676), + _NotoFont('Noto Sans Khmer UI', [ + CodeunitRange(6016, 6143), + CodeunitRange(8204, 8204), + CodeunitRange(9676, 9676), ]), - _NotoFont('Noto Sans KR', <_UnicodeRange>[ - _UnicodeRange(12593, 12686), - _UnicodeRange(12800, 12828), - _UnicodeRange(12896, 12923), - _UnicodeRange(44032, 55215), + _NotoFont('Noto Sans KR', [ + CodeunitRange(12593, 12686), + CodeunitRange(12800, 12828), + CodeunitRange(12896, 12923), + CodeunitRange(44032, 55215), ]), - _NotoFont('Noto Sans Lao UI', <_UnicodeRange>[ - _UnicodeRange(3713, 3807), - _UnicodeRange(9676, 9676), + _NotoFont('Noto Sans Lao UI', [ + CodeunitRange(3713, 3807), + CodeunitRange(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 Malayalam UI', [ + CodeunitRange(775, 775), + CodeunitRange(803, 803), + CodeunitRange(2404, 2405), + CodeunitRange(3330, 3455), + CodeunitRange(8204, 8205), + CodeunitRange(8377, 8377), + CodeunitRange(9676, 9676), ]), - _NotoFont('Noto Sans Sinhala', <_UnicodeRange>[ - _UnicodeRange(2404, 2405), - _UnicodeRange(3458, 3572), - _UnicodeRange(8204, 8205), - _UnicodeRange(9676, 9676), + _NotoFont('Noto Sans Sinhala', [ + CodeunitRange(2404, 2405), + CodeunitRange(3458, 3572), + CodeunitRange(8204, 8205), + CodeunitRange(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 Tamil UI', [ + CodeunitRange(2404, 2405), + CodeunitRange(2946, 3066), + CodeunitRange(8204, 8205), + CodeunitRange(8377, 8377), + CodeunitRange(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 Telugu UI', [ + CodeunitRange(2385, 2386), + CodeunitRange(2404, 2405), + CodeunitRange(3072, 3199), + CodeunitRange(7386, 7386), + CodeunitRange(8204, 8205), + CodeunitRange(9676, 9676), ]), - _NotoFont('Noto Sans Thai UI', <_UnicodeRange>[ - _UnicodeRange(3585, 3675), - _UnicodeRange(8204, 8205), - _UnicodeRange(9676, 9676), + _NotoFont('Noto Sans Thai UI', [ + CodeunitRange(3585, 3675), + CodeunitRange(8204, 8205), + CodeunitRange(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), + _NotoFont('Noto Sans', [ + CodeunitRange(0, 255), + CodeunitRange(305, 305), + CodeunitRange(338, 339), + CodeunitRange(699, 700), + CodeunitRange(710, 710), + CodeunitRange(730, 730), + CodeunitRange(732, 732), + CodeunitRange(8192, 8303), + CodeunitRange(8308, 8308), + CodeunitRange(8364, 8364), + CodeunitRange(8482, 8482), + CodeunitRange(8593, 8593), + CodeunitRange(8595, 8595), + CodeunitRange(8722, 8722), + CodeunitRange(8725, 8725), + CodeunitRange(65279, 65279), + CodeunitRange(65533, 65533), + CodeunitRange(1024, 1119), + CodeunitRange(1168, 1169), + CodeunitRange(1200, 1201), + CodeunitRange(8470, 8470), + CodeunitRange(1120, 1327), + CodeunitRange(7296, 7304), + CodeunitRange(8372, 8372), + CodeunitRange(11744, 11775), + CodeunitRange(42560, 42655), + CodeunitRange(65070, 65071), + CodeunitRange(880, 1023), + CodeunitRange(7936, 8191), + CodeunitRange(256, 591), + CodeunitRange(601, 601), + CodeunitRange(7680, 7935), + CodeunitRange(8224, 8224), + CodeunitRange(8352, 8363), + CodeunitRange(8365, 8399), + CodeunitRange(8467, 8467), + CodeunitRange(11360, 11391), + CodeunitRange(42784, 43007), + CodeunitRange(258, 259), + CodeunitRange(272, 273), + CodeunitRange(296, 297), + CodeunitRange(360, 361), + CodeunitRange(416, 417), + CodeunitRange(431, 432), + CodeunitRange(7840, 7929), + CodeunitRange(8363, 8363), ]), ]; -class _FallbackFontDownloadQueue { +class FallbackFontDownloadQueue { + NotoDownloader downloader = NotoDownloader(); + final Set<_ResolvedNotoSubset> downloadedSubsets = <_ResolvedNotoSubset>{}; final Set<_ResolvedNotoSubset> pendingSubsets = <_ResolvedNotoSubset>{}; @@ -610,10 +610,7 @@ class _FallbackFontDownloadQueue { downloads.add(Future(() async { ByteBuffer buffer; try { - buffer = await html.window.fetch(subset.url).then( - (dynamic fetchResult) => fetchResult - .arrayBuffer() - .then((dynamic x) => x as ByteBuffer)); + buffer = await downloader.downloadAsBytes(subset.url); } catch (e) { html.window.console .warn('Failed to load font ${subset.family} at ${subset.url}'); @@ -640,7 +637,26 @@ class _FallbackFontDownloadQueue { } } +class NotoDownloader { + /// Downloads the [url] and returns it as a [ByteBuffer]. + /// + /// Override this for testing. + Future downloadAsBytes(String url) { + return html.window.fetch(url).then((dynamic fetchResult) => fetchResult + .arrayBuffer() + .then((dynamic x) => x as ByteBuffer)); + } + + /// Downloads the [url] and returns is as a [String]. + /// + /// Override this for testing. + Future downloadAsString(String url) { + return html.window.fetch(url).then((dynamic response) => + response.text().then((dynamic x) => x as String)); + } +} + /// The Noto font interval tree. -_IntervalTree<_NotoFont>? _notoTree; +IntervalTree<_NotoFont>? _notoTree; -_FallbackFontDownloadQueue _notoDownloadQueue = _FallbackFontDownloadQueue(); +FallbackFontDownloadQueue notoDownloadQueue = FallbackFontDownloadQueue(); diff --git a/lib/web_ui/lib/src/engine/canvaskit/interval_tree.dart b/lib/web_ui/lib/src/engine/canvaskit/interval_tree.dart index 0c7f472098866..d614ee86bf46c 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/interval_tree.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/interval_tree.dart @@ -5,60 +5,63 @@ // @dart = 2.12 part of engine; -// TODO(hterkelsen): Tests. -/// Associates a [T] with one or more [_UnicodeRange]s. -class _IntervalTree { - final _IntervalTreeNode root; +/// Associates a [T] with one or more [CodeunitRange]s. +class IntervalTree { + final IntervalTreeNode root; - _IntervalTree._(this.root); + IntervalTree._(this.root); - factory _IntervalTree.createFromRanges( - Map> rangesMap) { + factory IntervalTree.createFromRanges(Map> rangesMap) { // Get a list of all the ranges ordered by start index. - List<_Interval> intervals = <_Interval>[]; + List> intervals = >[]; for (T key in rangesMap.keys) { - for (_UnicodeRange range in rangesMap[key]!) { - intervals.add(_Interval(key, range.start, range.end)); + for (CodeunitRange range in rangesMap[key]!) { + intervals.add(IntervalTreeNode(key, range.start, range.end)); } } - intervals.sort((_Interval a, _Interval b) => a.start - b.start); + intervals + .sort((IntervalTreeNode a, IntervalTreeNode b) => a.low - b.low); - // Make a balanced binary search tree from the sorted intervals. - _IntervalTreeNode? _makeBalancedTree(List<_Interval> intervals) { - if (intervals.length == 0) { + // Make a balanced binary search tree from the nodes sorted by low value. + IntervalTreeNode? _makeBalancedTree(List> nodes) { + if (nodes.length == 0) { return null; } - if (intervals.length == 1) { - return _IntervalTreeNode(intervals.single); + if (nodes.length == 1) { + return nodes.single; } - int mid = intervals.length ~/ 2; - _IntervalTreeNode root = _IntervalTreeNode(intervals[mid]); - root.left = _makeBalancedTree(intervals.sublist(0, mid)); - root.right = _makeBalancedTree(intervals.sublist(mid + 1)); + int mid = nodes.length ~/ 2; + IntervalTreeNode root = nodes[mid]; + root.left = _makeBalancedTree(nodes.sublist(0, mid)); + root.right = _makeBalancedTree(nodes.sublist(mid + 1)); return root; } - void _computeHigh(_IntervalTreeNode root) { + void _computeHigh(IntervalTreeNode root) { if (root.left == null && root.right == null) { - root.high = root.interval.end; + root.computedHigh = root.high; } else if (root.left == null) { _computeHigh(root.right!); - root.high = math.max(root.interval.end, root.right!.high); + root.computedHigh = math.max(root.high, root.right!.computedHigh); } else if (root.right == null) { _computeHigh(root.left!); - root.high = math.max(root.interval.end, root.left!.high); + root.computedHigh = math.max(root.high, root.left!.computedHigh); } else { _computeHigh(root.right!); _computeHigh(root.left!); - root.high = math.max( - root.interval.end, math.max(root.left!.high, root.right!.high)); + root.computedHigh = math.max( + root.high, + math.max( + root.left!.computedHigh, + root.right!.computedHigh, + )); } } - _IntervalTreeNode root = _makeBalancedTree(intervals)!; + IntervalTreeNode root = _makeBalancedTree(intervals)!; _computeHigh(root); - return _IntervalTree._(root); + return IntervalTree._(root); } /// Returns the list of objects which have been associated with intervals that @@ -70,37 +73,29 @@ class _IntervalTree { } } -class _Interval { +class IntervalTreeNode { final T value; - final int start; - final int end; + final int low; + final int high; + int computedHigh; - bool contains(int x) { - return start <= x && x <= end; - } - - const _Interval(this.value, this.start, this.end); -} + IntervalTreeNode? left; + IntervalTreeNode? right; -class _IntervalTreeNode { - final _Interval interval; + IntervalTreeNode(this.value, this.low, this.high) : computedHigh = high; - int get low => interval.start; - int high; - - _IntervalTreeNode? left; - _IntervalTreeNode? right; - - _IntervalTreeNode(this.interval) : high = interval.end; + bool contains(int x) { + return low <= x && x <= high; + } // Searches the tree rooted at this node for all T containing [x]. void searchForPoint(int x, List result) { - if (x > high) { + if (x > computedHigh) { return; } left?.searchForPoint(x, result); - if (interval.contains(x)) { - result.add(interval.value); + if (this.contains(x)) { + result.add(value); } if (x < low) { return; diff --git a/lib/web_ui/lib/src/engine/util.dart b/lib/web_ui/lib/src/engine/util.dart index ada70cd6abcad..e422d6661018b 100644 --- a/lib/web_ui/lib/src/engine/util.dart +++ b/lib/web_ui/lib/src/engine/util.dart @@ -597,45 +597,45 @@ int clampInt(int value, int min, int max) { } ui.Rect computeBoundingRectangleFromMatrix(Matrix4 transform, ui.Rect rect) { - final Float32List m = transform.storage; - // Apply perspective transform to all 4 corners. Can't use left,top, bottom, - // right since for example rotating 45 degrees would yield inaccurate size. - double x = rect.left; - double y = rect.top; - double wp = 1.0 / ((m[3] * x) + (m[7] * y) + m[15]); - double xp = ((m[0] * x) + (m[4] * y) + m[12]) * wp; - double yp = ((m[1] * x) + (m[5] * y) + m[13]) * wp; - double minX = xp, maxX = xp; - double minY =yp, maxY = yp; - x = rect.right; - y = rect.bottom; - wp = 1.0 / ((m[3] * x) + (m[7] * y) + m[15]); - xp = ((m[0] * x) + (m[4] * y) + m[12]) * wp; - yp = ((m[1] * x) + (m[5] * y) + m[13]) * wp; - - minX = math.min(minX, xp); - maxX = math.max(maxX, xp); - minY = math.min(minY, yp); - maxY = math.max(maxY, yp); - - x = rect.left; - y = rect.bottom; - wp = 1.0 / ((m[3] * x) + (m[7] * y) + m[15]); - xp = ((m[0] * x) + (m[4] * y) + m[12]) * wp; - yp = ((m[1] * x) + (m[5] * y) + m[13]) * wp; - minX = math.min(minX, xp); - maxX = math.max(maxX, xp); - minY = math.min(minY, yp); - maxY = math.max(maxY, yp); - - x = rect.right; - y = rect.top; - wp = 1.0 / ((m[3] * x) + (m[7] * y) + m[15]); - xp = ((m[0] * x) + (m[4] * y) + m[12]) * wp; - yp = ((m[1] * x) + (m[5] * y) + m[13]) * wp; - minX = math.min(minX, xp); - maxX = math.max(maxX, xp); - minY = math.min(minY, yp); - maxY = math.max(maxY, yp); - return ui.Rect.fromLTWH(minX, minY, maxX-minX, maxY-minY); - } + final Float32List m = transform.storage; + // Apply perspective transform to all 4 corners. Can't use left,top, bottom, + // right since for example rotating 45 degrees would yield inaccurate size. + double x = rect.left; + double y = rect.top; + double wp = 1.0 / ((m[3] * x) + (m[7] * y) + m[15]); + double xp = ((m[0] * x) + (m[4] * y) + m[12]) * wp; + double yp = ((m[1] * x) + (m[5] * y) + m[13]) * wp; + double minX = xp, maxX = xp; + double minY = yp, maxY = yp; + x = rect.right; + y = rect.bottom; + wp = 1.0 / ((m[3] * x) + (m[7] * y) + m[15]); + xp = ((m[0] * x) + (m[4] * y) + m[12]) * wp; + yp = ((m[1] * x) + (m[5] * y) + m[13]) * wp; + + minX = math.min(minX, xp); + maxX = math.max(maxX, xp); + minY = math.min(minY, yp); + maxY = math.max(maxY, yp); + + x = rect.left; + y = rect.bottom; + wp = 1.0 / ((m[3] * x) + (m[7] * y) + m[15]); + xp = ((m[0] * x) + (m[4] * y) + m[12]) * wp; + yp = ((m[1] * x) + (m[5] * y) + m[13]) * wp; + minX = math.min(minX, xp); + maxX = math.max(maxX, xp); + minY = math.min(minY, yp); + maxY = math.max(maxY, yp); + + x = rect.right; + y = rect.top; + wp = 1.0 / ((m[3] * x) + (m[7] * y) + m[15]); + xp = ((m[0] * x) + (m[4] * y) + m[12]) * wp; + yp = ((m[1] * x) + (m[5] * y) + m[13]) * wp; + minX = math.min(minX, xp); + maxX = math.max(maxX, xp); + minY = math.min(minY, yp); + maxY = math.max(maxY, yp); + return ui.Rect.fromLTWH(minX, minY, maxX - minX, maxY - minY); +} diff --git a/lib/web_ui/test/canvaskit/fallback_fonts_test.dart b/lib/web_ui/test/canvaskit/fallback_fonts_test.dart new file mode 100644 index 0000000000000..73ab28fde3070 --- /dev/null +++ b/lib/web_ui/test/canvaskit/fallback_fonts_test.dart @@ -0,0 +1,127 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// @dart = 2.12 +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:ui/ui.dart' as ui; +import 'package:ui/src/engine.dart'; + +import 'package:test/test.dart'; +import 'package:test/bootstrap/browser.dart'; +import 'package:web_engine_tester/golden_tester.dart'; + +import 'common.dart'; + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +const ui.Rect kDefaultRegion = const ui.Rect.fromLTRB(0, 0, 500, 250); + +Future matchPictureGolden(String goldenFile, CkPicture picture, + {ui.Rect region = kDefaultRegion, bool write = false}) async { + final EnginePlatformDispatcher dispatcher = + ui.window.platformDispatcher as EnginePlatformDispatcher; + final LayerSceneBuilder sb = LayerSceneBuilder(); + sb.pushOffset(0, 0); + sb.addPicture(ui.Offset.zero, picture); + dispatcher.rasterizer!.draw(sb.build().layerTree); + await matchGoldenFile(goldenFile, + region: region, maxDiffRatePercent: 0.0, write: write); +} + +void testMain() { + setUpCanvasKitTest(); + + group('Font fallbacks', () { + /// Used to save and restore [ui.window.onPlatformMessage] after each test. + ui.PlatformMessageCallback? savedCallback; + + setUp(() { + notoDownloadQueue.downloader = TestDownloader(); + savedCallback = ui.window.onPlatformMessage; + }); + + tearDown(() { + ui.window.onPlatformMessage = savedCallback; + }); + + test('will download Noto Naskh Arabic if Arabic text is added', () async { + final Completer fontChangeCompleter = Completer(); + // Intercept the system font change message. + ui.window.onPlatformMessage = (String name, ByteData? data, + ui.PlatformMessageResponseCallback? callback) { + if (name == 'flutter/system') { + const JSONMessageCodec codec = JSONMessageCodec(); + final dynamic message = codec.decodeMessage(data); + if (message is Map) { + if (message['type'] == 'fontsChange') { + fontChangeCompleter.complete(); + } + } + } + if (savedCallback != null) { + savedCallback!(name, data, callback); + } + }; + + // Creating this paragraph should cause us to start to download the + // fallback font. + CkParagraphBuilder pb = CkParagraphBuilder( + CkParagraphStyle(), + ); + pb.addText('مرحبا'); + + await fontChangeCompleter.future; + + expect(skiaFontCollection.globalFontFallbacks, + contains('Noto Naskh Arabic UI 0')); + + final CkPictureRecorder recorder = CkPictureRecorder(); + final CkCanvas canvas = recorder.beginRecording(kDefaultRegion); + + pb = CkParagraphBuilder( + CkParagraphStyle( + fontSize: 32, + ), + ); + pb.addText('مرحبا'); + final CkParagraph paragraph = pb.build(); + paragraph.layout(ui.ParagraphConstraints(width: 1000)); + + canvas.drawParagraph(paragraph, ui.Offset(200, 120)); + + await matchPictureGolden( + 'canvaskit_font_fallback_arabic.png', + recorder.endRecording(), + write: true, + ); + }); + }); +} + +class TestDownloader extends NotoDownloader { + final Map mockDownloads = { + 'https://fonts.googleapis.com/css2?family=Noto+Naskh+Arabic+UI': ''' +/* arabic */ +@font-face { + font-family: 'Noto Naskh Arabic UI'; + font-style: normal; + font-weight: 400; + src: url(packages/ui/assets/NotoNaskhArabic-Regular.ttf) format('ttf'); + unicode-range: U+0600-06FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE80-FEFC; +} +''', + }; + @override + Future downloadAsString(String url) async { + if (mockDownloads.containsKey(url)) { + return mockDownloads[url]!; + } else { + return ''; + } + } +} diff --git a/lib/web_ui/test/canvaskit/interval_tree_test.dart b/lib/web_ui/test/canvaskit/interval_tree_test.dart new file mode 100644 index 0000000000000..42b089ca65558 --- /dev/null +++ b/lib/web_ui/test/canvaskit/interval_tree_test.dart @@ -0,0 +1,85 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// @dart = 2.12 + +import 'package:ui/src/engine.dart'; + +import 'package:test/test.dart'; +import 'package:test/bootstrap/browser.dart'; + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() { + group('$IntervalTree', () { + test('is balanced', () { + var ranges = >{ + 'A': [CodeunitRange(0, 5), CodeunitRange(6, 10)], + 'B': [CodeunitRange(4, 6)], + }; + + // Should create a balanced 3-node tree with a root with a left and right + // child. + var tree = IntervalTree.createFromRanges(ranges); + var root = tree.root; + expect(root.left, isNotNull); + expect(root.right, isNotNull); + expect(root.left!.left, isNull); + expect(root.left!.right, isNull); + expect(root.right!.left, isNull); + expect(root.right!.right, isNull); + + // Should create a balanced 15-node tree (4 layers deep). + var ranges2 = >{ + 'A': [ + CodeunitRange(1, 1), + CodeunitRange(2, 2), + CodeunitRange(3, 3), + CodeunitRange(4, 4), + CodeunitRange(5, 5), + CodeunitRange(6, 6), + CodeunitRange(7, 7), + CodeunitRange(8, 8), + CodeunitRange(9, 9), + CodeunitRange(10, 10), + CodeunitRange(11, 11), + CodeunitRange(12, 12), + CodeunitRange(13, 13), + CodeunitRange(14, 14), + CodeunitRange(15, 15), + ], + }; + + // Should create a balanced 3-node tree with a root with a left and right + // child. + var tree2 = IntervalTree.createFromRanges(ranges2); + var root2 = tree2.root; + + expect(root2.left!.left!.left, isNotNull); + expect(root2.left!.left!.right, isNotNull); + expect(root2.left!.right!.left, isNotNull); + expect(root2.left!.right!.right, isNotNull); + expect(root2.right!.left!.left, isNotNull); + expect(root2.right!.left!.right, isNotNull); + expect(root2.right!.right!.left, isNotNull); + expect(root2.right!.right!.right, isNotNull); + }); + + test('finds values whose intervals overlap with a given point', () { + var ranges = >{ + 'A': [CodeunitRange(0, 5), CodeunitRange(7, 10)], + 'B': [CodeunitRange(4, 6)], + }; + var tree = IntervalTree.createFromRanges(ranges); + + expect(tree.intersections(1), ['A']); + expect(tree.intersections(4), ['A', 'B']); + expect(tree.intersections(6), ['B']); + expect(tree.intersections(7), ['A']); + expect(tree.intersections(11), []); + }); + }); +} From 322cf1618522673f0f7a8e7bb5242e27d3e2beef Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Wed, 20 Jan 2021 17:16:22 -0800 Subject: [PATCH 09/14] Respond to comments --- .../src/engine/canvaskit/font_fallbacks.dart | 50 ++++++++++++----- .../lib/src/engine/canvaskit/fonts.dart | 19 ++++++- .../src/engine/canvaskit/interval_tree.dart | 17 ++++-- ...t.dart => fallback_fonts_golden_test.dart} | 54 +++++++++++++------ 4 files changed, 106 insertions(+), 34 deletions(-) rename lib/web_ui/test/canvaskit/{fallback_fonts_test.dart => fallback_fonts_golden_test.dart} (75%) 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 b7f94f850f65f..5c6ad00aab42b 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart @@ -42,6 +42,10 @@ Future _findFontsForMissingCodeunits(List codeunits) async { Set<_ResolvedNotoSubset> resolvedFonts = <_ResolvedNotoSubset>{}; for (int codeunit in coveredCodeUnits) { for (_NotoFont font in fonts) { + if (font.resolvedFont == null) { + // We failed to resolve the font earlier. + continue; + } resolvedFonts.addAll(font.resolvedFont!.tree.intersections(codeunit)); } } @@ -56,8 +60,9 @@ Future _findFontsForMissingCodeunits(List codeunits) async { } else { if (!notoDownloadQueue.isPending) { html.window.console.log( - 'Could not find a Noto font to display all missing characters. ' - 'Please add a font asset for the missing characters.'); + 'Could not find a set of Noto fonts to display all missing ' + 'characters. Please add a font asset for the missing characters.' + ' See: https://flutter.dev/docs/cookbook/design/fonts'); codeUnitsWithNoKnownFont.addAll(missingCodeUnits); } } @@ -92,7 +97,7 @@ Future _findFontsForMissingCodeunits(List codeunits) async { /// 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) { +_ResolvedNotoFont? _makeResolvedNotoFontFromCss(String css, String name) { List<_ResolvedNotoSubset> subsets = <_ResolvedNotoSubset>[]; bool resolvingFontFace = false; String? fontFaceUrl; @@ -110,7 +115,8 @@ _ResolvedNotoFont _makeResolvedNotoFontFromCss(String css, String name) { if (line.startsWith(' src:')) { int urlStart = line.indexOf('url('); if (urlStart == -1) { - throw new Exception('Unable to resolve Noto font URL: $line'); + html.window.console.warn('Unable to resolve Noto font URL: $line'); + return null; } int urlEnd = line.indexOf(')'); fontFaceUrl = line.substring(urlStart + 4, urlEnd); @@ -136,8 +142,12 @@ _ResolvedNotoFont _makeResolvedNotoFontFromCss(String css, String name) { } } } else if (line == '}') { - subsets.add( - _ResolvedNotoSubset(fontFaceUrl!, name, fontFaceUnicodeRanges!)); + if (fontFaceUrl == null || fontFaceUnicodeRanges == null) { + html.window.console.warn('Unable to parse Google Fonts CSS: $css'); + return null; + } + subsets + .add(_ResolvedNotoSubset(fontFaceUrl, name, fontFaceUnicodeRanges)); resolvingFontFace = false; } else { continue; @@ -145,6 +155,11 @@ _ResolvedNotoFont _makeResolvedNotoFontFromCss(String css, String name) { } } + if (resolvingFontFace) { + html.window.console.warn('Unable to parse Google Fonts CSS: $css'); + return null; + } + Map<_ResolvedNotoSubset, List> rangesMap = <_ResolvedNotoSubset, List>{}; for (_ResolvedNotoSubset subset in subsets) { @@ -177,27 +192,34 @@ Future _registerSymbolsAndEmoji() async { String emojiCss = await notoDownloadQueue.downloader.downloadAsString(emojiUrl); - String extractUrlFromCss(String css) { + 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'); + html.window.console.warn('Unable to resolve Noto font URL: $line'); + return null; } int urlEnd = line.indexOf(')'); return line.substring(urlStart + 4, urlEnd); } } - throw Exception('Unable to determine URL for Noto font'); + html.window.console.warn('Unable to determine URL for Noto font'); + return null; } - String symbolsFontUrl = extractUrlFromCss(symbolsCss); - String emojiFontUrl = extractUrlFromCss(emojiCss); + String? symbolsFontUrl = extractUrlFromCss(symbolsCss); + String? emojiFontUrl = extractUrlFromCss(emojiCss); + + if (symbolsFontUrl == null || emojiFontUrl == null) { + html.window.console + .warn('Error parsing CSS for Noto Emoji and Symbols font.'); + } notoDownloadQueue.add(_ResolvedNotoSubset( - symbolsFontUrl, 'Noto Sans Symbols', const [])); + symbolsFontUrl!, 'Noto Sans Symbols', const [])); notoDownloadQueue.add(_ResolvedNotoSubset( - emojiFontUrl, 'Noto Color Emoji Compat', const [])); + emojiFontUrl!, 'Noto Color Emoji Compat', const [])); } /// Finds the minimum set of fonts which covers all of the [codeunits]. @@ -315,7 +337,7 @@ class _NotoFont { _decodingCompleter = Completer(); String googleFontCss = await notoDownloadQueue.downloader .downloadAsString(googleFontsCssUrl); - final _ResolvedNotoFont googleFont = + final _ResolvedNotoFont? googleFont = _makeResolvedNotoFontFromCss(googleFontCss, name); resolvedFont = googleFont; _decodingCompleter!.complete(); diff --git a/lib/web_ui/lib/src/engine/canvaskit/fonts.dart b/lib/web_ui/lib/src/engine/canvaskit/fonts.dart index 9f6074c7b1bc7..d3692a887283f 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/fonts.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/fonts.dart @@ -22,6 +22,9 @@ class SkiaFontCollection { /// Fonts which have been registered and loaded. final List<_RegisteredFont> _registeredFonts = <_RegisteredFont>[]; + /// Fallback fonts which have been registered and loaded. + final List<_RegisteredFont> _registeredFallbackFonts = <_RegisteredFont>[]; + final Map> familyToTypefaceMap = >{}; @@ -45,6 +48,13 @@ class SkiaFontCollection { .putIfAbsent(font.family, () => []) .add(font.typeface); } + + for (var font in _registeredFallbackFonts) { + fontProvider!.registerFont(font.bytes, font.family); + familyToTypefaceMap + .putIfAbsent(font.family, () => []) + .add(font.typeface); + } } /// Loads all of the unloaded fonts in [_unloadedFonts] and adds them @@ -146,7 +156,7 @@ class SkiaFontCollection { int fontFallbackTag = _fontFallbackCounts[family]!; _fontFallbackCounts[family] = _fontFallbackCounts[family]! + 1; String countedFamily = '$family $fontFallbackTag'; - _registeredFonts.add(_RegisteredFont(bytes, countedFamily)); + _registeredFallbackFonts.add(_RegisteredFont(bytes, countedFamily)); globalFontFallbacks.add(countedFamily); } @@ -164,6 +174,13 @@ class SkiaFontCollection { .then((dynamic x) => x as ByteBuffer); } + /// Resets the fallback fonts. Used for tests. + void debugResetFallbackFonts() { + _registeredFallbackFonts.clear(); + globalFontFallbacks.clear(); + _fontFallbackCounts.clear(); + } + SkFontMgr? skFontMgr; TypefaceFontProvider? fontProvider; } diff --git a/lib/web_ui/lib/src/engine/canvaskit/interval_tree.dart b/lib/web_ui/lib/src/engine/canvaskit/interval_tree.dart index d614ee86bf46c..88a097e65369a 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/interval_tree.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/interval_tree.dart @@ -5,20 +5,26 @@ // @dart = 2.12 part of engine; -/// Associates a [T] with one or more [CodeunitRange]s. +/// A tree which stores a set of intervals that can be queried for intersection. class IntervalTree { + /// The root node of the interval tree. final IntervalTreeNode root; IntervalTree._(this.root); + /// Creates an interval tree from a mapping of [T] values to a list of ranges. + /// + /// When the interval tree is queried, it will return a list of [T]s which + /// have a range which contains the point. factory IntervalTree.createFromRanges(Map> rangesMap) { // Get a list of all the ranges ordered by start index. List> intervals = >[]; - for (T key in rangesMap.keys) { - for (CodeunitRange range in rangesMap[key]!) { + rangesMap.forEach((T key, List rangeList) { + for (CodeunitRange range in rangeList) { intervals.add(IntervalTreeNode(key, range.start, range.end)); } - } + }); + intervals .sort((IntervalTreeNode a, IntervalTreeNode b) => a.low - b.low); @@ -37,6 +43,9 @@ class IntervalTree { return root; } + // Given a node, computes the highest `high` point of all of the subnodes. + // + // As a side effect, this also computes the high point of all subnodes. void _computeHigh(IntervalTreeNode root) { if (root.left == null && root.right == null) { root.computedHigh = root.high; diff --git a/lib/web_ui/test/canvaskit/fallback_fonts_test.dart b/lib/web_ui/test/canvaskit/fallback_fonts_golden_test.dart similarity index 75% rename from lib/web_ui/test/canvaskit/fallback_fonts_test.dart rename to lib/web_ui/test/canvaskit/fallback_fonts_golden_test.dart index 73ab28fde3070..be92997a3a2b5 100644 --- a/lib/web_ui/test/canvaskit/fallback_fonts_test.dart +++ b/lib/web_ui/test/canvaskit/fallback_fonts_golden_test.dart @@ -42,7 +42,9 @@ void testMain() { setUp(() { notoDownloadQueue.downloader = TestDownloader(); + TestDownloader.mockDownloads.clear(); savedCallback = ui.window.onPlatformMessage; + skiaFontCollection.debugResetFallbackFonts(); }); tearDown(() { @@ -68,6 +70,21 @@ void testMain() { } }; + TestDownloader.mockDownloads[ + 'https://fonts.googleapis.com/css2?family=Noto+Naskh+Arabic+UI'] = + ''' +/* arabic */ +@font-face { + font-family: 'Noto Naskh Arabic UI'; + font-style: normal; + font-weight: 400; + src: url(packages/ui/assets/NotoNaskhArabic-Regular.ttf) format('ttf'); + unicode-range: U+0600-06FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE80-FEFC; +} +'''; + + expect(skiaFontCollection.globalFontFallbacks, isEmpty); + // Creating this paragraph should cause us to start to download the // fallback font. CkParagraphBuilder pb = CkParagraphBuilder( @@ -95,27 +112,34 @@ void testMain() { canvas.drawParagraph(paragraph, ui.Offset(200, 120)); await matchPictureGolden( - 'canvaskit_font_fallback_arabic.png', - recorder.endRecording(), - write: true, + 'canvaskit_font_fallback_arabic.png', recorder.endRecording()); + }); + + test('will gracefully fail if we cannot parse the Google Fonts CSS', + () async { + TestDownloader.mockDownloads[ + 'https://fonts.googleapis.com/css2?family=Noto+Naskh+Arabic+UI'] = + 'invalid CSS... this should cause our parser to fail'; + + expect(skiaFontCollection.globalFontFallbacks, isEmpty); + + // Creating this paragraph should cause us to start to download the + // fallback font. + CkParagraphBuilder pb = CkParagraphBuilder( + CkParagraphStyle(), ); + pb.addText('مرحبا'); + + // Flush microtasks and test that we didn't start any downloads. + + expect(notoDownloadQueue.isPending, isFalse); + expect(skiaFontCollection.globalFontFallbacks, isEmpty); }); }); } class TestDownloader extends NotoDownloader { - final Map mockDownloads = { - 'https://fonts.googleapis.com/css2?family=Noto+Naskh+Arabic+UI': ''' -/* arabic */ -@font-face { - font-family: 'Noto Naskh Arabic UI'; - font-style: normal; - font-weight: 400; - src: url(packages/ui/assets/NotoNaskhArabic-Regular.ttf) format('ttf'); - unicode-range: U+0600-06FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE80-FEFC; -} -''', - }; + static final Map mockDownloads = {}; @override Future downloadAsString(String url) async { if (mockDownloads.containsKey(url)) { From d1d66e1b6c0765fb6894cda04b7347bca756fd0c Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Wed, 20 Jan 2021 17:31:44 -0800 Subject: [PATCH 10/14] Fix test --- lib/web_ui/test/canvaskit/fallback_fonts_golden_test.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/web_ui/test/canvaskit/fallback_fonts_golden_test.dart b/lib/web_ui/test/canvaskit/fallback_fonts_golden_test.dart index be92997a3a2b5..c77c492ed174d 100644 --- a/lib/web_ui/test/canvaskit/fallback_fonts_golden_test.dart +++ b/lib/web_ui/test/canvaskit/fallback_fonts_golden_test.dart @@ -131,6 +131,7 @@ void testMain() { pb.addText('مرحبا'); // Flush microtasks and test that we didn't start any downloads. + await Future.delayed(Duration.zero); expect(notoDownloadQueue.isPending, isFalse); expect(skiaFontCollection.globalFontFallbacks, isEmpty); From a56fc8588cd30509ddc19aa35e842e143436bd3e Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Wed, 20 Jan 2021 20:19:01 -0800 Subject: [PATCH 11/14] Update goldens lock --- lib/web_ui/dev/goldens_lock.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web_ui/dev/goldens_lock.yaml b/lib/web_ui/dev/goldens_lock.yaml index f309c77a250fc..ac002b153ebd9 100644 --- a/lib/web_ui/dev/goldens_lock.yaml +++ b/lib/web_ui/dev/goldens_lock.yaml @@ -1,2 +1,2 @@ repository: https://github.com/flutter/goldens.git -revision: ee03ff97af36cbf9bd2627ef4e32f5a45676f96f +revision: bdb442c42588b25c657779c78523822e349742d5 From 493b6cb2c0431f9f2ab7cad3bcfb438293a77145 Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Thu, 21 Jan 2021 10:06:17 -0800 Subject: [PATCH 12/14] Skip screenshot test on Safari --- lib/web_ui/test/canvaskit/fallback_fonts_golden_test.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/web_ui/test/canvaskit/fallback_fonts_golden_test.dart b/lib/web_ui/test/canvaskit/fallback_fonts_golden_test.dart index c77c492ed174d..060c411951cf4 100644 --- a/lib/web_ui/test/canvaskit/fallback_fonts_golden_test.dart +++ b/lib/web_ui/test/canvaskit/fallback_fonts_golden_test.dart @@ -113,7 +113,9 @@ void testMain() { await matchPictureGolden( 'canvaskit_font_fallback_arabic.png', recorder.endRecording()); - }); + // TODO: https://github.com/flutter/flutter/issues/60040 + // TODO: https://github.com/flutter/flutter/issues/71520 + }, skip: isIosSafari || isFirefox); test('will gracefully fail if we cannot parse the Google Fonts CSS', () async { From 7d4680585205324d5ada8dd8a65326ac9704195d Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Thu, 21 Jan 2021 10:55:52 -0800 Subject: [PATCH 13/14] Skip CanvasKit tests on iOS Safari --- lib/web_ui/test/canvaskit/fallback_fonts_golden_test.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/web_ui/test/canvaskit/fallback_fonts_golden_test.dart b/lib/web_ui/test/canvaskit/fallback_fonts_golden_test.dart index 060c411951cf4..f4ee8870e9678 100644 --- a/lib/web_ui/test/canvaskit/fallback_fonts_golden_test.dart +++ b/lib/web_ui/test/canvaskit/fallback_fonts_golden_test.dart @@ -138,7 +138,8 @@ void testMain() { expect(notoDownloadQueue.isPending, isFalse); expect(skiaFontCollection.globalFontFallbacks, isEmpty); }); - }); + // TODO: https://github.com/flutter/flutter/issues/60040 + }, skip: isIosSafari); } class TestDownloader extends NotoDownloader { From e1bc9b04185b40624272ff5e3568b24f59618c3d Mon Sep 17 00:00:00 2001 From: Harry Terkelsen Date: Thu, 21 Jan 2021 11:44:29 -0800 Subject: [PATCH 14/14] Move CanvasKit initialization so it doesn't run on iOS Safari --- lib/web_ui/test/canvaskit/fallback_fonts_golden_test.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/web_ui/test/canvaskit/fallback_fonts_golden_test.dart b/lib/web_ui/test/canvaskit/fallback_fonts_golden_test.dart index f4ee8870e9678..7c6a591f94c25 100644 --- a/lib/web_ui/test/canvaskit/fallback_fonts_golden_test.dart +++ b/lib/web_ui/test/canvaskit/fallback_fonts_golden_test.dart @@ -34,9 +34,9 @@ Future matchPictureGolden(String goldenFile, CkPicture picture, } void testMain() { - setUpCanvasKitTest(); - group('Font fallbacks', () { + setUpCanvasKitTest(); + /// Used to save and restore [ui.window.onPlatformMessage] after each test. ui.PlatformMessageCallback? savedCallback;