Skip to content

Commit c7d0fb7

Browse files
ignatzcbracken
authored andcommitted
More efficient decoding for down-sampled Flutter images using cache(Width|Height) (#15372)
When down-scaling images, decode encoded images into smaller images closer to the target size before finally down-scaling them to their target size. For very large images this can avoid inflating the image into its full size first before throwing it away. This can help to significantly reduce peak memory utilization. On a tangent, we could be even more efficient, if we'd interpret the cache(Width|Height) as sizing hints. I also opportunistically added warnings, I don't think a "caching" API should support scaling images up or changing their aspect ratio.
1 parent e0fe834 commit c7d0fb7

6 files changed

Lines changed: 169 additions & 28 deletions

File tree

ci/licenses_golden/licenses_flutter

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,7 @@ FILE: ../../../flutter/lib/ui/dart_ui.h
272272
FILE: ../../../flutter/lib/ui/dart_wrapper.h
273273
FILE: ../../../flutter/lib/ui/fixtures/DashInNooglerHat.jpg
274274
FILE: ../../../flutter/lib/ui/fixtures/Horizontal.jpg
275+
FILE: ../../../flutter/lib/ui/fixtures/Horizontal.png
275276
FILE: ../../../flutter/lib/ui/fixtures/hello_loop_2.gif
276277
FILE: ../../../flutter/lib/ui/fixtures/hello_loop_2.webp
277278
FILE: ../../../flutter/lib/ui/fixtures/ui_test.dart

lib/ui/BUILD.gn

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ if (current_toolchain == host_toolchain) {
155155
fixtures = [
156156
"fixtures/DashInNooglerHat.jpg",
157157
"fixtures/Horizontal.jpg",
158+
"fixtures/Horizontal.png",
158159
"fixtures/hello_loop_2.gif",
159160
"fixtures/hello_loop_2.webp",
160161
]

lib/ui/fixtures/Horizontal.png

25 KB
Loading

lib/ui/painting/image_decoder.cc

Lines changed: 123 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,18 @@
44

55
#include "flutter/lib/ui/painting/image_decoder.h"
66

7+
#include <algorithm>
8+
79
#include "flutter/fml/make_copyable.h"
8-
#include "flutter/fml/trace_event.h"
910
#include "third_party/skia/include/codec/SkCodec.h"
11+
#include "third_party/skia/src/codec/SkCodecImageGenerator.h"
1012

1113
namespace flutter {
14+
namespace {
15+
16+
constexpr double kAspectRatioChangedThreshold = 0.01;
17+
18+
} // namespace
1219

1320
ImageDecoder::ImageDecoder(
1421
TaskRunners runners,
@@ -25,6 +32,10 @@ ImageDecoder::ImageDecoder(
2532

2633
ImageDecoder::~ImageDecoder() = default;
2734

35+
static double AspectRatio(const SkISize& size) {
36+
return static_cast<double>(size.width()) / size.height();
37+
}
38+
2839
// Get the updated dimensions of the image. If both dimensions are specified,
2940
// use them. If one of them is specified, respect the one that is and use the
3041
// aspect ratio to calculate the other. If neither dimension is specified, use
@@ -40,8 +51,7 @@ static SkISize GetResizedDimensions(SkISize current_size,
4051
return SkISize::Make(target_width.value(), target_height.value());
4152
}
4253

43-
const auto aspect_ratio =
44-
static_cast<double>(current_size.width()) / current_size.height();
54+
const auto aspect_ratio = AspectRatio(current_size);
4555

4656
if (target_width) {
4757
return SkISize::Make(target_width.value(),
@@ -57,34 +67,52 @@ static SkISize GetResizedDimensions(SkISize current_size,
5767
}
5868

5969
static sk_sp<SkImage> ResizeRasterImage(sk_sp<SkImage> image,
60-
std::optional<uint32_t> target_width,
61-
std::optional<uint32_t> target_height,
70+
const SkISize& resized_dimensions,
6271
const fml::tracing::TraceFlow& flow) {
6372
FML_DCHECK(!image->isTextureBacked());
6473

65-
const auto resized_dimensions =
66-
GetResizedDimensions(image->dimensions(), target_width, target_height);
74+
TRACE_EVENT0("flutter", __FUNCTION__);
75+
flow.Step(__FUNCTION__);
6776

6877
if (resized_dimensions.isEmpty()) {
6978
FML_LOG(ERROR) << "Could not resize to empty dimensions.";
7079
return nullptr;
7180
}
7281

73-
if (resized_dimensions == image->dimensions()) {
74-
// The resized dimesions are the same as the intrinsic dimensions of the
75-
// image. There is nothing to do.
76-
return image;
82+
if (image->dimensions() == resized_dimensions) {
83+
return image->makeRasterImage();
7784
}
7885

79-
TRACE_EVENT0("flutter", __FUNCTION__);
80-
flow.Step(__FUNCTION__);
86+
if (resized_dimensions.width() > image->dimensions().width() ||
87+
resized_dimensions.height() > image->dimensions().height()) {
88+
FML_LOG(WARNING) << "Image is being upsized from "
89+
<< image->dimensions().width() << "x"
90+
<< image->dimensions().height() << " to "
91+
<< resized_dimensions.width() << "x"
92+
<< resized_dimensions.height()
93+
<< ". Are cache(Height|Width) used correctly?";
94+
// TOOD(48885): consider exiting here, there's no good reason to support
95+
// upsampling in a "caching"-optimization context..
96+
}
8197

82-
const auto scaled_image_info = image->imageInfo().makeWH(
83-
resized_dimensions.width(), resized_dimensions.height());
98+
const bool aspect_ratio_changed =
99+
std::abs(AspectRatio(resized_dimensions) -
100+
AspectRatio(image->dimensions())) > kAspectRatioChangedThreshold;
101+
if (aspect_ratio_changed) {
102+
// This is probably a bug. If a user passes dimensions that change the
103+
// aspect ratio in a "caching" context that's probably not working as
104+
// intended and rather a signal that the API is hard to use.
105+
FML_LOG(WARNING)
106+
<< "Aspect ratio changes. Are cache(Height|Width) used correctly?";
107+
}
108+
109+
const auto scaled_image_info =
110+
image->imageInfo().makeDimensions(resized_dimensions);
84111

85112
SkBitmap scaled_bitmap;
86113
if (!scaled_bitmap.tryAllocPixels(scaled_image_info)) {
87-
FML_LOG(ERROR) << "Could not allocate bitmap when attempting to scale.";
114+
FML_LOG(ERROR) << "Failed to allocate memory for bitmap of size "
115+
<< scaled_image_info.computeMinByteSize() << "B";
88116
return nullptr;
89117
}
90118

@@ -122,31 +150,98 @@ static sk_sp<SkImage> ImageFromDecompressedData(
122150
return nullptr;
123151
}
124152

125-
return ResizeRasterImage(std::move(image), target_width, target_height, flow);
153+
if (!target_width && !target_height) {
154+
// No resizing requested. Just rasterize the image.
155+
return image->makeRasterImage();
156+
}
157+
158+
auto resized_dimensions =
159+
GetResizedDimensions(image->dimensions(), target_width, target_height);
160+
161+
return ResizeRasterImage(std::move(image), resized_dimensions, flow);
126162
}
127163

128-
static sk_sp<SkImage> ImageFromCompressedData(
129-
sk_sp<SkData> data,
130-
std::optional<uint32_t> target_width,
131-
std::optional<uint32_t> target_height,
132-
const fml::tracing::TraceFlow& flow) {
164+
sk_sp<SkImage> ImageFromCompressedData(sk_sp<SkData> data,
165+
std::optional<uint32_t> target_width,
166+
std::optional<uint32_t> target_height,
167+
const fml::tracing::TraceFlow& flow) {
133168
TRACE_EVENT0("flutter", __FUNCTION__);
134169
flow.Step(__FUNCTION__);
135170

136-
auto decoded_image = SkImage::MakeFromEncoded(data);
171+
if (!target_width && !target_height) {
172+
// No resizing requested. Just decode & rasterize the image.
173+
return SkImage::MakeFromEncoded(data)->makeRasterImage();
174+
}
137175

138-
if (!decoded_image) {
176+
auto codec = SkCodec::MakeFromData(data);
177+
if (codec == nullptr) {
139178
return nullptr;
140179
}
141180

142-
// Make sure to resolve all lazy images.
143-
decoded_image = decoded_image->makeRasterImage();
181+
const auto* codec_ptr = codec.get();
144182

145-
if (!decoded_image) {
183+
// Note that we cannot read the dimensions from the codec since they don't
184+
// respect image orientation provided e.g. in EXIF data.
185+
auto image_generator = SkCodecImageGenerator::MakeFromCodec(std::move(codec));
186+
const auto& source_dimensions = image_generator->getInfo().dimensions();
187+
188+
auto resized_dimensions =
189+
GetResizedDimensions(source_dimensions, target_width, target_height);
190+
191+
// No resize needed.
192+
if (resized_dimensions == source_dimensions) {
193+
return SkImage::MakeFromEncoded(data)->makeRasterImage();
194+
}
195+
196+
auto decode_dimensions = codec_ptr->getScaledDimensions(
197+
std::max(static_cast<double>(resized_dimensions.width()) /
198+
source_dimensions.width(),
199+
static_cast<double>(resized_dimensions.height()) /
200+
source_dimensions.height()));
201+
202+
// If the codec supports efficient sub-pixel decoding, decoded at a resolution
203+
// close to the target resolution before resizing.
204+
if (decode_dimensions != codec_ptr->dimensions()) {
205+
if (source_dimensions != codec_ptr->dimensions()) {
206+
decode_dimensions =
207+
SkISize::Make(decode_dimensions.height(), decode_dimensions.width());
208+
}
209+
210+
auto scaled_image_info =
211+
image_generator->getInfo().makeDimensions(decode_dimensions);
212+
213+
SkBitmap scaled_bitmap;
214+
if (!scaled_bitmap.tryAllocPixels(scaled_image_info)) {
215+
FML_LOG(ERROR) << "Failed to allocate memory for bitmap of size "
216+
<< scaled_image_info.computeMinByteSize() << "B";
217+
return nullptr;
218+
}
219+
220+
const auto& pixmap = scaled_bitmap.pixmap();
221+
if (image_generator->getPixels(pixmap.info(), pixmap.writable_addr(),
222+
pixmap.rowBytes())) {
223+
// Marking this as immutable makes the MakeFromBitmap call share
224+
// the pixels instead of copying.
225+
scaled_bitmap.setImmutable();
226+
227+
auto decoded_image = SkImage::MakeFromBitmap(scaled_bitmap);
228+
FML_DCHECK(decoded_image);
229+
if (!decoded_image) {
230+
FML_LOG(ERROR)
231+
<< "Could not create a scaled image from a scaled bitmap.";
232+
return nullptr;
233+
}
234+
return ResizeRasterImage(std::move(decoded_image), resized_dimensions,
235+
flow);
236+
}
237+
}
238+
239+
auto image = SkImage::MakeFromEncoded(data);
240+
if (!image) {
146241
return nullptr;
147242
}
148243

149-
return ResizeRasterImage(decoded_image, target_width, target_height, flow);
244+
return ResizeRasterImage(std::move(image), resized_dimensions, flow);
150245
}
151246

152247
static SkiaGPUObject<SkImage> UploadRasterImage(

lib/ui/painting/image_decoder.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#include "flutter/fml/concurrent_message_loop.h"
1414
#include "flutter/fml/macros.h"
1515
#include "flutter/fml/mapping.h"
16+
#include "flutter/fml/trace_event.h"
1617
#include "flutter/lib/ui/io_manager.h"
1718
#include "third_party/skia/include/core/SkData.h"
1819
#include "third_party/skia/include/core/SkImage.h"
@@ -68,6 +69,11 @@ class ImageDecoder {
6869
FML_DISALLOW_COPY_AND_ASSIGN(ImageDecoder);
6970
};
7071

72+
sk_sp<SkImage> ImageFromCompressedData(sk_sp<SkData> data,
73+
std::optional<uint32_t> target_width,
74+
std::optional<uint32_t> target_height,
75+
const fml::tracing::TraceFlow& flow);
76+
7177
} // namespace flutter
7278

7379
#endif // FLUTTER_LIB_UI_PAINTING_IMAGE_DECODER_H_

lib/ui/painting/image_decoder_unittests.cc

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,5 +519,43 @@ TEST(ImageDecoderTest,
519519
ASSERT_EQ(webp_codec->getRepetitionCount(), 1);
520520
}
521521

522+
TEST(ImageDecoderTest, VerifySimpleDecoding) {
523+
auto data = OpenFixtureAsSkData("Horizontal.jpg");
524+
auto image = SkImage::MakeFromEncoded(data);
525+
ASSERT_TRUE(image != nullptr);
526+
ASSERT_EQ(SkISize::Make(600, 200), image->dimensions());
527+
528+
ASSERT_EQ(ImageFromCompressedData(data, 6, 2, fml::tracing::TraceFlow(""))
529+
->dimensions(),
530+
SkISize::Make(6, 2));
531+
}
532+
533+
TEST(ImageDecoderTest, VerifySubpixelDecodingPreservesExifOrientation) {
534+
auto data = OpenFixtureAsSkData("Horizontal.jpg");
535+
auto image = SkImage::MakeFromEncoded(data);
536+
ASSERT_TRUE(image != nullptr);
537+
ASSERT_EQ(SkISize::Make(600, 200), image->dimensions());
538+
539+
auto decode = [data](std::optional<uint32_t> target_width,
540+
std::optional<uint32_t> target_height) {
541+
return ImageFromCompressedData(data, target_width, target_height,
542+
fml::tracing::TraceFlow(""));
543+
};
544+
545+
auto expected_data = OpenFixtureAsSkData("Horizontal.png");
546+
ASSERT_TRUE(expected_data != nullptr);
547+
ASSERT_FALSE(expected_data->isEmpty());
548+
549+
auto assert_image = [&](auto decoded_image) {
550+
ASSERT_EQ(decoded_image->dimensions(), SkISize::Make(300, 100));
551+
ASSERT_TRUE(decoded_image->encodeToData(SkEncodedImageFormat::kPNG, 100)
552+
->equals(expected_data.get()));
553+
};
554+
555+
assert_image(decode(300, 100));
556+
assert_image(decode(300, {}));
557+
assert_image(decode({}, 100));
558+
}
559+
522560
} // namespace testing
523561
} // namespace flutter

0 commit comments

Comments
 (0)