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 61fe5fefa9917..f8e797d42eef9 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(), ); });