[palette_generator] Add PaletteGenerator.fromByteData (#450)
diff --git a/packages/palette_generator/CHANGELOG.md b/packages/palette_generator/CHANGELOG.md
index 786dcc7..8de0bd6 100644
--- a/packages/palette_generator/CHANGELOG.md
+++ b/packages/palette_generator/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.3.1
+
+* Add PaletteGenerator.fromByteData to allow creating palette from image byte data.
+
## 0.3.0
* Migrated to null safety.
diff --git a/packages/palette_generator/lib/palette_generator.dart b/packages/palette_generator/lib/palette_generator.dart
index 176ad51..fac6f87 100644
--- a/packages/palette_generator/lib/palette_generator.dart
+++ b/packages/palette_generator/lib/palette_generator.dart
@@ -10,13 +10,34 @@
import 'dart:math' as math;
import 'dart:typed_data';
import 'dart:ui' as ui;
-import 'dart:ui' show Color;
+import 'dart:ui' show Color, ImageByteFormat;
import 'package:collection/collection.dart'
show PriorityQueue, HeapPriorityQueue;
import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
+/// A description of an encoded image.
+///
+/// Used in [PaletteGenerator.fromEncodedImage].
+class EncodedImage {
+ /// Creates a description of an encoded image.
+ const EncodedImage(
+ this.byteData, {
+ required this.width,
+ required this.height,
+ });
+
+ /// Encoded image byte data.
+ final ByteData byteData;
+
+ /// Image width.
+ final int width;
+
+ /// Image height.
+ final int height;
+}
+
/// A class to extract prominent colors from an image for use as user interface
/// colors.
///
@@ -77,6 +98,72 @@
_selectSwatches();
}
+ // TODO(gspencergoog): remove `dart:ui` paragragh from [fromByteData] method when https://github.com/flutter/flutter/issues/10647 is resolved
+
+ /// Create a [PaletteGenerator] asynchronously from encoded image [ByteData],
+ /// width, and height. These parameters are packed in [EncodedImage].
+ ///
+ /// The image encoding must be RGBA with 8-bit per channel, this corresponds to
+ /// [ImageByteFormat.rawRgba] or [ImageByteFormat.rawStraightRgba].
+ ///
+ /// In contast with [fromImage] and [fromImageProvider] this method can be used
+ /// in non-root isolates, because it doesn't involve interaction with the
+ /// `dart:ui` library, which is currently not supported, see https://github.com/flutter/flutter/issues/10647.
+ ///
+ /// The [region] specifies the part of the image to inspect for color
+ /// candidates. By default it uses the entire image. Must not be equal to
+ /// [Rect.zero], and must not be larger than the image dimensions.
+ ///
+ /// The [maximumColorCount] sets the maximum number of colors that will be
+ /// returned in the [PaletteGenerator]. The default is 16 colors.
+ ///
+ /// The [filters] specify a lost of [PaletteFilter] instances that can be used
+ /// to include certain colors in the list of colors. The default filter is
+ /// an instance of [AvoidRedBlackWhitePaletteFilter], which stays away from
+ /// whites, blacks, and low-saturation reds.
+ ///
+ /// The [targets] are a list of target color types, specified by creating
+ /// custom [PaletteTarget]s. By default, this is the list of targets in
+ /// [PaletteTarget.baseTargets].
+ static Future<PaletteGenerator> fromByteData(
+ EncodedImage encodedImage, {
+ Rect? region,
+ int maximumColorCount = _defaultCalculateNumberColors,
+ List<PaletteFilter> filters = const <PaletteFilter>[
+ avoidRedBlackWhitePaletteFilter
+ ],
+ List<PaletteTarget> targets = const <PaletteTarget>[],
+ }) async {
+ assert(region == null || region != Rect.zero);
+ assert(
+ region == null ||
+ (region.topLeft.dx >= 0.0 && region.topLeft.dy >= 0.0),
+ 'Region $region is outside the image ${encodedImage.width}x${encodedImage.height}');
+ assert(
+ region == null ||
+ (region.bottomRight.dx <= encodedImage.width &&
+ region.bottomRight.dy <= encodedImage.height),
+ 'Region $region is outside the image ${encodedImage.width}x${encodedImage.height}');
+ assert(
+ encodedImage.byteData.lengthInBytes ~/ 4 ==
+ encodedImage.width * encodedImage.height,
+ "Image byte data doesn't match the image size, or has invalid encoding. "
+ 'The encoding must be RGBA with 8 bit per channel.',
+ );
+
+ final _ColorCutQuantizer quantizer = _ColorCutQuantizer(
+ encodedImage,
+ maxColors: maximumColorCount,
+ filters: filters,
+ region: region,
+ );
+ final List<PaletteColor> colors = await quantizer.quantizedColors;
+ return PaletteGenerator.fromColors(
+ colors,
+ targets: targets,
+ );
+ }
+
/// Create a [PaletteGenerator] from an [dart:ui.Image] asynchronously.
///
/// The [region] specifies the part of the image to inspect for color
@@ -103,26 +190,21 @@
],
List<PaletteTarget> targets = const <PaletteTarget>[],
}) async {
- assert(region == null || region != Rect.zero);
- assert(
- region == null ||
- (region.topLeft.dx >= 0.0 && region.topLeft.dy >= 0.0),
- 'Region $region is outside the image ${image.width}x${image.height}');
- assert(
- region == null ||
- (region.bottomRight.dx <= image.width &&
- region.bottomRight.dy <= image.height),
- 'Region $region is outside the image ${image.width}x${image.height}');
+ final ByteData? imageData =
+ await image.toByteData(format: ui.ImageByteFormat.rawRgba);
+ if (imageData == null) {
+ throw 'Failed to encode the image.';
+ }
- final _ColorCutQuantizer quantizer = _ColorCutQuantizer(
- image,
- maxColors: maximumColorCount,
- filters: filters,
+ return PaletteGenerator.fromByteData(
+ EncodedImage(
+ imageData,
+ width: image.width,
+ height: image.height,
+ ),
region: region,
- );
- final List<PaletteColor> colors = await quantizer.quantizedColors;
- return PaletteGenerator.fromColors(
- colors,
+ maximumColorCount: maximumColorCount,
+ filters: filters,
targets: targets,
);
}
@@ -1082,28 +1164,26 @@
class _ColorCutQuantizer {
_ColorCutQuantizer(
- this.image, {
+ this.encodedImage, {
this.maxColors = PaletteGenerator._defaultCalculateNumberColors,
this.region,
this.filters = const <PaletteFilter>[avoidRedBlackWhitePaletteFilter],
- }) : assert(region == null || region != Rect.zero),
- _paletteColors = <PaletteColor>[];
+ }) : assert(region == null || region != Rect.zero);
- FutureOr<List<PaletteColor>> get quantizedColors async {
- if (_paletteColors.isNotEmpty) {
- return _paletteColors;
- } else {
- return _quantizeColors(image);
- }
- }
-
- final ui.Image image;
- final List<PaletteColor> _paletteColors;
-
+ final EncodedImage encodedImage;
final int maxColors;
final Rect? region;
final List<PaletteFilter> filters;
+ Completer<List<PaletteColor>>? _paletteColorsCompleter;
+ FutureOr<List<PaletteColor>> get quantizedColors async {
+ if (_paletteColorsCompleter == null) {
+ _paletteColorsCompleter = Completer<List<PaletteColor>>();
+ _paletteColorsCompleter!.complete(_quantizeColors());
+ }
+ return _paletteColorsCompleter!.future;
+ }
+
Iterable<Color> _getImagePixels(ByteData pixels, int width, int height,
{Rect? region}) sync* {
final int rowStride = width * 4;
@@ -1152,7 +1232,7 @@
return false;
}
- Future<List<PaletteColor>> _quantizeColors(ui.Image image) async {
+ List<PaletteColor> _quantizeColors() {
const int quantizeWordWidth = 5;
const int quantizeChannelWidth = 8;
const int quantizeShift = quantizeChannelWidth - quantizeWordWidth;
@@ -1168,13 +1248,13 @@
);
}
- final ByteData? imageData =
- await image.toByteData(format: ui.ImageByteFormat.rawRgba);
- if (imageData == null) {
- throw 'Failed to encode the image.';
- }
- final Iterable<Color> pixels =
- _getImagePixels(imageData, image.width, image.height, region: region);
+ final List<PaletteColor> paletteColors = <PaletteColor>[];
+ final Iterable<Color> pixels = _getImagePixels(
+ encodedImage.byteData,
+ encodedImage.width,
+ encodedImage.height,
+ region: region,
+ );
final _ColorHistogram hist = _ColorHistogram();
Color? currentColor;
_ColorCount? currentColorCount;
@@ -1205,16 +1285,16 @@
if (hist.length <= maxColors) {
// The image has fewer colors than the maximum requested, so just return
// the colors.
- _paletteColors.clear();
+ paletteColors.clear();
for (final Color color in hist.keys) {
- _paletteColors.add(PaletteColor(color, hist[color]!.value));
+ paletteColors.add(PaletteColor(color, hist[color]!.value));
}
} else {
// We need use quantization to reduce the number of colors
- _paletteColors.clear();
- _paletteColors.addAll(_quantizePixels(maxColors, hist));
+ paletteColors.clear();
+ paletteColors.addAll(_quantizePixels(maxColors, hist));
}
- return _paletteColors;
+ return paletteColors;
}
List<PaletteColor> _quantizePixels(
diff --git a/packages/palette_generator/pubspec.yaml b/packages/palette_generator/pubspec.yaml
index 6c18d10..5923cb9 100644
--- a/packages/palette_generator/pubspec.yaml
+++ b/packages/palette_generator/pubspec.yaml
@@ -2,7 +2,7 @@
description: Flutter package for generating palette colors from a source image.
repository: https://github.com/flutter/packages/tree/master/packages/palette_generator
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+palette_generator%22
-version: 0.3.0
+version: 0.3.1
environment:
sdk: ">=2.12.0 <3.0.0"
diff --git a/packages/palette_generator/test/palette_generator_test.dart b/packages/palette_generator/test/palette_generator_test.dart
index e2de939..024b6c3 100644
--- a/packages/palette_generator/test/palette_generator_test.dart
+++ b/packages/palette_generator/test/palette_generator_test.dart
@@ -39,7 +39,7 @@
}
}
-Future<ImageProvider> loadImage(String name) async {
+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));
@@ -59,7 +59,8 @@
'dominant',
'landscape'
];
- final Map<String, ImageProvider> testImages = <String, ImageProvider>{};
+ final Map<String, FakeImageProvider> testImages =
+ <String, FakeImageProvider>{};
for (final String name in imageNames) {
testImages[name] = await loadImage('$name.png');
}
@@ -71,7 +72,34 @@
tester.pumpWidget(const Placeholder());
});
- test('PaletteGenerator works on 1-pixel wide blue image', () async {
+ 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));
@@ -79,7 +107,7 @@
within<Color>(distance: 8, from: const Color(0xff0000ff)));
});
- test('PaletteGenerator works on 1-pixel high red image', () async {
+ 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));
@@ -190,7 +218,7 @@
expect(palette.paletteColors.length, equals(15));
});
- test('PaletteGenerator Filters work', () async {
+ 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>[
@@ -358,6 +386,26 @@
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)));
+ });
+}
+
+Future<PaletteGenerator> _computeFromByteData(EncodedImage encodedImage) async {
+ return PaletteGenerator.fromByteData(encodedImage);
}
bool onlyBluePaletteFilter(HSLColor hslColor) {