diff --git a/lib/ui/BUILD.gn b/lib/ui/BUILD.gn index 2862a9b68cf96..8b3be72d5973b 100644 --- a/lib/ui/BUILD.gn +++ b/lib/ui/BUILD.gn @@ -24,6 +24,8 @@ source_set("ui") { "painting/gradient.h", "painting/image.cc", "painting/image.h", + "painting/image_encoding.cc", + "painting/image_encoding.h", "painting/image_filter.cc", "painting/image_filter.h", "painting/image_shader.cc", diff --git a/lib/ui/painting.dart b/lib/ui/painting.dart index 4b041a27df3eb..4a874200f7eeb 100644 --- a/lib/ui/painting.dart +++ b/lib/ui/painting.dart @@ -1196,6 +1196,80 @@ class Paint { } } +/// An encoding format to use with the [Image.toByteData]. +class EncodingFormat { + /// 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: + /// + /// * , the Wikipedia page on PNG. + /// * , 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: + /// + /// * , 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: + /// + /// * , the Wikipedia page on WebP. + const EncodingFormat.webp({int quality = 80}) + : _format = _webpFormat, + _quality = quality; + + 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; +} + /// Opaque handle to raw decoded image data (pixels). /// /// To obtain an [Image] object, use [instantiateImageCodec]. @@ -1215,6 +1289,23 @@ 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 toByteData({EncodingFormat format: const EncodingFormat.jpeg()}) { + return _futurize((_Callback callback) { + return _toByteData(format._format, format._quality, (Uint8List encoded) { + callback(encoded.buffer.asByteData()); + }); + }); + } + + /// Returns an error message on failure, null on success. + String _toByteData(int format, int quality, _Callback 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'; diff --git a/lib/ui/painting/image.cc b/lib/ui/painting/image.cc index 0b0127cd0c6c2..42e733241c990 100644 --- a/lib/ui/painting/image.cc +++ b/lib/ui/painting/image.cc @@ -5,6 +5,7 @@ #include "flutter/lib/ui/painting/image.h" #include "flutter/common/threads.h" +#include "flutter/lib/ui/painting/image_encoding.h" #include "flutter/lib/ui/painting/utils.h" #include "lib/tonic/converter/dart_converter.h" #include "lib/tonic/dart_args.h" @@ -20,6 +21,7 @@ IMPLEMENT_WRAPPERTYPEINFO(ui, Image); #define FOR_EACH_BINDING(V) \ V(Image, width) \ V(Image, height) \ + V(Image, toByteData) \ V(Image, dispose) FOR_EACH_BINDING(DART_NATIVE_CALLBACK) @@ -36,6 +38,12 @@ CanvasImage::~CanvasImage() { SkiaUnrefOnIOThread(&image_); } +Dart_Handle CanvasImage::toByteData(int format, + int quality, + Dart_Handle callback) { + return EncodeImage(this, format, quality, callback); +} + void CanvasImage::dispose() { ClearDartWrapper(); } diff --git a/lib/ui/painting/image.h b/lib/ui/painting/image.h index 8ad11660721f0..a7ed4298506f5 100644 --- a/lib/ui/painting/image.h +++ b/lib/ui/painting/image.h @@ -27,6 +27,7 @@ class CanvasImage final : public fxl::RefCountedThreadSafe, int width() { return image_->width(); } int height() { return image_->height(); } + Dart_Handle toByteData(int format, int quality, Dart_Handle callback); void dispose(); const sk_sp& image() const { return image_; } diff --git a/lib/ui/painting/image_encoding.cc b/lib/ui/painting/image_encoding.cc new file mode 100644 index 0000000000000..f356e955f6745 --- /dev/null +++ b/lib/ui/painting/image_encoding.cc @@ -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 callback, + sk_sp 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::ToDart( + buffer->bytes(), buffer->size()); + DartInvoke(callback->value(), {dart_data}); + } +} + +sk_sp EncodeImage(sk_sp image, + SkEncodedImageFormat format, + int quality) { + if (image == nullptr) { + return nullptr; + } + return image->encodeToData(format, quality); +} + +void EncodeImageAndInvokeDataCallback( + std::unique_ptr callback, + sk_sp image, + SkEncodedImageFormat format, + int quality) { + sk_sp 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( + tonic::DartState::Current(), callback_handle); + sk_sp 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 diff --git a/lib/ui/painting/image_encoding.h b/lib/ui/painting/image_encoding.h new file mode 100644 index 0000000000000..a121d597ccefd --- /dev/null +++ b/lib/ui/painting/image_encoding.h @@ -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_ diff --git a/testing/dart/encoding_test.dart b/testing/dart/encoding_test.dart new file mode 100644 index 0000000000000..5fbbb4eb63909 --- /dev/null +++ b/testing/dart/encoding_test.dart @@ -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 { + ByteData data = await testImage.toByteData(); + List expected = readFile('square-80.jpg'); + expect(new Uint8List.view(data.buffer), expected); + }); + + test('Encode JPEG', () async { + ByteData data = await testImage.toByteData( + format: new EncodingFormat.jpeg(quality: 80)); + List expected = readFile('square-80.jpg'); + expect(new Uint8List.view(data.buffer), expected); + }); + + test('Encode PNG', () async { + ByteData data = + await testImage.toByteData(format: new EncodingFormat.png()); + List expected = readFile('square.png'); + expect(new Uint8List.view(data.buffer), expected); + }); + + test('Encode WEBP', () async { + ByteData data = await testImage.toByteData( + format: new EncodingFormat.webp(quality: 80)); + List expected = readFile('square-80.webp'); + expect(new Uint8List.view(data.buffer), 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); +} + +List readFile(fileName) { + final file = new File(path.join('flutter', 'testing', 'resources', fileName)); + return file.readAsBytesSync(); +} diff --git a/testing/resources/square-80.jpg b/testing/resources/square-80.jpg new file mode 100644 index 0000000000000..1140c33bd3903 Binary files /dev/null and b/testing/resources/square-80.jpg differ diff --git a/testing/resources/square-80.webp b/testing/resources/square-80.webp new file mode 100644 index 0000000000000..fe3924cad729b Binary files /dev/null and b/testing/resources/square-80.webp differ diff --git a/testing/resources/square.png b/testing/resources/square.png new file mode 100644 index 0000000000000..a042cece55163 Binary files /dev/null and b/testing/resources/square.png differ diff --git a/travis/licenses_golden/licenses_flutter b/travis/licenses_golden/licenses_flutter index 7de50c115fd76..8749f6880dbf4 100644 --- a/travis/licenses_golden/licenses_flutter +++ b/travis/licenses_golden/licenses_flutter @@ -1642,6 +1642,8 @@ ORIGIN: ../../../flutter/assets/asset_provider.h + ../../../LICENSE TYPE: LicenseType.bsd FILE: ../../../flutter/assets/asset_provider.h FILE: ../../../flutter/flutter_kernel_transformers/lib/track_widget_constructor_locations.dart +FILE: ../../../flutter/lib/ui/painting/image_encoding.cc +FILE: ../../../flutter/lib/ui/painting/image_encoding.h FILE: ../../../flutter/shell/platform/android/apk_asset_provider.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/accessibility_text_entry.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/accessibility_text_entry.mm