-
Notifications
You must be signed in to change notification settings - Fork 6k
Enable Image encoding by leveraging existing Skia functionality #4762
Changes from 9 commits
f7ce006
5472da9
a4e3b63
4a39f8d
447d8d5
2ecab34
7202b16
e8a8820
813e9ec
cd5f265
c89adb9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1196,6 +1196,80 @@ class Paint { | |
| } | ||
| } | ||
|
|
||
| /// An encoding format to use with the [Image.toByteData]. | ||
| class EncodingFormat { | ||
| final int _format; | ||
| final int _quality; | ||
|
|
||
| // Be conservative with the formats we expose. It is easy to add new formats | ||
| // in future but difficult to remove. | ||
| // These values must be kept in sync with the logic in ToSkEncodedImageFormat. | ||
| static const int _jpegFormat = 0; | ||
| static const int _pngFormat = 1; | ||
| static const int _webpFormat = 2; | ||
|
|
||
| /// PNG format. | ||
| /// | ||
| /// A loss-less compression format for images. This format is well suited for | ||
| /// images with hard edges, such as screenshots or sprites, and images with | ||
| /// text. Transparency is supported. The PNG format supports images up to | ||
| /// 2,147,483,647 pixels in either dimension, though in practice available | ||
| /// memory provides a more immediate limitation on maximum image size. | ||
| /// | ||
| /// PNG images normally use the `.png` file extension and the `image/png` MIME | ||
| /// type. | ||
| /// | ||
| /// See also: | ||
| /// | ||
| /// * <https://en.wikipedia.org/wiki/Portable_Network_Graphics>, the Wikipedia page on PNG. | ||
| /// * <https://tools.ietf.org/rfc/rfc2083.txt>, the PNG standard. | ||
| const EncodingFormat.png() | ||
| : _format = _pngFormat, | ||
| _quality = 0; | ||
|
|
||
| /// JPEG format. | ||
| /// | ||
| /// This format, strictly speaking called JFIF, is a lossy compression | ||
| /// graphics format that can handle images up to 65,535 pixels in either | ||
| /// dimension. The [quality] metric is a value in the range 0 to 100 that | ||
| /// controls the compression ratio. Values in the range of about 50 to 90 are | ||
| /// somewhat reasonable; values above 95 increase the file size with little | ||
| /// noticeable improvement to the quality, values below 50 drop the quality | ||
| /// substantially. | ||
| /// | ||
| /// This format is well suited for photographs. It is very poorly suited for | ||
| /// images with hard edges or text. It does not support transparency. | ||
| /// | ||
| /// JPEG images normally use the `.jpeg` file extension and the `image/jpeg` | ||
| /// MIME type. | ||
| /// | ||
| /// See also: | ||
| /// | ||
| /// * <https://en.wikipedia.org/wiki/JPEG>, the Wikipedia page on JPEG. | ||
| const EncodingFormat.jpeg({int quality = 80}) | ||
| : _format = _jpegFormat, | ||
| _quality = quality; | ||
|
|
||
| /// WebP format. | ||
| /// | ||
| /// The WebP format supports both lossy and lossless compression; however, the | ||
| /// [Image.toByteData] method always uses lossy compression when [webp] is | ||
| /// specified. The [quality] metric is a value in the range 0 to 100 that | ||
| /// controls the compression ratio; higher values result in better quality but | ||
| /// larger file sizes, and vice versa. WebP images are limited to 16,383 | ||
| /// pixels in each direction (width and height). | ||
| /// | ||
| /// WebP images normally use the `.webp` file extension and the `image/webp` | ||
| /// MIME type. | ||
| /// | ||
| /// See also: | ||
| /// | ||
| /// * <https://en.wikipedia.org/wiki/WebP>, the Wikipedia page on WebP. | ||
| const EncodingFormat.webp({int quality = 80}) | ||
| : _format = _webpFormat, | ||
| _quality = quality; | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the constructors should come first
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
|
|
||
| /// Opaque handle to raw decoded image data (pixels). | ||
| /// | ||
| /// To obtain an [Image] object, use [instantiateImageCodec]. | ||
|
|
@@ -1215,6 +1289,21 @@ class Image extends NativeFieldWrapperClass2 { | |
| /// The number of image pixels along the image's vertical axis. | ||
| int get height native 'Image_height'; | ||
|
|
||
| /// Converts the [Image] object into a byte array. | ||
| /// | ||
| /// The [format] is encoding format to be used. | ||
| /// | ||
| /// Returns a future which complete with the binary image data (e.g a PNG or JPEG binary data) or | ||
| /// an error if encoding fails. | ||
| Future<Uint8List> toByteData({EncodingFormat format: const EncodingFormat.jpeg()}) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As @Hixie said, let's make this return a Unfortunately, it doesn't look like here's a Future<ByteData> toByteData({EncodingFormat format: const EncodingFormat.jpeg()}) {
return _futurize((_Callback<ByteData> callback) {
return _toByteData(format._format, format._quality, (Uint8List uint8List) {
callback(uint8List.buffer.asByteData());
});
});
}
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ... then the test will have to be updated to match too |
||
| return _futurize( | ||
| (_Callback<Uint8List> callback) => _toByteData(format._format, format._quality, callback) | ||
| ); | ||
| } | ||
|
|
||
| /// Returns an error message on failure, null on success. | ||
| String _toByteData(int format, int quality, _Callback<Uint8List> callback) native 'Image_toByteData'; | ||
|
|
||
| /// Release the resources used by this object. The object is no longer usable | ||
| /// after this method is called. | ||
| void dispose() native 'Image_dispose'; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| // Copyright 2018 The Chromium Authors. All rights reserved. | ||
| // Use of this source code is governed by a BSD-style license that can be | ||
| // found in the LICENSE file. | ||
|
|
||
| #include "flutter/lib/ui/painting/image_encoding.h" | ||
|
|
||
| #include "flutter/common/threads.h" | ||
| #include "flutter/lib/ui/painting/image.h" | ||
| #include "flutter/lib/ui/painting/resource_context.h" | ||
| #include "lib/fxl/build_config.h" | ||
| #include "lib/fxl/functional/make_copyable.h" | ||
| #include "lib/tonic/dart_persistent_value.h" | ||
| #include "lib/tonic/dart_state.h" | ||
| #include "lib/tonic/logging/dart_invoke.h" | ||
| #include "lib/tonic/typed_data/uint8_list.h" | ||
| #include "third_party/skia/include/core/SkEncodedImageFormat.h" | ||
| #include "third_party/skia/include/core/SkImage.h" | ||
|
|
||
| using tonic::DartInvoke; | ||
| using tonic::DartPersistentValue; | ||
| using tonic::ToDart; | ||
|
|
||
| namespace blink { | ||
| namespace { | ||
|
|
||
| void InvokeDataCallback(std::unique_ptr<DartPersistentValue> callback, | ||
| sk_sp<SkData> buffer) { | ||
| tonic::DartState* dart_state = callback->dart_state().get(); | ||
| if (!dart_state) { | ||
| return; | ||
| } | ||
| tonic::DartState::Scope scope(dart_state); | ||
| if (!buffer) { | ||
| DartInvoke(callback->value(), {Dart_Null()}); | ||
| } else { | ||
| Dart_Handle dart_data = tonic::DartConverter<tonic::Uint8List>::ToDart( | ||
| buffer->bytes(), buffer->size()); | ||
| DartInvoke(callback->value(), {dart_data}); | ||
| } | ||
| } | ||
|
|
||
| sk_sp<SkData> EncodeImage(sk_sp<SkImage> image, | ||
| SkEncodedImageFormat format, | ||
| int quality) { | ||
| if (image == nullptr) { | ||
| return nullptr; | ||
| } | ||
| return image->encodeToData(format, quality); | ||
| } | ||
|
|
||
| void EncodeImageAndInvokeDataCallback( | ||
| std::unique_ptr<DartPersistentValue> callback, | ||
| sk_sp<SkImage> image, | ||
| SkEncodedImageFormat format, | ||
| int quality) { | ||
| sk_sp<SkData> encoded = EncodeImage(std::move(image), format, quality); | ||
|
|
||
| Threads::UI()->PostTask( | ||
| fxl::MakeCopyable([callback = std::move(callback), encoded]() mutable { | ||
| InvokeDataCallback(std::move(callback), std::move(encoded)); | ||
| })); | ||
| } | ||
|
|
||
| SkEncodedImageFormat ToSkEncodedImageFormat(int format) { | ||
| // Map the formats exposed in flutter to formats supported in Skia. | ||
| // See: | ||
| // https://github.com/google/skia/blob/master/include/core/SkEncodedImageFormat.h | ||
| switch (format) { | ||
| case 0: | ||
| return SkEncodedImageFormat::kJPEG; | ||
| case 1: | ||
| return SkEncodedImageFormat::kPNG; | ||
| case 2: | ||
| return SkEncodedImageFormat::kWEBP; | ||
| default: | ||
| /* NOTREACHED */ | ||
| return SkEncodedImageFormat::kWEBP; | ||
| } | ||
| } | ||
|
|
||
| } // namespace | ||
|
|
||
| Dart_Handle EncodeImage(CanvasImage* canvas_image, | ||
| int format, | ||
| int quality, | ||
| Dart_Handle callback_handle) { | ||
| if (!canvas_image) | ||
| return ToDart("encode called with non-genuine Image."); | ||
|
|
||
| if (!Dart_IsClosure(callback_handle)) | ||
| return ToDart("Callback must be a function."); | ||
|
|
||
| SkEncodedImageFormat image_format = ToSkEncodedImageFormat(format); | ||
|
|
||
| if (quality > 100) | ||
| quality = 100; | ||
| if (quality < 0) | ||
| quality = 0; | ||
|
|
||
| auto callback = std::make_unique<DartPersistentValue>( | ||
| tonic::DartState::Current(), callback_handle); | ||
| sk_sp<SkImage> image = canvas_image->image(); | ||
|
|
||
| Threads::IO()->PostTask(fxl::MakeCopyable( | ||
| [callback = std::move(callback), image, image_format, quality]() mutable { | ||
| EncodeImageAndInvokeDataCallback(std::move(callback), std::move(image), | ||
| image_format, quality); | ||
| })); | ||
|
|
||
| return Dart_Null(); | ||
| } | ||
|
|
||
| } // namespace blink |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| // Copyright 2018 The Chromium Authors. All rights reserved. | ||
| // Use of this source code is governed by a BSD-style license that can be | ||
| // found in the LICENSE file. | ||
|
|
||
| #ifndef FLUTTER_LIB_UI_PAINTING_IMAGE_ENCODING_H_ | ||
| #define FLUTTER_LIB_UI_PAINTING_IMAGE_ENCODING_H_ | ||
|
|
||
| #include "lib/tonic/dart_library_natives.h" | ||
|
|
||
| namespace blink { | ||
|
|
||
| class CanvasImage; | ||
|
|
||
| Dart_Handle EncodeImage(CanvasImage* canvas_image, | ||
| int format, | ||
| int quality, | ||
| Dart_Handle callback_handle); | ||
|
|
||
| } // namespace blink | ||
|
|
||
| #endif // FLUTTER_LIB_UI_PAINTING_IMAGE_ENCODING_H_ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| // Copyright 2018 The Chromium 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:async'; | ||
| import 'dart:ui'; | ||
| import 'dart:typed_data'; | ||
| import 'dart:io'; | ||
|
|
||
| import 'package:test/test.dart'; | ||
| import 'package:path/path.dart' as path; | ||
|
|
||
| void main() { | ||
| final Image testImage = createSquareTestImage(); | ||
|
|
||
| test('Encode with default arguments', () async { | ||
| Uint8List data = await testImage.toByteData(); | ||
| Uint8List expected = readFile('square-80.jpg'); | ||
| expect(data, expected); | ||
| }); | ||
|
|
||
| test('Encode JPEG', () async { | ||
| Uint8List data = await testImage.toByteData( | ||
| format: new EncodingFormat.jpeg(quality: 80)); | ||
| Uint8List expected = readFile('square-80.jpg'); | ||
| expect(data, expected); | ||
| }); | ||
|
|
||
| test('Encode PNG', () async { | ||
| Uint8List data = | ||
| await testImage.toByteData(format: new EncodingFormat.png()); | ||
| Uint8List expected = readFile('square.png'); | ||
| expect(data, expected); | ||
| }); | ||
|
|
||
| test('Encode WEBP', () async { | ||
| Uint8List data = await testImage.toByteData( | ||
| format: new EncodingFormat.webp(quality: 80)); | ||
| Uint8List expected = readFile('square-80.webp'); | ||
| expect(data, expected); | ||
| }); | ||
| } | ||
|
|
||
| Image createSquareTestImage() { | ||
| PictureRecorder recorder = new PictureRecorder(); | ||
| Canvas canvas = new Canvas(recorder, new Rect.fromLTWH(0.0, 0.0, 10.0, 10.0)); | ||
|
|
||
| Paint black = new Paint() | ||
| ..strokeWidth = 1.0 | ||
| ..color = const Color.fromRGBO(0, 0, 0, 1.0); | ||
| Paint green = new Paint() | ||
| ..strokeWidth = 1.0 | ||
| ..color = const Color.fromRGBO(0, 255, 0, 1.0); | ||
|
|
||
| canvas.drawRect(new Rect.fromLTWH(0.0, 0.0, 10.0, 10.0), black); | ||
| canvas.drawRect(new Rect.fromLTWH(2.0, 2.0, 6.0, 6.0), green); | ||
| return recorder.endRecording().toImage(10, 10); | ||
| } | ||
|
|
||
| Uint8List readFile(fileName) { | ||
| final file = new File(path.join('flutter', 'testing', 'resources', fileName)); | ||
| return file.readAsBytesSync(); | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These should be lowercase.
Here are some doc strings for each one: