blob: fc0c52a85b66cd3ae435a548e75fedc494507c52 [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/core/loader/resource/image_resource_content.h"
#include <memory>
#include "base/metrics/histogram_macros.h"
#include "third_party/blink/public/common/feature_policy/policy_value.h"
#include "third_party/blink/public/mojom/feature_policy/document_policy_feature.mojom-blink.h"
#include "third_party/blink/public/mojom/feature_policy/feature_policy_feature.mojom-blink.h"
#include "third_party/blink/public/mojom/feature_policy/policy_value.mojom-blink.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/core/loader/resource/image_resource.h"
#include "third_party/blink/renderer/core/loader/resource/image_resource_info.h"
#include "third_party/blink/renderer/core/loader/resource/image_resource_observer.h"
#include "third_party/blink/renderer/core/svg/graphics/svg_image.h"
#include "third_party/blink/renderer/platform/geometry/int_size.h"
#include "third_party/blink/renderer/platform/graphics/bitmap_image.h"
#include "third_party/blink/renderer/platform/instrumentation/tracing/trace_event.h"
#include "third_party/blink/renderer/platform/network/http_parsers.h"
#include "third_party/blink/renderer/platform/network/mime/mime_type_registry.h"
#include "third_party/blink/renderer/platform/wtf/shared_buffer.h"
#include "third_party/blink/renderer/platform/wtf/std_lib_extras.h"
#include "third_party/blink/renderer/platform/wtf/vector.h"
#include "v8/include/v8.h"
namespace blink {
namespace {
class NullImageResourceInfo final
: public GarbageCollected<NullImageResourceInfo>,
public ImageResourceInfo {
public:
NullImageResourceInfo() = default;
void Trace(Visitor* visitor) const override {
ImageResourceInfo::Trace(visitor);
}
private:
const KURL& Url() const override { return url_; }
base::TimeTicks LoadResponseEnd() const override { return base::TimeTicks(); }
const ResourceResponse& GetResponse() const override { return response_; }
bool IsCacheValidator() const override { return false; }
bool IsAccessAllowed(
DoesCurrentFrameHaveSingleSecurityOrigin) const override {
return true;
}
bool HasCacheControlNoStoreHeader() const override { return false; }
base::Optional<ResourceError> GetResourceError() const override {
return base::nullopt;
}
void SetDecodedSize(size_t) override {}
void WillAddClientOrObserver() override {}
void DidRemoveClientOrObserver() override {}
void EmulateLoadStartedForInspector(
ResourceFetcher*,
const KURL&,
const AtomicString& initiator_name) override {}
void LoadDeferredImage(ResourceFetcher* fetcher) override {}
bool IsAdResource() const override { return false; }
const HashSet<String>* GetUnsupportedImageMimeTypes() const override {
return nullptr;
}
const KURL url_;
const ResourceResponse response_;
};
} // namespace
ImageResourceContent::ImageResourceContent(scoped_refptr<blink::Image> image)
: is_refetchable_data_from_disk_cache_(true),
device_pixel_ratio_header_value_(1.0),
has_device_pixel_ratio_header_value_(false),
image_(std::move(image)) {
DEFINE_STATIC_LOCAL(Persistent<NullImageResourceInfo>, null_info,
(MakeGarbageCollected<NullImageResourceInfo>()));
info_ = null_info;
}
ImageResourceContent* ImageResourceContent::CreateLoaded(
scoped_refptr<blink::Image> image) {
DCHECK(image);
ImageResourceContent* content =
MakeGarbageCollected<ImageResourceContent>(std::move(image));
content->content_status_ = ResourceStatus::kCached;
return content;
}
ImageResourceContent* ImageResourceContent::Fetch(FetchParameters& params,
ResourceFetcher* fetcher) {
// TODO(hiroshige): Remove direct references to ImageResource by making
// the dependencies around ImageResource and ImageResourceContent cleaner.
ImageResource* resource = ImageResource::Fetch(params, fetcher);
if (!resource)
return nullptr;
return resource->GetContent();
}
void ImageResourceContent::SetImageResourceInfo(ImageResourceInfo* info) {
info_ = info;
}
void ImageResourceContent::Trace(Visitor* visitor) const {
visitor->Trace(info_);
ImageObserver::Trace(visitor);
}
void ImageResourceContent::HandleObserverFinished(
ImageResourceObserver* observer) {
{
ProhibitAddRemoveObserverInScope prohibit_add_remove_observer_in_scope(
this);
auto it = observers_.find(observer);
if (it != observers_.end()) {
observers_.erase(it);
finished_observers_.insert(observer);
}
}
observer->ImageNotifyFinished(this);
UpdateImageAnimationPolicy();
}
void ImageResourceContent::AddObserver(ImageResourceObserver* observer) {
CHECK(!is_add_remove_observer_prohibited_);
info_->WillAddClientOrObserver();
{
ProhibitAddRemoveObserverInScope prohibit_add_remove_observer_in_scope(
this);
observers_.insert(observer);
}
if (info_->IsCacheValidator())
return;
if (image_ && !image_->IsNull()) {
observer->ImageChanged(this, CanDeferInvalidation::kNo);
}
if (IsLoaded() && observers_.Contains(observer))
HandleObserverFinished(observer);
}
void ImageResourceContent::RemoveObserver(ImageResourceObserver* observer) {
DCHECK(observer);
CHECK(!is_add_remove_observer_prohibited_);
ProhibitAddRemoveObserverInScope prohibit_add_remove_observer_in_scope(this);
auto it = observers_.find(observer);
bool fully_erased;
if (it != observers_.end()) {
fully_erased = observers_.erase(it) && finished_observers_.find(observer) ==
finished_observers_.end();
} else {
it = finished_observers_.find(observer);
DCHECK(it != finished_observers_.end());
fully_erased = finished_observers_.erase(it);
}
info_->DidRemoveClientOrObserver();
if (fully_erased)
observer->NotifyImageFullyRemoved(this);
}
static void PriorityFromObserver(const ImageResourceObserver* observer,
ResourcePriority& priority) {
ResourcePriority next_priority = observer->ComputeResourcePriority();
if (next_priority.visibility == ResourcePriority::kNotVisible)
return;
priority.visibility = ResourcePriority::kVisible;
priority.intra_priority_value += next_priority.intra_priority_value;
}
ResourcePriority ImageResourceContent::PriorityFromObservers() const {
ProhibitAddRemoveObserverInScope prohibit_add_remove_observer_in_scope(this);
ResourcePriority priority;
for (const auto& it : finished_observers_)
PriorityFromObserver(it.key, priority);
for (const auto& it : observers_)
PriorityFromObserver(it.key, priority);
return priority;
}
void ImageResourceContent::DestroyDecodedData() {
if (!image_)
return;
CHECK(!ErrorOccurred());
image_->DestroyDecodedData();
}
void ImageResourceContent::DoResetAnimation() {
if (image_)
image_->ResetAnimation();
}
scoped_refptr<const SharedBuffer> ImageResourceContent::ResourceBuffer() const {
if (image_)
return image_->Data();
return nullptr;
}
bool ImageResourceContent::ShouldUpdateImageImmediately() const {
// If we don't have the size available yet, then update immediately since
// we need to know the image size as soon as possible. Likewise for
// animated images, update right away since we shouldn't throttle animated
// images.
return size_available_ == Image::kSizeUnavailable ||
(image_ && image_->MaybeAnimated());
}
blink::Image* ImageResourceContent::GetImage() const {
if (!image_ || ErrorOccurred())
return Image::NullImage();
return image_.get();
}
IntSize ImageResourceContent::IntrinsicSize(
RespectImageOrientationEnum should_respect_image_orientation) const {
if (!image_)
return IntSize();
RespectImageOrientationEnum respect_orientation =
ForceOrientationIfNecessary(should_respect_image_orientation);
return image_->Size(respect_orientation);
}
RespectImageOrientationEnum ImageResourceContent::ForceOrientationIfNecessary(
RespectImageOrientationEnum default_orientation) const {
if (image_ && image_->IsBitmapImage() && !IsAccessAllowed())
return kRespectImageOrientation;
return default_orientation;
}
void ImageResourceContent::NotifyObservers(
NotifyFinishOption notifying_finish_option,
CanDeferInvalidation defer) {
{
Vector<ImageResourceObserver*> finished_observers_as_vector;
{
ProhibitAddRemoveObserverInScope prohibit_add_remove_observer_in_scope(
this);
finished_observers_as_vector = finished_observers_.AsVector();
}
for (auto* observer : finished_observers_as_vector) {
if (finished_observers_.Contains(observer))
observer->ImageChanged(this, defer);
}
}
{
Vector<ImageResourceObserver*> observers_as_vector;
{
ProhibitAddRemoveObserverInScope prohibit_add_remove_observer_in_scope(
this);
observers_as_vector = observers_.AsVector();
}
for (auto* observer : observers_as_vector) {
if (observers_.Contains(observer)) {
observer->ImageChanged(this, defer);
if (notifying_finish_option == kShouldNotifyFinish &&
observers_.Contains(observer)) {
HandleObserverFinished(observer);
}
}
}
}
}
scoped_refptr<Image> ImageResourceContent::CreateImage(bool is_multipart) {
String content_dpr_value =
info_->GetResponse().HttpHeaderField(http_names::kContentDPR);
wtf_size_t comma = content_dpr_value.ReverseFind(',');
if (comma != kNotFound && comma < content_dpr_value.length() - 1) {
content_dpr_value = content_dpr_value.Substring(comma + 1);
}
device_pixel_ratio_header_value_ =
content_dpr_value.ToFloat(&has_device_pixel_ratio_header_value_);
if (!has_device_pixel_ratio_header_value_ ||
device_pixel_ratio_header_value_ <= 0.0) {
device_pixel_ratio_header_value_ = 1.0;
has_device_pixel_ratio_header_value_ = false;
}
if (info_->GetResponse().MimeType() == "image/svg+xml")
return SVGImage::Create(this, is_multipart);
return BitmapImage::Create(this, is_multipart);
}
void ImageResourceContent::ClearImage() {
if (!image_)
return;
int64_t length = image_->Data() ? image_->Data()->size() : 0;
v8::Isolate::GetCurrent()->AdjustAmountOfExternalAllocatedMemory(-length);
// If our Image has an observer, it's always us so we need to clear the back
// pointer before dropping our reference.
image_->ClearImageObserver();
image_ = nullptr;
size_available_ = Image::kSizeUnavailable;
}
// |new_status| is the status of corresponding ImageResource.
void ImageResourceContent::UpdateToLoadedContentStatus(
ResourceStatus new_status) {
// When |ShouldNotifyFinish|, we set content_status_
// to a loaded ResourceStatus.
// Checks |new_status| (i.e. Resource's current status).
switch (new_status) {
case ResourceStatus::kCached:
case ResourceStatus::kPending:
// In case of successful load, Resource's status can be
// kCached (e.g. for second part of multipart image) or
// still Pending (e.g. for a non-multipart image).
// Therefore we use kCached as the new state here.
new_status = ResourceStatus::kCached;
break;
case ResourceStatus::kLoadError:
case ResourceStatus::kDecodeError:
// In case of error, Resource's status is set to an error status
// before UpdateImage() and thus we use the error status as-is.
break;
case ResourceStatus::kNotStarted:
CHECK(false);
break;
}
// Updates the status.
content_status_ = new_status;
}
void ImageResourceContent::NotifyStartLoad() {
// Checks ImageResourceContent's previous status.
switch (GetContentStatus()) {
case ResourceStatus::kPending:
CHECK(false);
break;
case ResourceStatus::kNotStarted:
// Normal load start.
break;
case ResourceStatus::kCached:
case ResourceStatus::kLoadError:
case ResourceStatus::kDecodeError:
// Load start due to revalidation/reload.
break;
}
content_status_ = ResourceStatus::kPending;
}
void ImageResourceContent::AsyncLoadCompleted(const blink::Image* image) {
if (image_ != image)
return;
CHECK_EQ(size_available_, Image::kSizeAvailableAndLoadingAsynchronously);
size_available_ = Image::kSizeAvailable;
UpdateToLoadedContentStatus(ResourceStatus::kCached);
NotifyObservers(kShouldNotifyFinish, CanDeferInvalidation::kNo);
}
ImageResourceContent::UpdateImageResult ImageResourceContent::UpdateImage(
scoped_refptr<SharedBuffer> data,
ResourceStatus status,
UpdateImageOption update_image_option,
bool all_data_received,
bool is_multipart) {
TRACE_EVENT0("blink", "ImageResourceContent::updateImage");
#if DCHECK_IS_ON()
DCHECK(!is_update_image_being_called_);
base::AutoReset<bool> scope(&is_update_image_being_called_, true);
#endif
// Clears the existing image, if instructed by |updateImageOption|.
switch (update_image_option) {
case kClearAndUpdateImage:
case kClearImageAndNotifyObservers:
ClearImage();
break;
case kUpdateImage:
break;
}
// Updates the image, if instructed by |updateImageOption|.
switch (update_image_option) {
case kClearImageAndNotifyObservers:
DCHECK(!data);
break;
case kUpdateImage:
case kClearAndUpdateImage:
// Have the image update its data from its internal buffer. It will not do
// anything now, but will delay decoding until queried for info (like size
// or specific image frames).
if (data) {
if (!image_)
image_ = CreateImage(is_multipart);
DCHECK(image_);
size_available_ = image_->SetData(std::move(data), all_data_received);
DCHECK(all_data_received ||
size_available_ !=
Image::kSizeAvailableAndLoadingAsynchronously);
}
// Go ahead and tell our observers to try to draw if we have either
// received all the data or the size is known. Each chunk from the network
// causes observers to repaint, which will force that chunk to decode.
if (size_available_ == Image::kSizeUnavailable && !all_data_received)
return UpdateImageResult::kNoDecodeError;
if (image_ && info_->GetUnsupportedImageMimeTypes()) {
// Filename extension is set by the image decoder based on the actual
// image content.
String file_extension = image_->FilenameExtension();
if (info_->GetUnsupportedImageMimeTypes()->Contains(
String("image/" + file_extension))) {
return UpdateImageResult::kShouldDecodeError;
}
}
// As per spec, zero intrinsic size SVG is a valid image so do not
// consider such an image as DecodeError.
// https://www.w3.org/TR/SVG/struct.html#SVGElementWidthAttribute
if (!image_ ||
(image_->IsNull() && (!IsA<SVGImage>(image_.get()) ||
size_available_ == Image::kSizeUnavailable))) {
ClearImage();
return UpdateImageResult::kShouldDecodeError;
}
break;
}
DCHECK(all_data_received ||
size_available_ != Image::kSizeAvailableAndLoadingAsynchronously);
// Notifies the observers.
// It would be nice to only redraw the decoded band of the image, but with the
// current design (decoding delayed until painting) that seems hard.
//
// In the case of kSizeAvailableAndLoadingAsynchronously, we are waiting for
// SVG image completion, and thus we notify observers of kDoNotNotifyFinish
// here, and will notify observers of finish later in AsyncLoadCompleted().
//
// Don't allow defering of invalidation if it resulted from a data update.
// This is necessary to ensure that all PaintImages in a recording committed
// to the compositor have the same data.
if (all_data_received &&
size_available_ != Image::kSizeAvailableAndLoadingAsynchronously) {
UpdateToLoadedContentStatus(status);
NotifyObservers(kShouldNotifyFinish, CanDeferInvalidation::kNo);
} else {
NotifyObservers(kDoNotNotifyFinish, CanDeferInvalidation::kNo);
}
return UpdateImageResult::kNoDecodeError;
}
ImageDecoder::CompressionFormat ImageResourceContent::GetCompressionFormat()
const {
if (!image_)
return ImageDecoder::kUndefinedFormat;
return ImageDecoder::GetCompressionFormat(image_->Data(),
GetResponse().HttpContentType());
}
bool ImageResourceContent::IsAcceptableCompressionRatio(
ExecutionContext& context) {
if (!image_)
return true;
uint64_t pixels = image_->Size().Area();
if (!pixels)
return true;
double resource_length =
static_cast<double>(GetResponse().ExpectedContentLength());
if (resource_length <= 0 && image_->Data()) {
// WPT and LayoutTests server returns -1 or 0 for the content length.
resource_length = static_cast<double>(image_->Data()->size());
}
// Calculate the image's compression ratio (in bytes per pixel) with both 1k
// and 10k overhead. The constant overhead allowance is provided to allow room
// for headers and to account for small images (which are harder to compress).
double compression_ratio_1k = (resource_length - 1024) / pixels;
double compression_ratio_10k = (resource_length - 10240) / pixels;
ImageDecoder::CompressionFormat compression_format = GetCompressionFormat();
// Pass image url to reporting API.
const String& image_url = Url().GetString();
const char* message_format =
"Image bpp (byte per pixel) exceeds max value set in %s.";
if (compression_format == ImageDecoder::kLossyFormat) {
// Enforce the lossy image policy.
return context.IsFeatureEnabled(
mojom::blink::DocumentPolicyFeature::kLossyImagesMaxBpp,
PolicyValue::CreateDecDouble(compression_ratio_1k),
ReportOptions::kReportOnFailure,
String::Format(message_format, "lossy-images-max-bpp"), image_url);
}
if (compression_format == ImageDecoder::kLosslessFormat) {
// Enforce the lossless image policy.
bool enabled_by_10k_policy = context.IsFeatureEnabled(
mojom::blink::DocumentPolicyFeature::kLosslessImagesMaxBpp,
PolicyValue::CreateDecDouble(compression_ratio_10k),
ReportOptions::kReportOnFailure,
String::Format(message_format, "lossless-images-max-bpp"), image_url);
bool enabled_by_1k_policy = context.IsFeatureEnabled(
mojom::blink::DocumentPolicyFeature::kLosslessImagesStrictMaxBpp,
PolicyValue::CreateDecDouble(compression_ratio_1k),
ReportOptions::kReportOnFailure,
String::Format(message_format, "lossless-images-strict-max-bpp"),
image_url);
return enabled_by_10k_policy && enabled_by_1k_policy;
}
return true;
}
void ImageResourceContent::DecodedSizeChangedTo(const blink::Image* image,
size_t new_size) {
if (!image || image != image_)
return;
info_->SetDecodedSize(new_size);
}
bool ImageResourceContent::ShouldPauseAnimation(const blink::Image* image) {
if (!image || image != image_)
return false;
ProhibitAddRemoveObserverInScope prohibit_add_remove_observer_in_scope(this);
for (const auto& it : finished_observers_) {
if (it.key->WillRenderImage())
return false;
}
for (const auto& it : observers_) {
if (it.key->WillRenderImage())
return false;
}
return true;
}
void ImageResourceContent::UpdateImageAnimationPolicy() {
if (!image_)
return;
mojom::blink::ImageAnimationPolicy new_policy =
mojom::blink::ImageAnimationPolicy::kImageAnimationPolicyAllowed;
{
ProhibitAddRemoveObserverInScope prohibit_add_remove_observer_in_scope(
this);
for (const auto& it : finished_observers_) {
if (it.key->GetImageAnimationPolicy(new_policy))
break;
}
for (const auto& it : observers_) {
if (it.key->GetImageAnimationPolicy(new_policy))
break;
}
}
image_->SetAnimationPolicy(new_policy);
}
void ImageResourceContent::Changed(const blink::Image* image) {
if (!image || image != image_)
return;
NotifyObservers(kDoNotNotifyFinish, CanDeferInvalidation::kYes);
}
bool ImageResourceContent::IsAccessAllowed() const {
return info_->IsAccessAllowed(
GetImage()->CurrentFrameHasSingleSecurityOrigin()
? ImageResourceInfo::kHasSingleSecurityOrigin
: ImageResourceInfo::kHasMultipleSecurityOrigin);
}
void ImageResourceContent::EmulateLoadStartedForInspector(
ResourceFetcher* fetcher,
const KURL& url,
const AtomicString& initiator_name) {
info_->EmulateLoadStartedForInspector(fetcher, url, initiator_name);
}
bool ImageResourceContent::IsLoaded() const {
return GetContentStatus() > ResourceStatus::kPending;
}
bool ImageResourceContent::IsLoading() const {
return GetContentStatus() == ResourceStatus::kPending;
}
bool ImageResourceContent::ErrorOccurred() const {
return GetContentStatus() == ResourceStatus::kLoadError ||
GetContentStatus() == ResourceStatus::kDecodeError;
}
bool ImageResourceContent::LoadFailedOrCanceled() const {
return GetContentStatus() == ResourceStatus::kLoadError;
}
ResourceStatus ImageResourceContent::GetContentStatus() const {
return content_status_;
}
// TODO(hiroshige): Consider removing the following methods, or stoping
// redirecting to ImageResource.
const KURL& ImageResourceContent::Url() const {
return info_->Url();
}
base::TimeTicks ImageResourceContent::LoadResponseEnd() const {
return info_->LoadResponseEnd();
}
bool ImageResourceContent::HasCacheControlNoStoreHeader() const {
return info_->HasCacheControlNoStoreHeader();
}
float ImageResourceContent::DevicePixelRatioHeaderValue() const {
return device_pixel_ratio_header_value_;
}
bool ImageResourceContent::HasDevicePixelRatioHeaderValue() const {
return has_device_pixel_ratio_header_value_;
}
const ResourceResponse& ImageResourceContent::GetResponse() const {
return info_->GetResponse();
}
base::Optional<ResourceError> ImageResourceContent::GetResourceError() const {
return info_->GetResourceError();
}
bool ImageResourceContent::IsCacheValidator() const {
return info_->IsCacheValidator();
}
void ImageResourceContent::LoadDeferredImage(ResourceFetcher* fetcher) {
info_->LoadDeferredImage(fetcher);
}
bool ImageResourceContent::IsAdResource() const {
return info_->IsAdResource();
}
void ImageResourceContent::RecordDecodedImageType(UseCounter* use_counter) {
if (auto* bitmap_image = DynamicTo<BitmapImage>(image_.get()))
bitmap_image->RecordDecodedImageType(use_counter);
}
} // namespace blink