// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/wallpaper/wallpaper_utils/wallpaper_resizer.h"
#include <utility>
#include "ash/utility/cropping_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback_forward.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/threading/thread_restrictions.h"
#include "base/time/time.h"
#include "cc/paint/color_filter.h"
#include "cc/paint/paint_flags.h"
#include "cc/paint/record_paint_canvas.h"
#include "third_party/skia/include/core/SkBlendMode.h"
#include "third_party/skia/include/core/SkColor.h"
#include "third_party/skia/include/core/SkImage.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/skia_conversions.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/image/image_skia_operations.h"
#include "ui/gfx/image/image_skia_rep.h"
namespace ash {
namespace {
// Kilobyte granularity is unnecessary because this metric is purely to prove
// that decoded wallpaper typically occupies several MBs, rather than 20 or 40.
constexpr int kDecodedWallpaperMetricMinMB = 1;
// Assuming 4 bytes per pixel (RGBA), 50 MB should be an image that's roughly
// 3630x3620. This exceeds the expected wallpaper size by a large margin.
constexpr int kDecodedWallpaperMetricMaxMB = 50;
constexpr int kDecodedWallpaperMetricNumBuckets = 10;
// Wallpapers with png format could be partially transparent. Ensures image
// pixels are opaque before painting.
//
// If `generate_immediate_bitmap` is true, the return value is backed by an
// `SkBitmap`. If false, it's backed by a sequence of paint operations, which,
// when evaluated later in the graphics pipeline, will generate the exact same
// opaque image.
gfx::ImageSkia MakeOpaque(const gfx::ImageSkia& image_in,
bool generate_immediate_bitmap) {
cc::RecordPaintCanvas record_canvas;
gfx::Canvas canvas(&record_canvas, /*image_scale=*/1.f);
cc::PaintFlags flags;
// Color filter ensures image pixels are opaque when drawn.
flags.setColorFilter(cc::ColorFilter::MakeBlend(
SkColor4f::FromColor(SK_ColorBLACK), SkBlendMode::kDstOver));
canvas.DrawImageInt(image_in, 0, 0, flags);
gfx::ImageSkiaRep opaque_image_rep(record_canvas.ReleaseAsRecord(),
image_in.size(),
/*scale=*/1.f);
return generate_immediate_bitmap
? gfx::ImageSkia::CreateFrom1xBitmap(opaque_image_rep.GetBitmap())
: gfx::ImageSkia(opaque_image_rep);
}
// Resizes `image` to `target_size` using `layout`.
//
// NOTE: `image` is intentionally a copy to ensure it exists for the duration of
// the function.
gfx::ImageSkia Resize(const gfx::ImageSkia image,
const gfx::Size& target_size,
WallpaperLayout layout) {
base::AssertLongCPUWorkAllowed();
SkBitmap orig_bitmap = *image.bitmap();
SkBitmap new_bitmap = orig_bitmap;
const int orig_width = orig_bitmap.width();
const int orig_height = orig_bitmap.height();
const int new_width = target_size.width();
const int new_height = target_size.height();
if (orig_width > new_width || orig_height > new_height) {
gfx::Rect wallpaper_rect(0, 0, orig_width, orig_height);
gfx::Size cropped_size = gfx::Size(std::min(new_width, orig_width),
std::min(new_height, orig_height));
switch (layout) {
case WALLPAPER_LAYOUT_CENTER:
wallpaper_rect.ClampToCenteredSize(cropped_size);
orig_bitmap.extractSubset(&new_bitmap,
gfx::RectToSkIRect(wallpaper_rect));
break;
case WALLPAPER_LAYOUT_TILE:
wallpaper_rect.set_size(cropped_size);
orig_bitmap.extractSubset(&new_bitmap,
gfx::RectToSkIRect(wallpaper_rect));
break;
case WALLPAPER_LAYOUT_STRETCH:
new_bitmap = skia::ImageOperations::Resize(
orig_bitmap, skia::ImageOperations::RESIZE_LANCZOS3, new_width,
new_height);
break;
case WALLPAPER_LAYOUT_CENTER_CROPPED:
if (orig_width > new_width && orig_height > new_height) {
new_bitmap = skia::ImageOperations::Resize(
CenterCropImage(orig_bitmap, target_size),
skia::ImageOperations::RESIZE_LANCZOS3, new_width, new_height);
}
break;
case NUM_WALLPAPER_LAYOUT:
NOTREACHED();
}
}
// Generating the bitmap right now is both acceptable and desirable since it's
// on a blocking thread that can handle potentially long cpu work and ensures
// the opaque bitmap is only ever generated once.
return MakeOpaque(gfx::ImageSkia::CreateFrom1xBitmap(new_bitmap),
/*generate_immediate_bitmap=*/true);
}
} // namespace
// static
uint32_t WallpaperResizer::GetImageId(const gfx::ImageSkia& image) {
const gfx::ImageSkiaRep& image_rep = image.GetRepresentation(1.0f);
return image_rep.is_null() ? 0 : image_rep.GetBitmap().getGenerationID();
}
// static
gfx::ImageSkia WallpaperResizer::GetResizedImage(const gfx::ImageSkia& image,
int max_size_in_dips) {
float aspect_ratio =
static_cast<float>(image.width()) / static_cast<float>(image.height());
int height = max_size_in_dips;
int width = static_cast<int>(aspect_ratio * height);
if (width > max_size_in_dips) {
width = max_size_in_dips;
height = static_cast<int>(width / aspect_ratio);
}
return gfx::ImageSkiaOperations::CreateResizedImage(
image, skia::ImageOperations::RESIZE_BEST, gfx::Size(width, height));
}
WallpaperResizer::WallpaperResizer(const gfx::ImageSkia& image,
const gfx::Size& target_size,
const WallpaperInfo& wallpaper_info)
// Generating the opaque bitmap could potentially require long cpu work,
// which is not appropriate on the main thread. The bitmap will be
// generated later in the compositor on a more appropriate thread. This
// non-resized version of the `image_` is short-lived anyways and will be
// replaced with the resized opaque version imminently.
: image_(MakeOpaque(image, /*generate_immediate_bitmap=*/false)),
original_image_id_(GetImageId(image)),
target_size_(target_size),
wallpaper_info_(wallpaper_info),
sequenced_task_runner_(base::ThreadPool::CreateSequencedTaskRunner(
{base::MayBlock(), base::TaskPriority::USER_VISIBLE,
base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN})) {
image_.MakeThreadSafe();
}
WallpaperResizer::~WallpaperResizer() = default;
void WallpaperResizer::StartResize(base::OnceClosure on_resize_done) {
weak_ptr_factory_.InvalidateWeakPtrs();
start_calculation_time_ = base::TimeTicks::Now();
if (!sequenced_task_runner_->PostTaskAndReplyWithResult(
FROM_HERE,
// The `DeepCopy()` of `image_` is necessary otherwise there's a
// potential race condition if `ImageSkiaRep::GetBitmap()` gets
// called concurrently from `sequenced_task_runner_`'s thread and the
// main thread. Note in this case, the deep copy is actually cheap
// because the `image_` is still backed by a paint record at this
// point (not actual pixels), and paint records are cheap to copy.
base::BindOnce(&Resize, image_.DeepCopy(), target_size_,
wallpaper_info_.layout),
base::BindOnce(&WallpaperResizer::OnResizeFinished,
weak_ptr_factory_.GetWeakPtr(),
std::move(on_resize_done)))) {
LOG(WARNING) << "PostSequencedWorkerTask failed. "
<< "Wallpaper may not be resized.";
}
}
void WallpaperResizer::OnResizeFinished(base::OnceClosure on_resize_done,
gfx::ImageSkia resized_image) {
static constexpr size_t kBytesPerMegabyte = 1024 * 1024;
base::UmaHistogramCustomCounts(
"Ash.Wallpaper.DecodedSizeMB",
base::ClampRound(
static_cast<float>(resized_image.bitmap()->computeByteSize()) /
kBytesPerMegabyte),
kDecodedWallpaperMetricMinMB, kDecodedWallpaperMetricMaxMB,
kDecodedWallpaperMetricNumBuckets);
DVLOG(2) << __func__ << " old=" << image_.size().ToString()
<< " new=" << resized_image.size().ToString()
<< " time=" << base::TimeTicks::Now() - start_calculation_time_;
image_ = std::move(resized_image);
std::move(on_resize_done).Run();
}
} // namespace ash