[go: nahoru, domu]

blob: 689be13a31408b4a618ff19b675b2750a8813f9c [file] [log] [blame]
// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ash/login/users/avatar/user_image_loader.h"
#include <memory>
#include <utility>
#include "ash/public/cpp/image_downloader.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_functions.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/single_thread_task_runner.h"
#include "base/task/thread_pool.h"
#include "chrome/browser/ash/login/helper.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/ui/ash/image_downloader_impl.h"
#include "components/user_manager/user_image/user_image.h"
#include "google_apis/credentials_mode.h"
#include "ipc/ipc_channel.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "services/data_decoder/public/cpp/data_decoder.h"
#include "services/data_decoder/public/cpp/decode_image.h"
#include "services/data_decoder/public/mojom/image_decoder.mojom.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "skia/ext/image_operations.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/encode/SkWebpEncoder.h"
#include "ui/gfx/codec/png_codec.h"
#include "ui/gfx/codec/webp_codec.h"
#include "ui/gfx/skbitmap_operations.h"
#include "url/gurl.h"
namespace ash {
namespace user_image_loader {
namespace {
constexpr const char kURLLoaderDownloadSuccessHistogramName[] =
"Ash.UserImage.URLLoaderDownloadSuccess";
constexpr int64_t kMaxImageSizeInBytes =
static_cast<int64_t>(IPC::Channel::kMaximumMessageSize);
// Contains attributes we need to know about each image we decode.
struct ImageInfo {
ImageInfo(const base::FilePath& file_path,
int pixels_per_side,
ImageDecoder::ImageCodec image_codec,
LoadedCallback loaded_cb)
: file_path(file_path),
pixels_per_side(pixels_per_side),
image_codec(image_codec),
loaded_cb(std::move(loaded_cb)) {}
ImageInfo(ImageInfo&&) = default;
ImageInfo& operator=(ImageInfo&&) = default;
~ImageInfo() {}
base::FilePath file_path;
int pixels_per_side;
ImageDecoder::ImageCodec image_codec;
LoadedCallback loaded_cb;
};
// Crops `image` to the square format and downsizes the image to
// `target_size` in pixels. On success, returns the bytes representation and
// stores the cropped image in `bitmap`, and the format of the bytes
// representation in `image_format`. On failure, returns nullptr, and
// the contents of `bitmap` and `image_format` are undefined.
scoped_refptr<base::RefCountedBytes> CropImage(
const SkBitmap& image,
int target_size,
SkBitmap* bitmap,
user_manager::UserImage::ImageFormat* image_format) {
DCHECK_GT(target_size, 0);
DCHECK(image_format);
SkBitmap final_image;
// Auto crop the image, taking the largest square in the center.
int pixels_per_side = std::min(image.width(), image.height());
int x = (image.width() - pixels_per_side) / 2;
int y = (image.height() - pixels_per_side) / 2;
SkBitmap cropped_image = SkBitmapOperations::CreateTiledBitmap(
image, x, y, pixels_per_side, pixels_per_side);
if (pixels_per_side > target_size) {
// Also downsize the image to save space and memory.
final_image = skia::ImageOperations::Resize(
cropped_image, skia::ImageOperations::RESIZE_LANCZOS3, target_size,
target_size);
} else {
final_image = cropped_image;
}
// Encode the cropped image to web-compatible bytes representation
*image_format = user_manager::UserImage::ChooseImageFormat(final_image);
scoped_refptr<base::RefCountedBytes> encoded =
user_manager::UserImage::Encode(final_image, *image_format);
if (encoded) {
bitmap->swap(final_image);
}
return encoded;
}
// Returns the image format for the bytes representation of the user image
// from the image codec used for loading the image.
user_manager::UserImage::ImageFormat ChooseImageFormatFromCodec(
ImageDecoder::ImageCodec image_codec) {
switch (image_codec) {
case ImageDecoder::PNG_CODEC:
return user_manager::UserImage::FORMAT_PNG;
case ImageDecoder::DEFAULT_CODEC:
// The default codec can accept many kinds of image formats, hence the
// image format of the bytes representation is unknown.
return user_manager::UserImage::FORMAT_UNKNOWN;
}
NOTREACHED();
return user_manager::UserImage::FORMAT_UNKNOWN;
}
// Handles the decoded image returned from ImageDecoder through the
// ImageRequest interface.
class UserImageRequest : public ImageDecoder::ImageRequest {
public:
UserImageRequest(
ImageInfo image_info,
const std::string& image_data,
scoped_refptr<base::SequencedTaskRunner> background_task_runner)
: image_info_(std::move(image_info)),
// TODO(crbug.com/593251): Remove the data copy here.
image_data_(new base::RefCountedBytes(
reinterpret_cast<const unsigned char*>(image_data.data()),
image_data.size())),
background_task_runner_(background_task_runner) {}
~UserImageRequest() override {}
// ImageDecoder::ImageRequest implementation.
void OnImageDecoded(const SkBitmap& decoded_image) override;
void OnDecodeImageFailed() override;
// Called after the image is cropped (and downsized) as needed.
void OnImageCropped(SkBitmap* bitmap,
user_manager::UserImage::ImageFormat* image_format,
scoped_refptr<base::RefCountedBytes> bytes);
// Called after the image is finalized. `image_bytes_regenerated` is true
// if `image_bytes` is regenerated from the cropped image.
void OnImageFinalized(const SkBitmap& image,
user_manager::UserImage::ImageFormat image_format,
scoped_refptr<base::RefCountedBytes> image_bytes,
bool image_bytes_regenerated);
private:
ImageInfo image_info_;
scoped_refptr<base::RefCountedBytes> image_data_;
scoped_refptr<base::SequencedTaskRunner> background_task_runner_;
// This should be the last member.
base::WeakPtrFactory<UserImageRequest> weak_ptr_factory_{this};
};
void UserImageRequest::OnImageDecoded(const SkBitmap& decoded_image) {
int target_size = image_info_.pixels_per_side;
if (target_size > 0) {
// Cropping an image could be expensive, hence posting to the background
// thread.
SkBitmap* bitmap = new SkBitmap;
auto* image_format = new user_manager::UserImage::ImageFormat(
user_manager::UserImage::FORMAT_UNKNOWN);
background_task_runner_->PostTaskAndReplyWithResult(
FROM_HERE,
base::BindOnce(&CropImage, decoded_image, target_size, bitmap,
image_format),
base::BindOnce(&UserImageRequest::OnImageCropped,
weak_ptr_factory_.GetWeakPtr(), base::Owned(bitmap),
base::Owned(image_format)));
} else {
const user_manager::UserImage::ImageFormat image_format =
ChooseImageFormatFromCodec(image_info_.image_codec);
OnImageFinalized(decoded_image, image_format, image_data_,
false /* image_bytes_regenerated */);
}
}
void UserImageRequest::OnImageCropped(
SkBitmap* bitmap,
user_manager::UserImage::ImageFormat* image_format,
scoped_refptr<base::RefCountedBytes> bytes) {
DCHECK_GT(image_info_.pixels_per_side, 0);
if (!bytes) {
OnDecodeImageFailed();
return;
}
OnImageFinalized(*bitmap, *image_format, bytes,
true /* image_bytes_regenerated */);
}
void UserImageRequest::OnImageFinalized(
const SkBitmap& image,
user_manager::UserImage::ImageFormat image_format,
scoped_refptr<base::RefCountedBytes> image_bytes,
bool image_bytes_regenerated) {
SkBitmap final_image = image;
// Make the SkBitmap immutable as we won't modify it. This is important
// because otherwise it gets duplicated during painting, wasting memory.
final_image.setImmutable();
gfx::ImageSkia final_image_skia =
gfx::ImageSkia::CreateFrom1xBitmap(final_image);
final_image_skia.MakeThreadSafe();
std::unique_ptr<user_manager::UserImage> user_image(
new user_manager::UserImage(final_image_skia, image_bytes, image_format));
user_image->set_file_path(image_info_.file_path);
// The user image is safe if it is decoded using one of the robust image
// decoders, or regenerated by Chrome's image encoder.
if (image_info_.image_codec == ImageDecoder::PNG_CODEC ||
image_bytes_regenerated) {
user_image->MarkAsSafe();
}
std::move(image_info_.loaded_cb).Run(std::move(user_image));
delete this;
}
void UserImageRequest::OnDecodeImageFailed() {
std::move(image_info_.loaded_cb)
.Run(base::WrapUnique(new user_manager::UserImage));
delete this;
}
// Starts decoding the image with ImageDecoder for the image `data` if
// `data_is_ready` is true.
void DecodeImage(
ImageInfo image_info,
scoped_refptr<base::SequencedTaskRunner> background_task_runner,
const std::string* data,
bool data_is_ready) {
if (!data_is_ready) {
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(std::move(image_info.loaded_cb),
base::WrapUnique(new user_manager::UserImage)));
return;
}
ImageDecoder::ImageCodec codec = image_info.image_codec;
UserImageRequest* image_request = new UserImageRequest(
std::move(image_info), *data, background_task_runner);
ImageDecoder::StartWithOptions(image_request, *data, codec, false);
}
void OnAnimationDecoded(
LoadedCallback loaded_cb,
bool require_encode,
std::unique_ptr<std::string> maybe_safe_encoded_data,
std::vector<data_decoder::mojom::AnimationFramePtr> mojo_frames) {
auto frame_size = mojo_frames.size();
if (!frame_size) {
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(std::move(loaded_cb),
std::make_unique<user_manager::UserImage>()));
return;
}
// If `require_encode` is false, do not encode again, return with
// `maybe_safe_encoded_data`.
if (!require_encode) {
auto image_skia =
gfx::ImageSkia::CreateFrom1xBitmap(mojo_frames[0]->bitmap);
image_skia.MakeThreadSafe();
auto bytes = base::MakeRefCounted<base::RefCountedBytes>(
reinterpret_cast<const uint8_t*>(maybe_safe_encoded_data->data()),
maybe_safe_encoded_data->size());
auto user_image = std::make_unique<user_manager::UserImage>(
image_skia, bytes,
frame_size == 1 ? user_manager::UserImage::FORMAT_PNG
: user_manager::UserImage::FORMAT_WEBP);
user_image->MarkAsSafe();
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(std::move(loaded_cb), std::move(user_image)));
return;
}
// Re-encode static image as PNG and send to requester.
if (frame_size == 1) {
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE,
base::BindOnce(
[](const SkBitmap& bitmap) {
auto encoded = base::MakeRefCounted<base::RefCountedBytes>();
if (!gfx::PNGCodec::EncodeBGRASkBitmap(
bitmap, /*discard_transparency=*/false,
&encoded->data())) {
return std::make_unique<user_manager::UserImage>();
}
auto image_skia = gfx::ImageSkia::CreateFrom1xBitmap(bitmap);
image_skia.MakeThreadSafe();
auto user_image = std::make_unique<user_manager::UserImage>(
image_skia, encoded, user_manager::UserImage::FORMAT_PNG);
user_image->MarkAsSafe();
return user_image;
},
mojo_frames[0]->bitmap),
std::move(loaded_cb));
return;
}
// The image is animated, re-encode as WebP animated image and send to
// requester.
std::vector<gfx::WebpCodec::Frame> frames;
for (auto& mojo_frame : mojo_frames) {
gfx::WebpCodec::Frame frame;
frame.bitmap = mojo_frame->bitmap;
frame.duration = mojo_frame->duration.InMilliseconds();
frames.push_back(frame);
}
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE,
base::BindOnce(
[](const std::vector<gfx::WebpCodec::Frame>& frames) {
SkWebpEncoder::Options options;
options.fCompression = SkWebpEncoder::Compression::kLossless;
// Lower quality under kLossless compression means compress faster
// into larger files.
options.fQuality = 0;
auto encoded = gfx::WebpCodec::EncodeAnimated(frames, options);
if (!encoded.has_value()) {
return std::make_unique<user_manager::UserImage>();
}
auto image_skia =
gfx::ImageSkia::CreateFrom1xBitmap(frames[0].bitmap);
image_skia.MakeThreadSafe();
auto bytes =
base::MakeRefCounted<base::RefCountedBytes>(encoded.value());
auto user_image = std::make_unique<user_manager::UserImage>(
image_skia, bytes, user_manager::UserImage::FORMAT_WEBP);
user_image->MarkAsSafe();
return user_image;
},
std::move(frames)),
std::move(loaded_cb));
}
void DecodeAnimation(LoadedCallback loaded_cb,
bool require_encode,
std::unique_ptr<std::string> data) {
if (!data || data->empty()) {
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(std::move(loaded_cb),
std::make_unique<user_manager::UserImage>()));
return;
}
auto bytes = base::as_byte_span(*data);
data_decoder::DecodeAnimationIsolated(
bytes, /*shrink_to_fit=*/true, kMaxImageSizeInBytes,
base::BindOnce(&OnAnimationDecoded, std::move(loaded_cb), require_encode,
std::move(data)));
}
void OnImageDownloaded(std::unique_ptr<network::SimpleURLLoader> loader,
LoadedCallback loaded_cb,
std::unique_ptr<std::string> body) {
if (loader->NetError() != net::OK || !body) {
base::UmaHistogramBoolean(kURLLoaderDownloadSuccessHistogramName, false);
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(std::move(loaded_cb),
std::make_unique<user_manager::UserImage>()));
return;
}
base::UmaHistogramBoolean(kURLLoaderDownloadSuccessHistogramName, true);
DecodeAnimation(std::move(loaded_cb), /*require_encode=*/true,
std::move(body));
}
} // namespace
void StartWithFilePath(
scoped_refptr<base::SequencedTaskRunner> background_task_runner,
const base::FilePath& file_path,
ImageDecoder::ImageCodec image_codec,
int pixels_per_side,
LoadedCallback loaded_cb) {
std::string* data = new std::string;
background_task_runner->PostTaskAndReplyWithResult(
FROM_HERE, base::BindOnce(&base::ReadFileToString, file_path, data),
base::BindOnce(&DecodeImage,
ImageInfo(file_path, pixels_per_side, image_codec,
std::move(loaded_cb)),
background_task_runner, base::Owned(data)));
}
void StartWithData(
scoped_refptr<base::SequencedTaskRunner> background_task_runner,
std::unique_ptr<std::string> data,
ImageDecoder::ImageCodec image_codec,
int pixels_per_side,
LoadedCallback loaded_cb) {
DecodeImage(ImageInfo(base::FilePath(), pixels_per_side, image_codec,
std::move(loaded_cb)),
background_task_runner, data.get(), true /* data_is_ready */);
}
void StartWithFilePathAnimated(
scoped_refptr<base::SequencedTaskRunner> background_task_runner,
const base::FilePath& file_path,
LoadedCallback loaded_cb) {
background_task_runner->PostTaskAndReplyWithResult(
FROM_HERE,
base::BindOnce(
[](const base::FilePath& file_path) {
std::string data;
if (!base::ReadFileToString(file_path, &data)) {
data.clear();
}
return std::make_unique<std::string>(std::move(data));
},
file_path),
base::BindOnce(&DecodeAnimation, std::move(loaded_cb),
/*require_encode=*/false));
}
// Used to load user images from GURL, specifically in the case of
// retrieving images from the cloud.
void StartWithGURLAnimated(const GURL& default_image_url,
LoadedCallback loaded_cb) {
constexpr net::NetworkTrafficAnnotationTag kNetworkTrafficAnnotationTag =
net::DefineNetworkTrafficAnnotation("user_image_downloader", R"(
semantics: {
sender: "User Image Downloader"
description:
"Google default user images are loaded from cloud avatar image "
"resources. Images are downloaded on an as-needed basis."
trigger:
"Triggered when the user opens the avatar image picker or "
"when the current default user image needs to be cached."
data: "The URL for which to retrieve a user image."
destination: GOOGLE_OWNED_SERVICE
}
policy: {
cookies_allowed: NO
setting:
"This request cannot be disabled, but it is only triggered "
"by user action."
policy_exception_justification: "Not implemented."
})");
auto request = std::make_unique<network::ResourceRequest>();
request->url = default_image_url;
request->credentials_mode =
google_apis::GetOmitCredentialsModeForGaiaRequests();
auto loader = network::SimpleURLLoader::Create(std::move(request),
kNetworkTrafficAnnotationTag);
loader->SetRetryOptions(
/*max_retries=*/5,
network::SimpleURLLoader::RetryMode::RETRY_ON_5XX |
network::SimpleURLLoader::RETRY_ON_NETWORK_CHANGE |
network::SimpleURLLoader::RETRY_ON_NAME_NOT_RESOLVED);
auto* loader_ptr = loader.get();
loader_ptr->DownloadToString(
g_browser_process->shared_url_loader_factory().get(),
base::BindOnce(&OnImageDownloaded, std::move(loader),
std::move(loaded_cb)),
network::SimpleURLLoader::kMaxBoundedStringDownloadSize);
}
} // namespace user_image_loader
} // namespace ash