| // 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. |
| |
| import 'dart:io'; |
| // TODO(a14n): remove this import once Flutter 3.1 or later reaches stable (including flutter/flutter#104231) |
| // ignore: unnecessary_import |
| import 'dart:typed_data'; |
| import 'dart:ui' as ui show Codec, FrameInfo, Image, instantiateImageCodec; |
| |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/material.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| import 'package:palette_generator/palette_generator.dart'; |
| import 'package:path/path.dart' as path; |
| |
| /// An image provider implementation for testing that takes a pre-loaded image. |
| /// This avoids handling asynchronous I/O in the test zone, which is |
| /// problematic. |
| class FakeImageProvider extends ImageProvider<FakeImageProvider> { |
| const FakeImageProvider(this._image, {this.scale = 1.0}); |
| |
| final ui.Image _image; |
| |
| /// The scale to place in the [ImageInfo] object of the image. |
| final double scale; |
| |
| @override |
| Future<FakeImageProvider> obtainKey(ImageConfiguration configuration) { |
| return SynchronousFuture<FakeImageProvider>(this); |
| } |
| |
| @override |
| // TODO(cyanglaz): migrate to use the new APIs |
| // https://github.com/flutter/flutter/issues/105336 |
| // ignore: deprecated_member_use |
| ImageStreamCompleter load(FakeImageProvider key, DecoderCallback decode) { |
| assert(key == this); |
| return OneFrameImageStreamCompleter( |
| SynchronousFuture<ImageInfo>( |
| ImageInfo(image: _image, scale: scale), |
| ), |
| ); |
| } |
| } |
| |
| Future<FakeImageProvider> loadImage(String name) async { |
| File imagePath = File(path.joinAll(<String>['assets', name])); |
| if (path.split(Directory.current.absolute.path).last != 'test') { |
| imagePath = File(path.join('test', imagePath.path)); |
| } |
| final Uint8List data = Uint8List.fromList(imagePath.readAsBytesSync()); |
| final ui.Codec codec = await ui.instantiateImageCodec(data); |
| final ui.FrameInfo frameInfo = await codec.getNextFrame(); |
| return FakeImageProvider(frameInfo.image); |
| } |
| |
| Future<void> main() async { |
| // Load the images outside of the test zone so that IO doesn't get |
| // complicated. |
| final List<String> imageNames = <String>[ |
| 'tall_blue', |
| 'wide_red', |
| 'dominant', |
| 'landscape' |
| ]; |
| final Map<String, FakeImageProvider> testImages = |
| <String, FakeImageProvider>{}; |
| for (final String name in imageNames) { |
| testImages[name] = await loadImage('$name.png'); |
| } |
| |
| testWidgets('Initialize the image cache', (WidgetTester tester) async { |
| // We need to have a testWidgets test in order to initialize the image |
| // cache for the other tests, but they timeout if they too are testWidgets |
| // tests. |
| tester.pumpWidget(const Placeholder()); |
| }); |
| |
| test( |
| "PaletteGenerator.fromByteData throws when the size doesn't match the byte data size", |
| () { |
| expect( |
| () async { |
| final ByteData? data = |
| await testImages['tall_blue']!._image.toByteData(); |
| await PaletteGenerator.fromByteData( |
| EncodedImage( |
| data!, |
| width: 1, |
| height: 1, |
| ), |
| ); |
| }, |
| throwsAssertionError, |
| ); |
| }); |
| |
| test('PaletteGenerator.fromImage works', () async { |
| final PaletteGenerator palette = |
| await PaletteGenerator.fromImage(testImages['tall_blue']!._image); |
| expect(palette.paletteColors.length, equals(1)); |
| expect(palette.paletteColors[0].color, |
| within<Color>(distance: 8, from: const Color(0xff0000ff))); |
| }); |
| |
| test('PaletteGenerator works on 1-pixel tall blue image', () async { |
| final PaletteGenerator palette = |
| await PaletteGenerator.fromImageProvider(testImages['tall_blue']!); |
| expect(palette.paletteColors.length, equals(1)); |
| expect(palette.paletteColors[0].color, |
| within<Color>(distance: 8, from: const Color(0xff0000ff))); |
| }); |
| |
| test('PaletteGenerator works on 1-pixel wide red image', () async { |
| final PaletteGenerator palette = |
| await PaletteGenerator.fromImageProvider(testImages['wide_red']!); |
| expect(palette.paletteColors.length, equals(1)); |
| expect(palette.paletteColors[0].color, |
| within<Color>(distance: 8, from: const Color(0xffff0000))); |
| }); |
| |
| test('PaletteGenerator finds dominant color and text colors', () async { |
| final PaletteGenerator palette = |
| await PaletteGenerator.fromImageProvider(testImages['dominant']!); |
| expect(palette.paletteColors.length, equals(3)); |
| expect(palette.dominantColor, isNotNull); |
| expect(palette.dominantColor!.color, |
| within<Color>(distance: 8, from: const Color(0xff0000ff))); |
| expect(palette.dominantColor!.titleTextColor, |
| within<Color>(distance: 8, from: const Color(0x8affffff))); |
| expect(palette.dominantColor!.bodyTextColor, |
| within<Color>(distance: 8, from: const Color(0xb2ffffff))); |
| }); |
| |
| test('PaletteGenerator works with regions', () async { |
| final ImageProvider imageProvider = testImages['dominant']!; |
| Rect region = const Rect.fromLTRB(0.0, 0.0, 100.0, 100.0); |
| const Size size = Size(100.0, 100.0); |
| PaletteGenerator palette = await PaletteGenerator.fromImageProvider( |
| imageProvider, |
| region: region, |
| size: size); |
| expect(palette.paletteColors.length, equals(3)); |
| expect(palette.dominantColor, isNotNull); |
| expect(palette.dominantColor!.color, |
| within<Color>(distance: 8, from: const Color(0xff0000ff))); |
| |
| region = const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0); |
| palette = await PaletteGenerator.fromImageProvider(imageProvider, |
| region: region, size: size); |
| expect(palette.paletteColors.length, equals(1)); |
| expect(palette.dominantColor, isNotNull); |
| expect(palette.dominantColor!.color, |
| within<Color>(distance: 8, from: const Color(0xffff0000))); |
| |
| region = const Rect.fromLTRB(0.0, 0.0, 30.0, 20.0); |
| palette = await PaletteGenerator.fromImageProvider(imageProvider, |
| region: region, size: size); |
| expect(palette.paletteColors.length, equals(3)); |
| expect(palette.dominantColor, isNotNull); |
| expect(palette.dominantColor!.color, |
| within<Color>(distance: 8, from: const Color(0xff00ff00))); |
| }); |
| |
| test('PaletteGenerator works as expected on a real image', () async { |
| final PaletteGenerator palette = |
| await PaletteGenerator.fromImageProvider(testImages['landscape']!); |
| final List<PaletteColor> expectedSwatches = <PaletteColor>[ |
| PaletteColor(const Color(0xff3f630c), 10137), |
| PaletteColor(const Color(0xff3c4b2a), 4773), |
| PaletteColor(const Color(0xff81b2e9), 4762), |
| PaletteColor(const Color(0xffc0d6ec), 4714), |
| PaletteColor(const Color(0xff4c4f50), 2465), |
| PaletteColor(const Color(0xff5c635b), 2463), |
| PaletteColor(const Color(0xff6e80a2), 2421), |
| PaletteColor(const Color(0xff9995a3), 1214), |
| PaletteColor(const Color(0xff676c4d), 1213), |
| PaletteColor(const Color(0xffc4b2b2), 1173), |
| PaletteColor(const Color(0xff445166), 1040), |
| PaletteColor(const Color(0xff475d83), 1019), |
| PaletteColor(const Color(0xff7e7360), 589), |
| PaletteColor(const Color(0xfff6b835), 286), |
| PaletteColor(const Color(0xffb9983d), 152), |
| PaletteColor(const Color(0xffe3ab35), 149), |
| ]; |
| final Iterable<Color> expectedColors = |
| expectedSwatches.map<Color>((PaletteColor swatch) => swatch.color); |
| expect(palette.paletteColors, containsAll(expectedSwatches)); |
| expect(palette.vibrantColor, isNotNull); |
| expect(palette.lightVibrantColor, isNotNull); |
| expect(palette.darkVibrantColor, isNotNull); |
| expect(palette.mutedColor, isNotNull); |
| expect(palette.lightMutedColor, isNotNull); |
| expect(palette.darkMutedColor, isNotNull); |
| expect(palette.vibrantColor!.color, |
| within<Color>(distance: 8, from: const Color(0xfff6b835))); |
| expect(palette.lightVibrantColor!.color, |
| within<Color>(distance: 8, from: const Color(0xff82b2e9))); |
| expect(palette.darkVibrantColor!.color, |
| within<Color>(distance: 8, from: const Color(0xff3f630c))); |
| expect(palette.mutedColor!.color, |
| within<Color>(distance: 8, from: const Color(0xff6c7fa2))); |
| expect(palette.lightMutedColor!.color, |
| within<Color>(distance: 8, from: const Color(0xffc4b2b2))); |
| expect(palette.darkMutedColor!.color, |
| within<Color>(distance: 8, from: const Color(0xff3c4b2a))); |
| expect(palette.colors, containsAllInOrder(expectedColors)); |
| expect(palette.colors.length, equals(palette.paletteColors.length)); |
| }); |
| |
| test('PaletteGenerator limits max colors', () async { |
| final ImageProvider imageProvider = testImages['landscape']!; |
| PaletteGenerator palette = await PaletteGenerator.fromImageProvider( |
| imageProvider, |
| maximumColorCount: 32); |
| expect(palette.paletteColors.length, equals(31)); |
| palette = await PaletteGenerator.fromImageProvider(imageProvider, |
| maximumColorCount: 1); |
| expect(palette.paletteColors.length, equals(1)); |
| palette = await PaletteGenerator.fromImageProvider(imageProvider, |
| maximumColorCount: 15); |
| expect(palette.paletteColors.length, equals(15)); |
| }); |
| |
| test('PaletteGenerator filters work', () async { |
| final ImageProvider imageProvider = testImages['landscape']!; |
| // First, test that supplying the default filter is the same as not supplying one. |
| List<PaletteFilter> filters = <PaletteFilter>[ |
| avoidRedBlackWhitePaletteFilter |
| ]; |
| PaletteGenerator palette = await PaletteGenerator.fromImageProvider( |
| imageProvider, |
| filters: filters); |
| final List<PaletteColor> expectedSwatches = <PaletteColor>[ |
| PaletteColor(const Color(0xff3f630c), 10137), |
| PaletteColor(const Color(0xff3c4b2a), 4773), |
| PaletteColor(const Color(0xff81b2e9), 4762), |
| PaletteColor(const Color(0xffc0d6ec), 4714), |
| PaletteColor(const Color(0xff4c4f50), 2465), |
| PaletteColor(const Color(0xff5c635b), 2463), |
| PaletteColor(const Color(0xff6e80a2), 2421), |
| PaletteColor(const Color(0xff9995a3), 1214), |
| PaletteColor(const Color(0xff676c4d), 1213), |
| PaletteColor(const Color(0xffc4b2b2), 1173), |
| PaletteColor(const Color(0xff445166), 1040), |
| PaletteColor(const Color(0xff475d83), 1019), |
| PaletteColor(const Color(0xff7e7360), 589), |
| PaletteColor(const Color(0xfff6b835), 286), |
| PaletteColor(const Color(0xffb9983d), 152), |
| PaletteColor(const Color(0xffe3ab35), 149), |
| ]; |
| final Iterable<Color> expectedColors = |
| expectedSwatches.map<Color>((PaletteColor swatch) => swatch.color); |
| expect(palette.paletteColors, containsAll(expectedSwatches)); |
| expect(palette.dominantColor, isNotNull); |
| expect(palette.dominantColor!.color, |
| within<Color>(distance: 8, from: const Color(0xff3f630c))); |
| expect(palette.colors, containsAllInOrder(expectedColors)); |
| |
| // A non-default filter works (and the default filter isn't applied too). |
| filters = <PaletteFilter>[onlyBluePaletteFilter]; |
| palette = await PaletteGenerator.fromImageProvider(imageProvider, |
| filters: filters); |
| final List<PaletteColor> blueSwatches = <PaletteColor>[ |
| PaletteColor(const Color(0xff4c5c75), 1515), |
| PaletteColor(const Color(0xff7483a1), 1505), |
| PaletteColor(const Color(0xff515661), 1476), |
| PaletteColor(const Color(0xff769dd4), 1470), |
| PaletteColor(const Color(0xff3e4858), 777), |
| PaletteColor(const Color(0xff98a3bc), 760), |
| PaletteColor(const Color(0xffb4c7e0), 760), |
| PaletteColor(const Color(0xff99bbe5), 742), |
| PaletteColor(const Color(0xffcbdef0), 701), |
| PaletteColor(const Color(0xff1c212b), 429), |
| PaletteColor(const Color(0xff393c46), 417), |
| PaletteColor(const Color(0xff526483), 394), |
| PaletteColor(const Color(0xff61708b), 372), |
| PaletteColor(const Color(0xff5e8ccc), 345), |
| PaletteColor(const Color(0xff587ab4), 194), |
| PaletteColor(const Color(0xff5584c8), 182), |
| ]; |
| final Iterable<Color> expectedBlues = |
| blueSwatches.map<Color>((PaletteColor swatch) => swatch.color); |
| |
| expect(palette.paletteColors, containsAll(blueSwatches)); |
| expect(palette.dominantColor, isNotNull); |
| expect(palette.dominantColor!.color, |
| within<Color>(distance: 8, from: const Color(0xff4c5c75))); |
| expect(palette.colors, containsAllInOrder(expectedBlues)); |
| |
| // More than one filter is the intersection of the two filters. |
| filters = <PaletteFilter>[onlyBluePaletteFilter, onlyCyanPaletteFilter]; |
| palette = await PaletteGenerator.fromImageProvider(imageProvider, |
| filters: filters); |
| final List<PaletteColor> blueGreenSwatches = <PaletteColor>[ |
| PaletteColor(const Color(0xffc8e8f8), 87), |
| PaletteColor(const Color(0xff5c6c74), 73), |
| PaletteColor(const Color(0xff6f8088), 49), |
| PaletteColor(const Color(0xff687880), 49), |
| PaletteColor(const Color(0xff506068), 45), |
| PaletteColor(const Color(0xff485860), 39), |
| PaletteColor(const Color(0xff405058), 21), |
| PaletteColor(const Color(0xffd6ebf3), 11), |
| PaletteColor(const Color(0xff2f3f47), 7), |
| PaletteColor(const Color(0xff0f1f27), 6), |
| PaletteColor(const Color(0xffc0e0f0), 6), |
| PaletteColor(const Color(0xff203038), 3), |
| PaletteColor(const Color(0xff788890), 2), |
| PaletteColor(const Color(0xff384850), 2), |
| PaletteColor(const Color(0xff98a8b0), 1), |
| PaletteColor(const Color(0xffa8b8c0), 1), |
| ]; |
| final Iterable<Color> expectedBlueGreens = |
| blueGreenSwatches.map<Color>((PaletteColor swatch) => swatch.color); |
| |
| expect(palette.paletteColors, containsAll(blueGreenSwatches)); |
| expect(palette.dominantColor, isNotNull); |
| expect(palette.dominantColor!.color, |
| within<Color>(distance: 8, from: const Color(0xffc8e8f8))); |
| expect(palette.colors, containsAllInOrder(expectedBlueGreens)); |
| |
| // Mutually exclusive filters return an empty palette. |
| filters = <PaletteFilter>[onlyBluePaletteFilter, onlyGreenPaletteFilter]; |
| palette = await PaletteGenerator.fromImageProvider(imageProvider, |
| filters: filters); |
| expect(palette.paletteColors, isEmpty); |
| expect(palette.dominantColor, isNull); |
| expect(palette.colors, isEmpty); |
| }); |
| |
| test('PaletteGenerator targets work', () async { |
| final ImageProvider imageProvider = testImages['landscape']!; |
| // Passing an empty set of targets works the same as passing a null targets |
| // list. |
| PaletteGenerator palette = await PaletteGenerator.fromImageProvider( |
| imageProvider, |
| targets: <PaletteTarget>[]); |
| expect(palette.selectedSwatches, isNotEmpty); |
| expect(palette.vibrantColor, isNotNull); |
| expect(palette.lightVibrantColor, isNotNull); |
| expect(palette.darkVibrantColor, isNotNull); |
| expect(palette.mutedColor, isNotNull); |
| expect(palette.lightMutedColor, isNotNull); |
| expect(palette.darkMutedColor, isNotNull); |
| |
| // Passing targets augments the baseTargets, and those targets are found. |
| final List<PaletteTarget> saturationExtremeTargets = <PaletteTarget>[ |
| PaletteTarget(minimumSaturation: 0.85), |
| PaletteTarget(maximumSaturation: .25), |
| ]; |
| palette = await PaletteGenerator.fromImageProvider(imageProvider, |
| targets: saturationExtremeTargets); |
| expect(palette.vibrantColor, isNotNull); |
| expect(palette.lightVibrantColor, isNotNull); |
| expect(palette.darkVibrantColor, isNotNull); |
| expect(palette.mutedColor, isNotNull); |
| expect(palette.lightMutedColor, isNotNull); |
| expect(palette.darkMutedColor, isNotNull); |
| expect(palette.selectedSwatches.length, |
| equals(PaletteTarget.baseTargets.length + 2)); |
| final PaletteColor? selectedSwatchesFirst = |
| palette.selectedSwatches[saturationExtremeTargets[0]]; |
| final PaletteColor? selectedSwatchesSecond = |
| palette.selectedSwatches[saturationExtremeTargets[1]]; |
| expect(selectedSwatchesFirst, isNotNull); |
| expect(selectedSwatchesSecond, isNotNull); |
| expect(selectedSwatchesFirst!.color, equals(const Color(0xfff6b835))); |
| expect(selectedSwatchesSecond!.color, equals(const Color(0xff6e80a2))); |
| }); |
| |
| test('PaletteGenerator produces consistent results', () async { |
| final ImageProvider imageProvider = testImages['landscape']!; |
| |
| PaletteGenerator lastPalette = |
| await PaletteGenerator.fromImageProvider(imageProvider); |
| for (int i = 0; i < 5; ++i) { |
| final PaletteGenerator palette = |
| await PaletteGenerator.fromImageProvider(imageProvider); |
| expect(palette.paletteColors.length, lastPalette.paletteColors.length); |
| expect(palette.vibrantColor, equals(lastPalette.vibrantColor)); |
| expect(palette.lightVibrantColor, equals(lastPalette.lightVibrantColor)); |
| expect(palette.darkVibrantColor, equals(lastPalette.darkVibrantColor)); |
| expect(palette.mutedColor, equals(lastPalette.mutedColor)); |
| expect(palette.lightMutedColor, equals(lastPalette.lightMutedColor)); |
| expect(palette.darkMutedColor, equals(lastPalette.darkMutedColor)); |
| expect(palette.dominantColor, isNotNull); |
| expect(lastPalette.dominantColor, isNotNull); |
| expect(palette.dominantColor!.color, |
| within<Color>(distance: 8, from: lastPalette.dominantColor!.color)); |
| lastPalette = palette; |
| } |
| }); |
| |
| // TODO(gspencergoog): rewrite to use fromImageProvider when https://github.com/flutter/flutter/issues/10647 is resolved, |
| // since fromImageProvider calls fromImage which calls fromByteData |
| |
| test('PaletteGenerator.fromByteData works in non-root isolate', () async { |
| final ui.Image image = testImages['tall_blue']!._image; |
| final ByteData? data = await image.toByteData(); |
| final PaletteGenerator palette = |
| await compute<EncodedImage, PaletteGenerator>( |
| _computeFromByteData, |
| EncodedImage(data!, width: image.width, height: image.height), |
| ); |
| expect(palette.paletteColors.length, equals(1)); |
| expect(palette.paletteColors[0].color, |
| within<Color>(distance: 8, from: const Color(0xff0000ff))); |
| }); |
| |
| test('PaletteColor == does not crash on invalid comparisons', () { |
| final PaletteColor paletteColorA = PaletteColor(const Color(0xFFFFFFFF), 1); |
| final PaletteColor paletteColorB = PaletteColor(const Color(0xFFFFFFFF), 1); |
| final Object object = Object(); |
| |
| expect(paletteColorA == paletteColorB, true); |
| expect(paletteColorA == object, false); |
| }); |
| |
| test('PaletteTarget == does not crash on invalid comparisons', () { |
| final PaletteTarget paletteTargetA = PaletteTarget(); |
| final PaletteTarget paletteTargetB = PaletteTarget(); |
| final Object object = Object(); |
| |
| expect(paletteTargetA == paletteTargetB, true); |
| expect(paletteTargetA == object, false); |
| }); |
| } |
| |
| Future<PaletteGenerator> _computeFromByteData(EncodedImage encodedImage) async { |
| return PaletteGenerator.fromByteData(encodedImage); |
| } |
| |
| bool onlyBluePaletteFilter(HSLColor hslColor) { |
| const double blueLineMinHue = 185.0; |
| const double blueLineMaxHue = 260.0; |
| const double blueLineMaxSaturation = 0.82; |
| return hslColor.hue >= blueLineMinHue && |
| hslColor.hue <= blueLineMaxHue && |
| hslColor.saturation <= blueLineMaxSaturation; |
| } |
| |
| bool onlyCyanPaletteFilter(HSLColor hslColor) { |
| const double cyanLineMinHue = 165.0; |
| const double cyanLineMaxHue = 200.0; |
| const double cyanLineMaxSaturation = 0.82; |
| return hslColor.hue >= cyanLineMinHue && |
| hslColor.hue <= cyanLineMaxHue && |
| hslColor.saturation <= cyanLineMaxSaturation; |
| } |
| |
| bool onlyGreenPaletteFilter(HSLColor hslColor) { |
| const double greenLineMinHue = 80.0; |
| const double greenLineMaxHue = 165.0; |
| const double greenLineMaxSaturation = 0.82; |
| return hslColor.hue >= greenLineMinHue && |
| hslColor.hue <= greenLineMaxHue && |
| hslColor.saturation <= greenLineMaxSaturation; |
| } |