-
Notifications
You must be signed in to change notification settings - Fork 6k
Enable Image encoding by leveraging existing Skia functionality #4762
Conversation
|
I needed this functionality on my own project and thought this may be of general value since |
lib/ui/painting.dart
Outdated
| JPEG, | ||
| PNG, | ||
| WEBP, | ||
| } |
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 will need documentation, ideally including a pointer to the relevant specification for each one.
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.
Also for each one we should say what the quality argument does.
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 map to Skia values. Skia's does not document the supported format in much details. I was not able to even find link to relevant specification from them. So I have not added any spec links since I was not confident. If you have authoritative links I am happy to add.
Here is the Skia documentation on quality. It has no details on exact impact of quality for each format beyond "it is format specific and may be ignored".
My tests using the demo in the above link confirms that the quality actually works for this three
formats. So I have updated the wording to reflect this with more detail on low vs high.
lib/ui/painting.dart
Outdated
| } | ||
|
|
||
| /// Callback signature for [encodeImage]. | ||
| typedef void ImageEncoderCallback(Uint8List result); |
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.
Please document what the "result" argument is (presumably, bytes representing the encoded image in the requested format?).
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.
(this can just be removed if we go the route suggested below of returning a future)
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.
Removed. Using a Future instead.
lib/ui/painting.dart
Outdated
| /// Callback signature for [encodeImage]. | ||
| typedef void ImageEncoderCallback(Uint8List result); | ||
|
|
||
| /// Convert an [Image] object into a byte array/ |
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.
typo: / -> .
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.
Done.
lib/ui/painting.dart
Outdated
| /// [format] is encoding format to be used. | ||
| /// | ||
| /// [quality] is a value in [0, 100] where 0 corresponds to the lowest quality. | ||
| void encodeImage(Image image, EncodedImageFormat format, int quality, ImageEncoderCallback callback) |
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.
This should be a method on Image.
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.
We should probably make the format and quality arguments named optional arguments with defaults.
It's probably also a good idea to have this return a Future<ByteData> rather than using the callback approach. That would be more idiomatic. We can use the callback approach internally in this function in order to implement the Future, though, since dealing with Futures and the engine is non-trivial otherwise.
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.
Also, if we make this a method on Image, we should probably call it toByteData, to be more idiomatic.
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.
+1. FYI for context, see flutter/flutter#11648 (comment)
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.
Ack. I think it makes sense for this to be a method on Image.
Can you please provide my an example of how a Future returning method is implemented in
engine?
I presume Hixie's suggestion was to have a private method implemented natively that uses
callback and then call this private method from public dart method. Effectively creating
the future in dart and resolving that future via callback roughly:
class Image {
Future<Uint8List> toByteData(format, quality) {
final Completer<Uint8List> completer = new Completer<Uint8List>();
_encodeImage(this, format, quality, completer.complete); // this is natively implemented.
return completer.future;
}
}
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.
Yup, that's it exactly.
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.
You can just use the _futurize method in this file to do it for you though.
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.
Done.
Turned into an async method on Image. Format and quality are now names arguments with defaults (jpeg, 80). I choose jpeg since it is very common and 80 since it is what I think
mac os preview export defaults to. It is supposed to give a reasonable quality/size trade off. Happy to change if you have other ideas.
Most implementation is still kept in image_encoding.h.
lib/ui/painting.dart
Outdated
| /// | ||
| /// [format] is encoding format to be used. | ||
| /// | ||
| /// [quality] is a value in [0, 100] where 0 corresponds to the lowest quality. |
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.
Please start each paragraph with a capital letter, e.g. by saying The [format] argument is...
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.
Rather than in [0, 100] let's say something like in the range 0 to 100.
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.
Done. Improved the wording and provide more information of what lowest and highest quality mean.
|
cc @chinmaygarde for review, especially of the C++ side with which I am less familiar. Fixes flutter/flutter#11648. |
|
Thanks for your contribution! This is a super-popular request, so you will definitely receive the gratitude of many for implementing this! |
|
It would be great if you could include a test of this (just verify that some trivial Image, created with a Canvas, returns the expected bytes). Other relevant tests are in https://github.com/flutter/engine/blob/master/testing/dart/ . |
|
The Travis failure is some minor thing with our license script -- you just need to edit the file as suggested in the Travis output to add your FILE lines. |
lib/ui/painting/image_encoding.cc
Outdated
| tonic::DartConverter<CanvasImage*>::FromArguments(args, 0, exception); | ||
|
|
||
| if (exception) { | ||
| Dart_ThrowException(exception); |
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.
We are trying to reduce/remove our use of Dart_ThrowException because it does not return and hence will not call destructors of C++ objects on the stack (not that this is an issue in your patch). Can you make this method return an error flag and message that is sent back to the Dart method? On error, you can then throw a Dart exception from that wrapper to get similar behavior.
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.
Done. This is no longer uses Dart_ThrowException instead relies on return values.
The error value is handled via existing _futurize method in the dart side and turned into
and exception.
|
<3 This will make a lot of people happy. Thank you! |
|
Good work @majido |
|
@majido just checking in to see if there's anything you need to address the review comments. |
|
@tvolkert Thanks for checking in. I think all comments are clear and make sense. I just asked some clarification on how to return a future from native method. I just got around to implement a simple test per suggestion before embarking on the necessary refactors. I plan to make more progress this weekend. |
- Add tests - Move to make encode a method of Image - futurize - Avoid using Dart_ThrowException instead use return value - Improve formatting and comments
|
PR is now updated. I believe all issues should have been addressed. I also added a simple test which compares a generated canvas image against its encoded |
This patch allows one to convert any canvas into an encoded image. My usage is mainly based on directly drawing into a canvas. Something similar to this example. So if there is a way to paint the widget tree into a canvas, then perhaps yes. I haven't look at the internals of flutter rendering system, but each render object seems to have |
| JPEG, | ||
| PNG, | ||
| WEBP, | ||
| } |
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:
/// 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 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.
jpeg,
/// 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 `quality` metric is ignored for this format. 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.
png,
/// 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 determines 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.
webp,
testing/dart/encoding_test.dart
Outdated
| PictureRecorder recorder = new PictureRecorder(); | ||
| Canvas canvas = new Canvas(recorder, new Rect.fromLTWH(0.0, 0.0, 10.0, 10.0)); | ||
|
|
||
| var black = new Paint() |
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.
rather than using var, please specify explicit types
testing/dart/encoding_test.dart
Outdated
|
|
||
| void main() { | ||
|
|
||
| Image createSquareTestImage() { |
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.
for clarity, please move this and readFile out of main
It looks like this is the way to do so, though @rmacnak-google would know if there's a better way. new ByteData.view(uint8List.buffer); |
lib/ui/painting.dart
Outdated
| /// | ||
| /// 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({EncodeFormat format: EncodeFormat.JPEG, int quality: 80}) { |
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.
It feels a little weird to pass quality when it doesn't always make sense depending on the format. What if the API were as follows:
/// The encoding formats supported by the [Image.toByteData].
class EncodingFormat {
const EncodingFormat._(this._format, [this._quality = 0]);
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 `quality` metric is ignored for this format. 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.
static const EncodingFormat png = const EncodingFormat._(_pngFormat);
/// 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 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.
factory EncodingFormat.jpeg([int quality = 80]) => new EncodingFormat._(_jpegFormat, 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 determines 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.
factory EncodingFormat.webp([int quality = 80]) => new EncodingFormat._(_webpFormat, quality);
}
Future<Uint8List> toByteData({EncodingFormat format: EncodingFormat.png}) {
return _futurize(
(_Callback<Uint8List> callback) => _toByteData(format._format, format._quality, callback)
);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.
We could even then provide some suggested values...
static const EncodingFormat jpegHigh = const EncodingFormat._(_jpegFormat, 95);
static const EncodingFormat jpegMedium = const EncodingFormat._(_jpegFormat, 80);
static const EncodingFormat jpegLow = const EncodingFormat._(_jpegFormat, 60);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.
I like this quite a bit more. It provides a much nicer API and enables to have different default quality values for different formats.
nit: I will probably default the quality to be '100' when unspecified. It is safer if Skia ever decides to respect quality for a format in future.
nit: If we decide to provide additional (Low,Medium, High) suggested values we probably should
do it consistently for all formats where quality is supported.
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.
👍
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.
I like this proposal, though please use named arguments not optional arguments for the quality (easier to extend, easier to understand -- a number alone could be any number of things).
We may want to consider making the PNG case a constructor too, so that we can add arguments later.
We don't have to use factory constructors, we could just use real constructors. Then we don't have to have the private constructor.
I think if we're going to have a fallback default, 0 is safer, because it's more likely to cause noticeable problems (like crashes, divide by zero, horrible output, etc), whereas 100 is more likely to cause very subtle problems (like very large file sizes).
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.
I probably wouldn't bother with the low/medium/high defaults. I'm not sure what problem they really solve.
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.
Done. Used a class with three regular constructors, one for each supported format.
tvolkert
left a comment
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.
This is looking great. Just one comment from me, then I think we're good to go.
lib/ui/painting.dart
Outdated
| /// | ||
| /// 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()}) { |
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.
As @Hixie said, let's make this return a Future<ByteData>.
Unfortunately, it doesn't look like here's a tonic::DartConverter<tonic::DartByteData>::ToDart(), so we can just do this in Dart here:
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());
});
});
}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.
... then the test will have to be updated to match too
tvolkert
left a comment
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.
LGTM.
Waiting for an LGTM from @Hixie before merging.
| const EncodingFormat.webp({int quality = 80}) | ||
| : _format = _webpFormat, | ||
| _quality = quality; | ||
| } |
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.
the constructors should come first
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.
Done.
lib/ui/painting.dart
Outdated
| /// an error if encoding fails. | ||
| Future<ByteData> toByteData({EncodingFormat format: const EncodingFormat.jpeg()}) { | ||
| return _futurize((_Callback<ByteData> callback) { | ||
| _toByteData(format._format, format._quality, (Uint8List encoded) { |
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.
we're discarding the string returned by toByteData here, which seems bad
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.
Good catch. We just need to return _toByteData(... here
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.
Done.
flutter/flutter#16274 is the next piece of the puzzle required to do what you ask. |
lib/ui/painting.dart
Outdated
| Future<ByteData> toByteData({EncodingFormat format: const EncodingFormat.jpeg()}) { | ||
| return _futurize((_Callback<ByteData> callback) { | ||
| _toByteData(format._format, format._quality, (Uint8List encoded) { | ||
| callback(new ByteData.view(encoded.buffer)); |
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.
Nit: encoded.buffer.asByteData() is equivalent and slightly more concise.
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.
Done.
tvolkert
left a comment
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.
LGTM
|
Can we land this? The underlying API for accessing the task runners is going to change significantly in #4932 and I want to fix the build after I rebase the contents of this patch. Thanks. |
|
@chinmaygarde, done! @majido, thanks for the contribution! |
| /// | ||
| /// 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<ByteData> toByteData({EncodingFormat format: const EncodingFormat.jpeg()}) { |
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.
https://github.com/google/skia/blob/master/src/image/SkImage.cpp#L117
Skia seem to be using lossless PNG format as the default when encoding format is not specified.
Here, are we using JPEG as the default ?
Or should we pass null and use Skia's default ?
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.
Pass const EncodingFormat.png() as the format to get lossless PNG encoding.
|
It looks like this increased our release binary size by ~88K. Is that expected? |
|
Which binary size increased? I tried doing android_release builds of libflutter.so before and after this commit and saw a 4KB difference |
|
Hmm, this only seems to substantially affect iOS release binaries. |
|
Looks like the binary size increase is due to a newly introduced dependency on image encoding. The reason it only surfaced on iOS and not Android is that we only do LTO on iOS. As a result, Android's binary already had the image encoding support linked in before this change and was thus already inflated. @majido I think the right answer is to make This change first landed in |
|
Filed flutter/flutter#16537 to fix this. |
|
Do you think that video encinh is possible also at this level , rather than having to write a plug-in ? |
|
@tvolkert thanks ! |
Enable Image encoding by leveraging existing Skia functionality.
This patch introduces the following:
At the moment, it only exposes JPG, PNG, WEBP formats since it is easier to add more later
when there is a need.
Given this functionality it becomes easy to use the provided Canvas API in a flutter app to
draw and then export the result to an encoded image for sharing/download etc.
Fixes flutter/flutter#11648