Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/ui/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
29 changes: 29 additions & 0 deletions lib/ui/painting.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1196,6 +1196,16 @@ class Paint {
}
}

/// The encoding formats supported by the [Image.toByteData].
// These enum values must be kept in sync with the logic in ToSkEncodedImageFormat.
enum EncodeFormat {
// Be conservative with the formats we expose. It is easy to add new formats in future but
// difficult to remove.
JPEG,
PNG,
WEBP,
}

@Hixie Hixie Apr 1, 2018

Copy link
Copy Markdown
Contributor

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,

Copy link
Copy Markdown
Contributor

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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].
Expand All @@ -1215,6 +1225,25 @@ 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.
///
/// The [quality] is a value in the range 0 to 100. [quality] is a format specific metric trading
/// off size and encoding error. When used, 100 encodes with highest quality (i.e., least error)
/// and 0 encodes with lowest quality (i.e., smallest size).
///
/// 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}) {

Copy link
Copy Markdown
Contributor

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)
  );

Copy link
Copy Markdown
Contributor

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);

Copy link
Copy Markdown
Contributor Author

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Copy link
Copy Markdown
Contributor

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).

Copy link
Copy Markdown
Contributor

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.

Copy link
Copy Markdown
Contributor Author

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.

return _futurize(
(_Callback<Uint8List> callback) => _toByteData(format.index, 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';
Expand Down
8 changes: 8 additions & 0 deletions lib/ui/painting/image.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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)
Expand All @@ -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();
}
Expand Down
1 change: 1 addition & 0 deletions lib/ui/painting/image.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class CanvasImage final : public fxl::RefCountedThreadSafe<CanvasImage>,

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<SkImage>& image() const { return image_; }
Expand Down
113 changes: 113 additions & 0 deletions lib/ui/painting/image_encoding.cc
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
21 changes: 21 additions & 0 deletions lib/ui/painting/image_encoding.h
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_
61 changes: 61 additions & 0 deletions testing/dart/encoding_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// 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() {

Image createSquareTestImage() {

Copy link
Copy Markdown
Contributor

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

PictureRecorder recorder = new PictureRecorder();
Canvas canvas = new Canvas(recorder, new Rect.fromLTWH(0.0, 0.0, 10.0, 10.0));

var black = new Paint()

Copy link
Copy Markdown
Contributor

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

..strokeWidth = 1.0
..color = const Color.fromRGBO(0, 0, 0, 1.0);
var 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();
}

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: EncodeFormat.JPEG, quality:80);
Uint8List expected = readFile('square-80.jpg');
expect(data, expected);
});

test('Encode PNG', () async {
Uint8List data = await testImage.toByteData(format: EncodeFormat.PNG, quality: 80);
Uint8List expected = readFile('square-80.png');
expect(data, expected);
});

test('Encode WEBP', () async {
Uint8List data = await testImage.toByteData(format: EncodeFormat.WEBP, quality: 80);
Uint8List expected = readFile('square-80.webp');
expect(data, expected);
});
}
Binary file added testing/resources/square-80.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added testing/resources/square-80.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added testing/resources/square-80.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions travis/licenses_golden/licenses_flutter
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down