From 759c9803941d6b2f914e594d2d4f181b9403bc8a Mon Sep 17 00:00:00 2001 From: Todd Volkert Date: Sun, 15 Jan 2023 20:56:48 -0800 Subject: [PATCH 1/9] Add more flexible image loading API This adds a new `instantiateImageCodecWithSize` method, which can be used to decode an image into a size, where the target size isn't known until the caller is allowed to inspect the image descriptor. This enables the use case of loading an image whose aspect ratio isn't known at load time, and which needs to be resized to fit within a bounding box while also maintaining its original aspect ratio. https://github.com/flutter/flutter/issues/118543 --- lib/ui/painting.dart | 128 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 115 insertions(+), 13 deletions(-) diff --git a/lib/ui/painting.dart b/lib/ui/painting.dart index 52ce47ea731a1..855a3962aaa85 100644 --- a/lib/ui/painting.dart +++ b/lib/ui/painting.dart @@ -2105,7 +2105,7 @@ Future instantiateImageCodec( bool allowUpscaling = true, }) async { final ImmutableBuffer buffer = await ImmutableBuffer.fromUint8List(list); - return instantiateImageCodecFromBuffer( + return await instantiateImageCodecFromBuffer( buffer, targetWidth: targetWidth, targetHeight: targetHeight, @@ -2124,6 +2124,10 @@ Future instantiateImageCodec( /// The data can be for either static or animated images. The following image /// formats are supported: {@macro dart.ui.imageFormats} /// +/// The [buffer] will be disposed by this method once the codec has been created, +/// so the caller must relinquish ownership of the [buffer] when they call this +/// method. +/// /// The [targetWidth] and [targetHeight] arguments specify the size of the /// output image, in image pixels. If they are not equal to the intrinsic /// dimensions of the image, then the image will be scaled after being decoded. @@ -2146,21 +2150,119 @@ Future instantiateImageCodecFromBuffer( int? targetWidth, int? targetHeight, bool allowUpscaling = true, +}) { + return instantiateImageCodecWithSize( + buffer, + getTargetSize: (ImageDescriptor descriptor) { + if (!allowUpscaling) { + if (targetWidth != null && targetWidth! > descriptor.width) { + targetWidth = descriptor.width; + } + if (targetHeight != null && targetHeight! > descriptor.height) { + targetHeight = descriptor.height; + } + } + return TargetImageSize(width: targetWidth, height: targetHeight); + }, + ); +} + +/// Instantiates an image [Codec]. +/// +/// This method is a convenience wrapper around the [ImageDescriptor] API. +/// +/// The [buffer] parameter is the binary image data (e.g a PNG or GIF binary data). +/// The data can be for either static or animated images. The following image +/// formats are supported: {@macro dart.ui.imageFormats} +/// +/// The [buffer] will be disposed by this method once the codec has been created, +/// so the caller must relinquish ownership of the [buffer] when they call this +/// method. +/// +/// The [getTargetSize] parameter, when specified, will be invoked and passed +/// the image's [ImageDescriptor] to determine the size to decode the image to. +/// The width and the height of the size it returns must be positive values +/// greater than or equal to 1, or null. It is valid to return a [TargetImageSize] +/// that specifies only one of `width` and `height` with the other remaining +/// null, in which case the omitted dimension will be scaled to maintain the +/// aspect ratio of the original dimensions. When both are null or omitted, +/// the image will be decoded at its native resolution (as will be the case if +/// the [getTargetSize] parameter is omitted). +/// +/// Scaling the image to larger than its intrinsic size should usually be +/// avoided, since it causes the image to use more memory than necessary. +/// Instead, prefer scaling the [Canvas] transform. +/// +/// The returned future can complete with an error if the image decoding has +/// failed. +Future instantiateImageCodecWithSize( + ImmutableBuffer buffer, { + TargetImageSizeProducer? getTargetSize, }) async { + getTargetSize ??= (ImageDescriptor descriptor) => const TargetImageSize(); final ImageDescriptor descriptor = await ImageDescriptor.encoded(buffer); - if (!allowUpscaling) { - if (targetWidth != null && targetWidth > descriptor.width) { - targetWidth = descriptor.width; - } - if (targetHeight != null && targetHeight > descriptor.height) { - targetHeight = descriptor.height; - } + try { + final TargetImageSize targetSize = getTargetSize(descriptor); + assert(targetSize.width == null || targetSize.width! > 0); + assert(targetSize.height == null || targetSize.height! > 0); + return descriptor.instantiateCodec( + targetWidth: targetSize.width, + targetHeight: targetSize.height, + ); + } finally { + buffer.dispose(); } - buffer.dispose(); - return descriptor.instantiateCodec( - targetWidth: targetWidth, - targetHeight: targetHeight, - ); +} + +/// Signature for a callback that determines the size to which an image should +/// be decoded given its [ImageDescriptor]. +/// +/// See also: +/// +/// * [instantiateImageCodecWithSize], which used this signature for its +/// `getTargetSize` argument. +typedef TargetImageSizeProducer = TargetImageSize Function(ImageDescriptor descriptor); + +/// A specification of the size to which an image should be decoded. +/// +/// See also: +/// +/// * [TargetImageSizeProducer], a callback that returns instances of this +/// class when consulted by image decoding methods such as +/// [instantiateImageCodecWithSize]. +class TargetImageSize { + /// Creates a new instance of this class. + /// + /// The `width` and `height` may both be null, but if they're non-null, they + /// must be positive. + const TargetImageSize({this.width, this.height}) + : assert(width == null || width > 0), + assert(width == null || width > 0); + + /// The width into which to load the image. + /// + /// If this is non-null, the image will be decoded into the specified width. + /// If this is null and [height] is also null, the image will be decoded into + /// its intrinsic size. If this is null and [height] is non-null, the image + /// will be decoded into a width that maintains its intrinsic aspect ratio + /// while respecting the [height] value. + /// + /// If this value is non-null, it must be positive. + final int? width; + + /// The height into which to load the image. + /// + /// If this is non-null, the image will be decoded into the specified height. + /// If this is null and [width] is also null, the image will be decoded into + /// its intrinsic size. If this is null and [width] is non-null, the image + /// will be decoded into a height that maintains its intrinsic aspect ratio + /// while respecting the [width] value. + /// + /// If this value is non-null, it must be positive. + final int? height; + + @override + String toString() => 'TargetImageSize($width x $height)'; } /// Loads a single image frame from a byte array into an [Image] object. From b095f72704018b1ef7ec012cd039020dda65f136 Mon Sep 17 00:00:00 2001 From: Todd Volkert Date: Sun, 15 Jan 2023 21:31:16 -0800 Subject: [PATCH 2/9] Add test --- testing/dart/codec_test.dart | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/testing/dart/codec_test.dart b/testing/dart/codec_test.dart index 3ac053abbacab..b79a320194539 100644 --- a/testing/dart/codec_test.dart +++ b/testing/dart/codec_test.dart @@ -86,6 +86,33 @@ void main() { ])); }); + test('with size', () async { + final Uint8List data = await _getSkiaResource('baby_tux.png').readAsBytes(); + final ui.ImmutableBuffer buffer = await ui.ImmutableBuffer.fromUint8List(data); + final ui.Codec codec = await ui.instantiateImageCodecWithSize( + buffer, + getTargetSize: (ui.ImageDescriptor descriptor) { + return ui.TargetImageSize( + width: descriptor.width ~/ 2, + height: descriptor.height ~/ 2, + ); + }, + ); + final List> decodedFrameInfos = >[]; + for (int i = 0; i < 2; i++) { + final ui.FrameInfo frameInfo = await codec.getNextFrame(); + decodedFrameInfos.add([ + frameInfo.duration.inMilliseconds, + frameInfo.image.width, + frameInfo.image.height, + ]); + } + expect(decodedFrameInfos, equals(>[ + [0, 120, 123], + [0, 120, 123], + ])); + }); + test('disposed decoded image', () async { final Uint8List data = await _getSkiaResource('flutter_logo.jpg').readAsBytes(); final ui.Codec codec = await ui.instantiateImageCodec(data); From e6371f37d9e412e1ce20fae6d3974882a46ce473 Mon Sep 17 00:00:00 2001 From: Todd Volkert Date: Sun, 15 Jan 2023 22:38:46 -0800 Subject: [PATCH 3/9] Fixed test failure --- lib/ui/painting.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ui/painting.dart b/lib/ui/painting.dart index 855a3962aaa85..5dd0b1110a5d9 100644 --- a/lib/ui/painting.dart +++ b/lib/ui/painting.dart @@ -2105,7 +2105,7 @@ Future instantiateImageCodec( bool allowUpscaling = true, }) async { final ImmutableBuffer buffer = await ImmutableBuffer.fromUint8List(list); - return await instantiateImageCodecFromBuffer( + return instantiateImageCodecFromBuffer( buffer, targetWidth: targetWidth, targetHeight: targetHeight, From ca4dc78a93b468d024d0e7466644dfb15eb9c319 Mon Sep 17 00:00:00 2001 From: Todd Volkert Date: Wed, 18 Jan 2023 09:23:27 -0800 Subject: [PATCH 4/9] Update * Respond to review comments --- lib/ui/painting.dart | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/ui/painting.dart b/lib/ui/painting.dart index 5dd0b1110a5d9..8491567d202cd 100644 --- a/lib/ui/painting.dart +++ b/lib/ui/painting.dart @@ -2197,9 +2197,9 @@ Future instantiateImageCodecFromBuffer( /// failed. Future instantiateImageCodecWithSize( ImmutableBuffer buffer, { - TargetImageSizeProducer? getTargetSize, + TargetImageSizeCallback? getTargetSize, }) async { - getTargetSize ??= (ImageDescriptor descriptor) => const TargetImageSize(); + getTargetSize ??= _getDefaultImageSize; final ImageDescriptor descriptor = await ImageDescriptor.encoded(buffer); try { final TargetImageSize targetSize = getTargetSize(descriptor); @@ -2214,6 +2214,8 @@ Future instantiateImageCodecWithSize( } } +TargetImageSize _getDefaultImageSize(ImageDescriptor descriptor) => const TargetImageSize(); + /// Signature for a callback that determines the size to which an image should /// be decoded given its [ImageDescriptor]. /// @@ -2221,13 +2223,13 @@ Future instantiateImageCodecWithSize( /// /// * [instantiateImageCodecWithSize], which used this signature for its /// `getTargetSize` argument. -typedef TargetImageSizeProducer = TargetImageSize Function(ImageDescriptor descriptor); +typedef TargetImageSizeCallback = TargetImageSize Function(ImageDescriptor descriptor); /// A specification of the size to which an image should be decoded. /// /// See also: /// -/// * [TargetImageSizeProducer], a callback that returns instances of this +/// * [TargetImageSizeCallback], a callback that returns instances of this /// class when consulted by image decoding methods such as /// [instantiateImageCodecWithSize]. class TargetImageSize { From 1a026b3e0a2a5dc5a3af528661c378c332a957ff Mon Sep 17 00:00:00 2001 From: Todd Volkert Date: Wed, 18 Jan 2023 22:14:06 -0800 Subject: [PATCH 5/9] Add web implementation --- lib/web_ui/lib/painting.dart | 44 ++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/lib/web_ui/lib/painting.dart b/lib/web_ui/lib/painting.dart index 0d602c10fc3b4..1c26a32ac453f 100644 --- a/lib/web_ui/lib/painting.dart +++ b/lib/web_ui/lib/painting.dart @@ -491,6 +491,50 @@ Future instantiateImageCodecFromBuffer( targetHeight: targetHeight, allowUpscaling: allowUpscaling); +class _SizeOnlyImageDescriptor implements ImageDescriptor { + const _SizeOnlyImageDescriptor(this.width, this.height); + + @override + final int width; + + @override + final int height; + + @override + dynamic noSuchMethod(Invocation invocation) => throw UnsupportedError('ImageDescriptor.${invocation.method} is not supported on web within a TargetImageSizeCallback.'); +} + +Future instantiateImageCodecWithSize( + ImmutableBuffer buffer, { + TargetImageSizeCallback? getTargetSize, +}) async { + if (getTargetSize == null) { + return engine.renderer.instantiateImageCodec(buffer._list!); + } else { + final Codec codec = await engine.renderer.instantiateImageCodec(buffer._list!); + final FrameInfo info = await codec.getNextFrame(); + final int width = info.image.width; + final int height = info.image.height; + info.image.dispose(); + codec.dispose(); + final ImageDescriptor descriptor = _SizeOnlyImageDescriptor(width, height); + final TargetImageSize targetSize = getTargetSize(descriptor); + return engine.renderer.instantiateImageCodec(buffer._list!, + targetWidth: targetSize.width, targetHeight: targetSize.height, allowUpscaling: false); + } +} + +typedef TargetImageSizeCallback = TargetImageSize Function(ImageDescriptor descriptor); + +class TargetImageSize { + const TargetImageSize({this.width, this.height}) + : assert(width == null || width > 0), + assert(width == null || width > 0); + + final int? width; + final int? height; +} + Future webOnlyInstantiateImageCodecFromUrl(Uri uri, {engine.WebOnlyImageCodecChunkCallback? chunkCallback}) => engine.renderer.instantiateImageCodecFromUrl(uri, chunkCallback: chunkCallback); From 5c7d8302c5fef3ff0a1c0b83d49d635cb0a0c8dc Mon Sep 17 00:00:00 2001 From: Todd Volkert Date: Wed, 18 Jan 2023 22:39:21 -0800 Subject: [PATCH 6/9] Fixed typo --- lib/web_ui/lib/painting.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web_ui/lib/painting.dart b/lib/web_ui/lib/painting.dart index 1c26a32ac453f..71332b39f37ae 100644 --- a/lib/web_ui/lib/painting.dart +++ b/lib/web_ui/lib/painting.dart @@ -501,7 +501,7 @@ class _SizeOnlyImageDescriptor implements ImageDescriptor { final int height; @override - dynamic noSuchMethod(Invocation invocation) => throw UnsupportedError('ImageDescriptor.${invocation.method} is not supported on web within a TargetImageSizeCallback.'); + dynamic noSuchMethod(Invocation invocation) => throw UnsupportedError('ImageDescriptor.${invocation.memberName} is not supported on web within a TargetImageSizeCallback.'); } Future instantiateImageCodecWithSize( From cb7cf84591a60efd38846f0e0abbdc0813df43be Mon Sep 17 00:00:00 2001 From: Todd Volkert Date: Thu, 19 Jan 2023 15:37:21 -0800 Subject: [PATCH 7/9] Review comments Also changed the TargetImageSizeCallback to just take intrinsic width & height, rather than the full image descriptor. --- lib/ui/painting.dart | 65 +++++++++++++++++++++++------------- lib/web_ui/lib/painting.dart | 27 +++++++++------ 2 files changed, 58 insertions(+), 34 deletions(-) diff --git a/lib/ui/painting.dart b/lib/ui/painting.dart index 8491567d202cd..85ac21108f97d 100644 --- a/lib/ui/painting.dart +++ b/lib/ui/painting.dart @@ -2145,6 +2145,13 @@ Future instantiateImageCodec( /// /// The returned future can complete with an error if the image decoding has /// failed. +/// +/// ## Compatibility note on the web +/// +/// When running Flutter on the web, only the CanvasKit renderer supports image +/// resizing capabilities (not the HTML renderer). So if image resizing is +/// critical to your use case, and you're deploying to the web, you should +/// build using the CanvasKit renderer. Future instantiateImageCodecFromBuffer( ImmutableBuffer buffer, { int? targetWidth, @@ -2153,13 +2160,13 @@ Future instantiateImageCodecFromBuffer( }) { return instantiateImageCodecWithSize( buffer, - getTargetSize: (ImageDescriptor descriptor) { + getTargetSize: (int intrinsicWidth, int intrinsicHeight) { if (!allowUpscaling) { - if (targetWidth != null && targetWidth! > descriptor.width) { - targetWidth = descriptor.width; + if (targetWidth != null && targetWidth! > intrinsicWidth) { + targetWidth = intrinsicWidth; } - if (targetHeight != null && targetHeight! > descriptor.height) { - targetHeight = descriptor.height; + if (targetHeight != null && targetHeight! > intrinsicHeight) { + targetHeight = intrinsicHeight; } } return TargetImageSize(width: targetWidth, height: targetHeight); @@ -2171,23 +2178,23 @@ Future instantiateImageCodecFromBuffer( /// /// This method is a convenience wrapper around the [ImageDescriptor] API. /// -/// The [buffer] parameter is the binary image data (e.g a PNG or GIF binary data). -/// The data can be for either static or animated images. The following image -/// formats are supported: {@macro dart.ui.imageFormats} +/// The [buffer] parameter is the binary image data (e.g a PNG or GIF binary +/// data). The data can be for either static or animated images. The following +/// image formats are supported: {@macro dart.ui.imageFormats} /// -/// The [buffer] will be disposed by this method once the codec has been created, -/// so the caller must relinquish ownership of the [buffer] when they call this -/// method. +/// The [buffer] will be disposed by this method once the codec has been +/// created, so the caller must relinquish ownership of the [buffer] when they +/// call this method. /// /// The [getTargetSize] parameter, when specified, will be invoked and passed -/// the image's [ImageDescriptor] to determine the size to decode the image to. +/// the image's intrinsic size to determine the size to decode the image to. /// The width and the height of the size it returns must be positive values -/// greater than or equal to 1, or null. It is valid to return a [TargetImageSize] -/// that specifies only one of `width` and `height` with the other remaining -/// null, in which case the omitted dimension will be scaled to maintain the -/// aspect ratio of the original dimensions. When both are null or omitted, -/// the image will be decoded at its native resolution (as will be the case if -/// the [getTargetSize] parameter is omitted). +/// greater than or equal to 1, or null. It is valid to return a +/// [TargetImageSize] that specifies only one of `width` and `height` with the +/// other remaining null, in which case the omitted dimension will be scaled to +/// maintain the aspect ratio of the original dimensions. When both are null or +/// omitted, the image will be decoded at its native resolution (as will be the +/// case if the [getTargetSize] parameter is omitted). /// /// Scaling the image to larger than its intrinsic size should usually be /// avoided, since it causes the image to use more memory than necessary. @@ -2195,6 +2202,13 @@ Future instantiateImageCodecFromBuffer( /// /// The returned future can complete with an error if the image decoding has /// failed. +/// +/// ## Compatibility note on the web +/// +/// When running Flutter on the web, only the CanvasKit renderer supports image +/// resizing capabilities (not the HTML renderer). So if image resizing is +/// critical to your use case, and you're deploying to the web, you should +/// build using the CanvasKit renderer. Future instantiateImageCodecWithSize( ImmutableBuffer buffer, { TargetImageSizeCallback? getTargetSize, @@ -2202,7 +2216,7 @@ Future instantiateImageCodecWithSize( getTargetSize ??= _getDefaultImageSize; final ImageDescriptor descriptor = await ImageDescriptor.encoded(buffer); try { - final TargetImageSize targetSize = getTargetSize(descriptor); + final TargetImageSize targetSize = getTargetSize(descriptor.width, descriptor.height); assert(targetSize.width == null || targetSize.width! > 0); assert(targetSize.height == null || targetSize.height! > 0); return descriptor.instantiateCodec( @@ -2214,16 +2228,21 @@ Future instantiateImageCodecWithSize( } } -TargetImageSize _getDefaultImageSize(ImageDescriptor descriptor) => const TargetImageSize(); +TargetImageSize _getDefaultImageSize(int intrinsicWidth, int intrinsicHeight) { + return const TargetImageSize(); +} /// Signature for a callback that determines the size to which an image should -/// be decoded given its [ImageDescriptor]. +/// be decoded given its intrinsic size. /// /// See also: /// /// * [instantiateImageCodecWithSize], which used this signature for its /// `getTargetSize` argument. -typedef TargetImageSizeCallback = TargetImageSize Function(ImageDescriptor descriptor); +typedef TargetImageSizeCallback = TargetImageSize Function( + int intrinsicWidth, + int intrinsicHeight, +); /// A specification of the size to which an image should be decoded. /// @@ -2239,7 +2258,7 @@ class TargetImageSize { /// must be positive. const TargetImageSize({this.width, this.height}) : assert(width == null || width > 0), - assert(width == null || width > 0); + assert(height == null || height > 0); /// The width into which to load the image. /// diff --git a/lib/web_ui/lib/painting.dart b/lib/web_ui/lib/painting.dart index 71332b39f37ae..bd7bf7de0062c 100644 --- a/lib/web_ui/lib/painting.dart +++ b/lib/web_ui/lib/painting.dart @@ -512,24 +512,29 @@ Future instantiateImageCodecWithSize( return engine.renderer.instantiateImageCodec(buffer._list!); } else { final Codec codec = await engine.renderer.instantiateImageCodec(buffer._list!); - final FrameInfo info = await codec.getNextFrame(); - final int width = info.image.width; - final int height = info.image.height; - info.image.dispose(); - codec.dispose(); - final ImageDescriptor descriptor = _SizeOnlyImageDescriptor(width, height); - final TargetImageSize targetSize = getTargetSize(descriptor); - return engine.renderer.instantiateImageCodec(buffer._list!, - targetWidth: targetSize.width, targetHeight: targetSize.height, allowUpscaling: false); + try { + final FrameInfo info = await codec.getNextFrame(); + try { + final int width = info.image.width; + final int height = info.image.height; + final TargetImageSize targetSize = getTargetSize(width, height); + return engine.renderer.instantiateImageCodec(buffer._list!, + targetWidth: targetSize.width, targetHeight: targetSize.height, allowUpscaling: false); + } finally { + info.image.dispose(); + } + } finally { + codec.dispose(); + } } } -typedef TargetImageSizeCallback = TargetImageSize Function(ImageDescriptor descriptor); +typedef TargetImageSizeCallback = TargetImageSize Function(int intrinsicWidth, int intrinsicHeight); class TargetImageSize { const TargetImageSize({this.width, this.height}) : assert(width == null || width > 0), - assert(width == null || width > 0); + assert(height == null || height > 0); final int? width; final int? height; From d31fa5854496170130c2d2fb6b579da1f1119e86 Mon Sep 17 00:00:00 2001 From: Todd Volkert Date: Thu, 19 Jan 2023 15:46:01 -0800 Subject: [PATCH 8/9] Forgot to remove the _SizeOnlyImageDescriptor class - it's no longer needed --- lib/web_ui/lib/painting.dart | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/lib/web_ui/lib/painting.dart b/lib/web_ui/lib/painting.dart index bd7bf7de0062c..f887a3a04178d 100644 --- a/lib/web_ui/lib/painting.dart +++ b/lib/web_ui/lib/painting.dart @@ -491,19 +491,6 @@ Future instantiateImageCodecFromBuffer( targetHeight: targetHeight, allowUpscaling: allowUpscaling); -class _SizeOnlyImageDescriptor implements ImageDescriptor { - const _SizeOnlyImageDescriptor(this.width, this.height); - - @override - final int width; - - @override - final int height; - - @override - dynamic noSuchMethod(Invocation invocation) => throw UnsupportedError('ImageDescriptor.${invocation.memberName} is not supported on web within a TargetImageSizeCallback.'); -} - Future instantiateImageCodecWithSize( ImmutableBuffer buffer, { TargetImageSizeCallback? getTargetSize, From 3d79727732b9155c5a28c9654ffce6c3a63e4203 Mon Sep 17 00:00:00 2001 From: Todd Volkert Date: Thu, 19 Jan 2023 16:22:50 -0800 Subject: [PATCH 9/9] Forgot to update test --- testing/dart/codec_test.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/testing/dart/codec_test.dart b/testing/dart/codec_test.dart index b79a320194539..08b38b0ce0ccf 100644 --- a/testing/dart/codec_test.dart +++ b/testing/dart/codec_test.dart @@ -91,10 +91,10 @@ void main() { final ui.ImmutableBuffer buffer = await ui.ImmutableBuffer.fromUint8List(data); final ui.Codec codec = await ui.instantiateImageCodecWithSize( buffer, - getTargetSize: (ui.ImageDescriptor descriptor) { + getTargetSize: (int intrinsicWidth, int intrinsicHeight) { return ui.TargetImageSize( - width: descriptor.width ~/ 2, - height: descriptor.height ~/ 2, + width: intrinsicWidth ~/ 2, + height: intrinsicHeight ~/ 2, ); }, );