blob: c8e4857d80a5586886409419445fe39f4dd60271 [file] [log] [blame]
// Copyright 2019 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/ui/thumbnails/thumbnail_image.h"
#include <utility>
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_macros.h"
#include "base/ranges/algorithm.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/time/time.h"
#include "base/trace_event/trace_event.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/gfx/codec/jpeg_codec.h"
#include "ui/gfx/geometry/skia_conversions.h"
ThumbnailImage::Subscription::Subscription(
scoped_refptr<ThumbnailImage> thumbnail)
: thumbnail_(std::move(thumbnail)) {}
ThumbnailImage::Subscription::~Subscription() {
thumbnail_->HandleSubscriptionDestroyed(this);
}
ThumbnailImage::CaptureReadiness ThumbnailImage::Delegate::GetCaptureReadiness()
const {
return CaptureReadiness::kNotReady;
}
ThumbnailImage::Delegate::~Delegate() {
if (thumbnail_)
thumbnail_->delegate_ = nullptr;
}
ThumbnailImage::ThumbnailImage(Delegate* delegate, CompressedThumbnailData data)
: delegate_(delegate), data_(std::move(data)) {
DETACH_FROM_SEQUENCE(sequence_checker_);
DCHECK(delegate_);
DCHECK(!delegate_->thumbnail_);
delegate_->thumbnail_ = this;
}
ThumbnailImage::~ThumbnailImage() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (delegate_)
delegate_->thumbnail_ = nullptr;
}
ThumbnailImage::CaptureReadiness ThumbnailImage::GetCaptureReadiness() const {
return delegate_ ? delegate_->GetCaptureReadiness()
: CaptureReadiness::kNotReady;
}
std::unique_ptr<ThumbnailImage::Subscription> ThumbnailImage::Subscribe() {
// Use explicit new since Subscription constructor is private.
auto subscription =
base::WrapUnique(new Subscription(base::WrapRefCounted(this)));
subscribers_.insert(subscribers_.end(), subscription.get());
// Notify |delegate_| if this is the first subscriber.
if (subscribers_.size() == 1)
delegate_->ThumbnailImageBeingObservedChanged(true);
return subscription;
}
void ThumbnailImage::AssignSkBitmap(SkBitmap bitmap,
std::optional<uint64_t> frame_id) {
thumbnail_id_ = base::Token::CreateRandom();
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE,
{base::TaskPriority::USER_VISIBLE,
base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN},
base::BindOnce(&ThumbnailImage::CompressBitmap, std::move(bitmap),
frame_id),
base::BindOnce(&ThumbnailImage::AssignJPEGData,
weak_ptr_factory_.GetWeakPtr(), thumbnail_id_,
base::TimeTicks::Now(), frame_id));
}
void ThumbnailImage::ClearData() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!data_ && thumbnail_id_.is_zero())
return;
// If there was stored data we should notify observers that it was
// cleared. Otherwise, a bitmap was assigned but never compressed so
// the observers still think the thumbnail is blank.
const bool should_notify = !!data_;
data_.reset();
thumbnail_id_ = base::Token();
// Notify observers of the new, blank thumbnail.
if (should_notify) {
NotifyCompressedDataObservers(data_);
NotifyUncompressedDataObservers(thumbnail_id_, gfx::ImageSkia());
}
}
void ThumbnailImage::RequestThumbnailImage() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
ConvertJPEGDataToImageSkiaAndNotifyObservers();
}
void ThumbnailImage::RequestCompressedThumbnailData() {
if (data_)
NotifyCompressedDataObservers(data_);
}
size_t ThumbnailImage::GetCompressedDataSizeInBytes() const {
if (!data_)
return 0;
return data_->data.size();
}
void ThumbnailImage::AssignJPEGData(base::Token thumbnail_id,
base::TimeTicks assign_sk_bitmap_time,
std::optional<uint64_t> frame_id_for_trace,
std::vector<uint8_t> data) {
// If the image is stale (a new thumbnail was assigned or the
// thumbnail was cleared after AssignSkBitmap), ignore it.
if (thumbnail_id != thumbnail_id_) {
if (async_operation_finished_callback_)
async_operation_finished_callback_.Run();
return;
}
data_ = base::MakeRefCounted<base::RefCountedData<std::vector<uint8_t>>>(
std::move(data));
// We select a TRACE_EVENT_* macro based on |frame_id|'s presence.
// Since these are scoped traces, the macro invocation must be in the
// enclosing scope of these operations. Extract them into a common
// function.
auto notify = [&]() {
NotifyCompressedDataObservers(data_);
ConvertJPEGDataToImageSkiaAndNotifyObservers();
};
if (frame_id_for_trace) {
TRACE_EVENT_WITH_FLOW0("ui", "Tab.Preview.JPEGReceivedOnUIThreadWithFlow",
*frame_id_for_trace, TRACE_EVENT_FLAG_FLOW_IN);
notify();
} else {
TRACE_EVENT0("ui", "Tab.Preview.JPEGReceivedOnUIThread");
notify();
}
}
bool ThumbnailImage::ConvertJPEGDataToImageSkiaAndNotifyObservers() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!data_) {
if (async_operation_finished_callback_)
async_operation_finished_callback_.Run();
return false;
}
return base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE,
{base::TaskPriority::USER_VISIBLE,
base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN},
base::BindOnce(&ThumbnailImage::UncompressImage, data_),
base::BindOnce(&ThumbnailImage::NotifyUncompressedDataObservers,
weak_ptr_factory_.GetWeakPtr(), thumbnail_id_));
}
void ThumbnailImage::NotifyUncompressedDataObservers(base::Token thumbnail_id,
gfx::ImageSkia image) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (async_operation_finished_callback_)
async_operation_finished_callback_.Run();
// If the image is stale (a new thumbnail was assigned or the
// thumbnail was cleared after AssignSkBitmap), ignore it.
if (thumbnail_id != thumbnail_id_)
return;
for (Subscription* subscription : subscribers_) {
auto size_hint = subscription->size_hint_;
if (subscription->uncompressed_image_callback_) {
auto cropped_image = size_hint && !image.isNull()
? CropPreviewImage(image, *size_hint)
: image;
subscription->uncompressed_image_callback_.Run(cropped_image);
}
}
}
void ThumbnailImage::NotifyCompressedDataObservers(
CompressedThumbnailData data) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
for (Subscription* subscription : subscribers_) {
if (subscription->compressed_image_callback_)
subscription->compressed_image_callback_.Run(data);
}
}
// static
std::vector<uint8_t> ThumbnailImage::CompressBitmap(
SkBitmap bitmap,
std::optional<uint64_t> frame_id) {
constexpr int kCompressionQuality = 97;
std::vector<uint8_t> data;
// Similar to above, extract logic into function so we can select a
// TRACE_EVENT_* macro.
auto compress = [&]() {
const bool result =
gfx::JPEGCodec::Encode(bitmap, kCompressionQuality, &data);
DCHECK(result);
};
if (frame_id) {
TRACE_EVENT_WITH_FLOW0(
"ui", "Tab.Preview.CompressJPEGWithFlow", *frame_id,
TRACE_EVENT_FLAG_FLOW_IN | TRACE_EVENT_FLAG_FLOW_OUT);
compress();
} else {
TRACE_EVENT0("ui", "Tab.Preview.CompressJPEG");
compress();
}
return data;
}
// static
gfx::ImageSkia ThumbnailImage::UncompressImage(
CompressedThumbnailData compressed) {
gfx::ImageSkia result;
std::unique_ptr<SkBitmap> bitmap(
gfx::JPEGCodec::Decode(compressed->data.data(), compressed->data.size()));
if (bitmap.get()) {
result = gfx::ImageSkia::CreateFrom1xBitmap(*bitmap);
}
result.MakeThreadSafe();
return result;
}
// static
gfx::ImageSkia ThumbnailImage::CropPreviewImage(
const gfx::ImageSkia& source_image,
const gfx::Size& minimum_size) {
DCHECK(!source_image.isNull());
DCHECK(!source_image.size().IsEmpty());
DCHECK(!minimum_size.IsEmpty());
const float desired_aspect =
static_cast<float>(minimum_size.width()) / minimum_size.height();
const float source_aspect =
static_cast<float>(source_image.width()) / source_image.height();
if (source_aspect == desired_aspect ||
source_image.width() < minimum_size.width() ||
source_image.height() < minimum_size.height()) {
return source_image;
}
gfx::Rect clip_rect;
if (source_aspect > desired_aspect) {
// Wider than tall, clip horizontally: we center the smaller
// thumbnail in the wider screen.
const int new_width = source_image.height() * desired_aspect;
const int x_offset = (source_image.width() - new_width) / 2;
clip_rect = {x_offset, 0, new_width, source_image.height()};
} else {
// Taller than wide; clip vertically.
const int new_height = source_image.width() / desired_aspect;
clip_rect = {0, 0, source_image.width(), new_height};
}
SkBitmap cropped;
source_image.bitmap()->extractSubset(&cropped, gfx::RectToSkIRect(clip_rect));
return gfx::ImageSkia::CreateFrom1xBitmap(cropped);
}
void ThumbnailImage::HandleSubscriptionDestroyed(Subscription* subscription) {
// The order of |subscribers_| does not matter. We can simply swap
// |subscription| in |subscribers_| with the last element, then pop it
// off the back.
auto it = base::ranges::find(subscribers_, subscription);
DCHECK(it != subscribers_.end());
std::swap(*it, *(subscribers_.end() - 1));
subscribers_.pop_back();
// If that was the last subscriber, tell |delegate_| (if it still exists).
if (delegate_ && subscribers_.empty())
delegate_->ThumbnailImageBeingObservedChanged(false);
}