| /* |
| * Copyright (C) 2006 Samuel Weinig (sam.weinig@gmail.com) |
| * Copyright (C) 2004, 2005, 2006 Apple Computer, Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY |
| * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR |
| * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
| * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
| * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
| * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY |
| * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "third_party/blink/renderer/platform/graphics/image.h" |
| |
| #include "base/numerics/checked_math.h" |
| #include "build/build_config.h" |
| #include "cc/tiles/software_image_decode_cache.h" |
| #include "third_party/blink/public/platform/platform.h" |
| #include "third_party/blink/public/platform/web_data.h" |
| #include "third_party/blink/renderer/platform/drag_image.h" |
| #include "third_party/blink/renderer/platform/geometry/float_point.h" |
| #include "third_party/blink/renderer/platform/geometry/float_rect.h" |
| #include "third_party/blink/renderer/platform/geometry/float_size.h" |
| #include "third_party/blink/renderer/platform/geometry/length.h" |
| #include "third_party/blink/renderer/platform/graphics/bitmap_image.h" |
| #include "third_party/blink/renderer/platform/graphics/deferred_image_decoder.h" |
| #include "third_party/blink/renderer/platform/graphics/graphics_context.h" |
| #include "third_party/blink/renderer/platform/graphics/paint/paint_image.h" |
| #include "third_party/blink/renderer/platform/graphics/paint/paint_recorder.h" |
| #include "third_party/blink/renderer/platform/graphics/paint/paint_shader.h" |
| #include "third_party/blink/renderer/platform/graphics/scoped_interpolation_quality.h" |
| #include "third_party/blink/renderer/platform/histogram.h" |
| #include "third_party/blink/renderer/platform/instrumentation/tracing/trace_event.h" |
| #include "third_party/blink/renderer/platform/shared_buffer.h" |
| #include "third_party/blink/renderer/platform/wtf/std_lib_extras.h" |
| #include "third_party/blink/renderer/platform/wtf/text/wtf_string.h" |
| #include "third_party/skia/include/core/SkImage.h" |
| |
| #include <math.h> |
| #include <tuple> |
| |
| namespace blink { |
| |
| class CombinedImageDecodeCache { |
| public: |
| CombinedImageDecodeCache(size_t locked_memory_limit_bytes) |
| : locked_memory_limit_bytes_(locked_memory_limit_bytes) { |
| constexpr int kMaxIndex = |
| (kMaxCanvasPixelFormat + 1) * (kMaxCanvasColorSpace + 1); |
| decode_caches_.resize(kMaxIndex); |
| } |
| |
| cc::ImageDecodeCache* GetCache(CanvasColorSpace color_space, |
| CanvasPixelFormat pixel_format) { |
| base::AutoLock lock(lock_); |
| int index = (kMaxCanvasColorSpace + 1) * pixel_format + color_space; |
| if (!decode_caches_[index]) { |
| decode_caches_[index] = std::make_unique<cc::SoftwareImageDecodeCache>( |
| CanvasColorParams::PixelFormatToSkColorType(pixel_format), |
| locked_memory_limit_bytes_, PaintImage::kDefaultGeneratorClientId, |
| blink::CanvasColorParams::CanvasColorSpaceToSkColorSpace( |
| color_space)); |
| } |
| return decode_caches_[index].get(); |
| } |
| |
| private: |
| std::vector<std::unique_ptr<cc::SoftwareImageDecodeCache>> decode_caches_; |
| const size_t locked_memory_limit_bytes_; |
| base::Lock lock_; |
| }; |
| |
| Image::Image(ImageObserver* observer, bool is_multipart) |
| : image_observer_disabled_(false), |
| image_observer_(observer), |
| stable_image_id_(PaintImage::GetNextId()), |
| is_multipart_(is_multipart), |
| high_contrast_classification_( |
| HighContrastClassification::kNotClassified) {} |
| |
| Image::~Image() = default; |
| |
| Image* Image::NullImage() { |
| DCHECK(IsMainThread()); |
| DEFINE_STATIC_REF(Image, null_image, (BitmapImage::Create())); |
| return null_image; |
| } |
| |
| // static |
| cc::ImageDecodeCache* Image::SharedCCDecodeCache( |
| CanvasColorSpace color_space, |
| CanvasPixelFormat pixel_format) { |
| // This denotes the allocated locked memory budget for the cache used for |
| // book-keeping. The cache indicates when the total memory locked exceeds this |
| // budget in cc::DecodedDrawImage. |
| static const size_t kLockedMemoryLimitBytes = 64 * 1024 * 1024; |
| DEFINE_THREAD_SAFE_STATIC_LOCAL(CombinedImageDecodeCache, combined_cache, |
| (kLockedMemoryLimitBytes)); |
| return combined_cache.GetCache(color_space, pixel_format); |
| } |
| |
| scoped_refptr<Image> Image::LoadPlatformResource(const char* name) { |
| const WebData& resource = Platform::Current()->GetDataResource(name); |
| if (resource.IsEmpty()) |
| return Image::NullImage(); |
| |
| scoped_refptr<Image> image = BitmapImage::Create(); |
| image->SetData(resource, true); |
| return image; |
| } |
| |
| Image::SizeAvailability Image::SetData(scoped_refptr<SharedBuffer> data, |
| bool all_data_received) { |
| encoded_image_data_ = std::move(data); |
| if (!encoded_image_data_.get()) |
| return kSizeAvailable; |
| |
| int length = encoded_image_data_->size(); |
| if (!length) |
| return kSizeAvailable; |
| |
| return DataChanged(all_data_received); |
| } |
| |
| String Image::FilenameExtension() const { |
| return String(); |
| } |
| |
| // TODO(schenney): Lift this code, with the calculations for subsetting the |
| // image and the like, up the stack into a BackgroundPainter. |
| void Image::DrawTiledBackground(GraphicsContext& ctxt, |
| const FloatSize& unsnapped_subset_size, |
| const FloatRect& snapped_paint_rect, |
| const FloatPoint& phase, |
| const FloatSize& tile_size, |
| SkBlendMode op, |
| const FloatSize& repeat_spacing) { |
| if (tile_size.IsEmpty()) |
| return; |
| |
| // Use the intrinsic size of the image if it has one, otherwise force the |
| // generated image to be the tile size. |
| FloatSize intrinsic_tile_size(Size()); |
| FloatSize scale(1, 1); |
| if (HasRelativeSize()) { |
| intrinsic_tile_size.SetWidth(tile_size.Width()); |
| intrinsic_tile_size.SetHeight(tile_size.Height()); |
| } else { |
| scale = FloatSize(tile_size.Width() / intrinsic_tile_size.Width(), |
| tile_size.Height() / intrinsic_tile_size.Height()); |
| } |
| |
| const FloatRect one_tile_rect = ComputePhaseForBackground( |
| snapped_paint_rect.Location(), tile_size, phase, repeat_spacing); |
| |
| // Check and see if a single draw of the image can cover the entire area we |
| // are supposed to tile. The dest_rect_for_subset must use the same |
| // location that was used in ComputePhaseForBackground and the unsnapped |
| // destination rect in order to correctly evaluate the subset size and |
| // location in the presence of border snapping and zoom. |
| FloatRect dest_rect_for_subset(snapped_paint_rect.Location(), |
| unsnapped_subset_size); |
| if (one_tile_rect.Contains(dest_rect_for_subset)) { |
| FloatRect visible_src_rect = ComputeSubsetForBackground( |
| one_tile_rect, dest_rect_for_subset, intrinsic_tile_size); |
| // Round to avoid filtering pulling in neighboring pixels, for the |
| // common case of sprite maps. |
| // TODO(schenney): Snapping at this level is a problem for cases where we |
| // might be animating background-position to pan over an image. Ideally we |
| // would either snap only if close to integral, or move snapping |
| // calculations up the stack. |
| visible_src_rect = FloatRect(RoundedIntRect(visible_src_rect)); |
| ctxt.DrawImage(this, kSyncDecode, snapped_paint_rect, &visible_src_rect, op, |
| kDoNotRespectImageOrientation); |
| return; |
| } |
| |
| // Note that this tile rect the image's pre-scaled size. |
| FloatRect tile_rect(FloatPoint(), intrinsic_tile_size); |
| // This call takes the unscaled image, applies the given scale, and paints |
| // it into the snapped_dest_rect using phase from one_tile_rect and the |
| // given repeat spacing. Note the phase is already scaled. |
| DrawPattern(ctxt, tile_rect, scale, one_tile_rect.Location(), op, |
| snapped_paint_rect, repeat_spacing); |
| |
| StartAnimation(); |
| } |
| |
| // TODO(schenney): Lift this code, with the calculations for subsetting the |
| // image and the like, up the stack into a border painting class. |
| void Image::DrawTiledBorder(GraphicsContext& ctxt, |
| const FloatRect& dst_rect, |
| const FloatRect& src_rect, |
| const FloatSize& provided_tile_scale_factor, |
| TileRule h_rule, |
| TileRule v_rule, |
| SkBlendMode op) { |
| // TODO(cavalcantii): see crbug.com/662513. |
| FloatSize tile_scale_factor = provided_tile_scale_factor; |
| if (v_rule == kRoundTile) { |
| float v_repetitions = std::max( |
| 1.0f, roundf(dst_rect.Height() / |
| (tile_scale_factor.Height() * src_rect.Height()))); |
| tile_scale_factor.SetHeight(dst_rect.Height() / |
| (src_rect.Height() * v_repetitions)); |
| } |
| |
| if (h_rule == kRoundTile) { |
| float h_repetitions = |
| std::max(1.0f, roundf(dst_rect.Width() / |
| (tile_scale_factor.Width() * src_rect.Width()))); |
| tile_scale_factor.SetWidth(dst_rect.Width() / |
| (src_rect.Width() * h_repetitions)); |
| } |
| |
| // We want to construct the phase such that the pattern is centered (when |
| // stretch is not set for a particular rule). |
| float v_phase = tile_scale_factor.Height() * src_rect.Y(); |
| float h_phase = tile_scale_factor.Width() * src_rect.X(); |
| if (v_rule == kRepeatTile) { |
| float scaled_tile_height = tile_scale_factor.Height() * src_rect.Height(); |
| v_phase -= (dst_rect.Height() - scaled_tile_height) / 2; |
| } |
| |
| if (h_rule == kRepeatTile) { |
| float scaled_tile_width = tile_scale_factor.Width() * src_rect.Width(); |
| h_phase -= (dst_rect.Width() - scaled_tile_width) / 2; |
| } |
| |
| FloatSize spacing; |
| auto calculate_space_needed = |
| [](const float destination, |
| const float source) -> std::tuple<bool, float> { |
| DCHECK_GT(source, 0); |
| DCHECK_GT(destination, 0); |
| |
| float repeat_tiles_count = floorf(destination / source); |
| if (!repeat_tiles_count) |
| return std::make_tuple(false, -1); |
| |
| float space = destination; |
| space -= source * repeat_tiles_count; |
| space /= repeat_tiles_count + 1.0; |
| |
| return std::make_tuple(true, space); |
| }; |
| |
| if (v_rule == kSpaceTile) { |
| std::tuple<bool, float> space = |
| calculate_space_needed(dst_rect.Height(), src_rect.Height()); |
| if (!std::get<0>(space)) |
| return; |
| |
| spacing.SetHeight(std::get<1>(space)); |
| tile_scale_factor.SetHeight(1.0); |
| v_phase = src_rect.Y(); |
| v_phase -= spacing.Height(); |
| } |
| |
| if (h_rule == kSpaceTile) { |
| std::tuple<bool, float> space = |
| calculate_space_needed(dst_rect.Width(), src_rect.Width()); |
| if (!std::get<0>(space)) |
| return; |
| |
| spacing.SetWidth(std::get<1>(space)); |
| tile_scale_factor.SetWidth(1.0); |
| h_phase = src_rect.X(); |
| h_phase -= spacing.Width(); |
| } |
| |
| FloatPoint pattern_phase(dst_rect.X() - h_phase, dst_rect.Y() - v_phase); |
| |
| // TODO(cavalcantii): see crbug.com/662507. |
| if ((h_rule == kRoundTile) || (v_rule == kRoundTile)) { |
| ScopedInterpolationQuality interpolation_quality_scope(ctxt, |
| kInterpolationLow); |
| DrawPattern(ctxt, src_rect, tile_scale_factor, pattern_phase, op, dst_rect, |
| FloatSize()); |
| } else { |
| DrawPattern(ctxt, src_rect, tile_scale_factor, pattern_phase, op, dst_rect, |
| spacing); |
| } |
| |
| StartAnimation(); |
| } |
| |
| namespace { |
| |
| sk_sp<PaintShader> CreatePatternShader(const PaintImage& image, |
| const SkMatrix& shader_matrix, |
| const PaintFlags& paint, |
| const FloatSize& spacing, |
| SkShader::TileMode tmx, |
| SkShader::TileMode tmy) { |
| if (spacing.IsZero()) { |
| return PaintShader::MakeImage(image, tmx, tmy, &shader_matrix); |
| } |
| |
| // Arbitrary tiling is currently only supported for SkPictureShader, so we use |
| // that instead of a plain bitmap shader to implement spacing. |
| const SkRect tile_rect = SkRect::MakeWH(image.width() + spacing.Width(), |
| image.height() + spacing.Height()); |
| |
| PaintRecorder recorder; |
| cc::PaintCanvas* canvas = recorder.beginRecording(tile_rect); |
| canvas->drawImage(image, 0, 0, &paint); |
| |
| return PaintShader::MakePaintRecord(recorder.finishRecordingAsPicture(), |
| tile_rect, tmx, tmy, &shader_matrix); |
| } |
| |
| SkShader::TileMode ComputeTileMode(float left, |
| float right, |
| float min, |
| float max) { |
| DCHECK(left < right); |
| return left >= min && right <= max ? SkShader::kClamp_TileMode |
| : SkShader::kRepeat_TileMode; |
| } |
| |
| } // anonymous namespace |
| |
| void Image::DrawPattern(GraphicsContext& context, |
| const FloatRect& float_src_rect, |
| const FloatSize& scale_src_to_dest, |
| const FloatPoint& phase, |
| SkBlendMode composite_op, |
| const FloatRect& dest_rect, |
| const FloatSize& repeat_spacing) { |
| TRACE_EVENT0("skia", "Image::drawPattern"); |
| |
| if (dest_rect.IsEmpty()) |
| return; // nothing to draw |
| |
| PaintImage image = PaintImageForCurrentFrame(); |
| if (!image) |
| return; // nothing to draw |
| |
| // The subset_rect is in source image space, unscaled. |
| IntRect subset_rect = EnclosingIntRect(float_src_rect); |
| subset_rect.Intersect(IntRect(0, 0, image.width(), image.height())); |
| if (subset_rect.IsEmpty()) |
| return; // nothing to draw |
| |
| SkMatrix local_matrix; |
| // We also need to translate it such that the origin of the pattern is the |
| // origin of the destination rect, which is what Blink expects. Skia uses |
| // the coordinate system origin as the base for the pattern. If Blink wants |
| // a shifted image, it will shift it from there using the localMatrix. |
| const float adjusted_x = |
| phase.X() + subset_rect.X() * scale_src_to_dest.Width(); |
| const float adjusted_y = |
| phase.Y() + subset_rect.Y() * scale_src_to_dest.Height(); |
| local_matrix.setTranslate(SkFloatToScalar(adjusted_x), |
| SkFloatToScalar(adjusted_y)); |
| |
| // Apply the scale to have the subset correctly fill the destination. |
| local_matrix.preScale(scale_src_to_dest.Width(), scale_src_to_dest.Height()); |
| |
| // Fetch this now as subsetting may swap the image. |
| auto image_id = image.GetSkImage()->uniqueID(); |
| |
| image = PaintImageBuilder::WithCopy(std::move(image)) |
| .make_subset(subset_rect) |
| .TakePaintImage(); |
| if (!image) |
| return; |
| |
| const FloatSize tile_size( |
| image.width() * scale_src_to_dest.Width() + repeat_spacing.Width(), |
| image.height() * scale_src_to_dest.Height() + repeat_spacing.Height()); |
| const auto tmx = ComputeTileMode(dest_rect.X(), dest_rect.MaxX(), adjusted_x, |
| adjusted_x + tile_size.Width()); |
| const auto tmy = ComputeTileMode(dest_rect.Y(), dest_rect.MaxY(), adjusted_y, |
| adjusted_y + tile_size.Height()); |
| |
| PaintFlags flags = context.FillFlags(); |
| flags.setColor(SK_ColorBLACK); |
| flags.setBlendMode(composite_op); |
| flags.setFilterQuality( |
| context.ComputeFilterQuality(this, dest_rect, FloatRect(subset_rect))); |
| flags.setAntiAlias(context.ShouldAntialias()); |
| flags.setShader(CreatePatternShader( |
| image, local_matrix, flags, |
| FloatSize(repeat_spacing.Width() / scale_src_to_dest.Width(), |
| repeat_spacing.Height() / scale_src_to_dest.Height()), |
| tmx, tmy)); |
| // If the shader could not be instantiated (e.g. non-invertible matrix), |
| // draw transparent. |
| // Note: we can't simply bail, because of arbitrary blend mode. |
| if (!flags.HasShader()) |
| flags.setColor(SK_ColorTRANSPARENT); |
| |
| context.DrawRect(dest_rect, flags); |
| |
| if (CurrentFrameIsLazyDecoded()) { |
| TRACE_EVENT_INSTANT1(TRACE_DISABLED_BY_DEFAULT("devtools.timeline"), |
| "Draw LazyPixelRef", TRACE_EVENT_SCOPE_THREAD, |
| "LazyPixelRef", image_id); |
| } |
| } |
| |
| scoped_refptr<Image> Image::ImageForDefaultFrame() { |
| scoped_refptr<Image> image(this); |
| |
| return image; |
| } |
| |
| PaintImageBuilder Image::CreatePaintImageBuilder() { |
| auto animation_type = MaybeAnimated() ? PaintImage::AnimationType::ANIMATED |
| : PaintImage::AnimationType::STATIC; |
| return PaintImageBuilder::WithDefault() |
| .set_id(stable_image_id_) |
| .set_animation_type(animation_type) |
| .set_is_multipart(is_multipart_); |
| } |
| |
| bool Image::ApplyShader(PaintFlags& flags, const SkMatrix& local_matrix) { |
| // Default shader impl: attempt to build a shader based on the current frame |
| // SkImage. |
| PaintImage image = PaintImageForCurrentFrame(); |
| if (!image) |
| return false; |
| |
| flags.setShader(PaintShader::MakeImage(image, SkShader::kRepeat_TileMode, |
| SkShader::kRepeat_TileMode, |
| &local_matrix)); |
| if (!flags.HasShader()) |
| return false; |
| |
| // Animation is normally refreshed in draw() impls, which we don't call when |
| // painting via shaders. |
| StartAnimation(); |
| |
| return true; |
| } |
| |
| FloatRect Image::ComputePhaseForBackground(const FloatPoint& destination_offset, |
| const FloatSize& size, |
| const FloatPoint& phase, |
| const FloatSize& spacing) { |
| const FloatSize step_per_tile(size + spacing); |
| return FloatRect( |
| FloatPoint( |
| destination_offset.X() + fmodf(-phase.X(), step_per_tile.Width()), |
| destination_offset.Y() + fmodf(-phase.Y(), step_per_tile.Height())), |
| size); |
| } |
| |
| FloatRect Image::ComputeSubsetForBackground(const FloatRect& phase_and_size, |
| const FloatRect& subset, |
| const FloatSize& intrinsic_size) { |
| // TODO(schenney): Re-enable this after determining why it fails for |
| // CAP, and maybe other cases. |
| // DCHECK(phase_and_size.Contains(subset)); |
| |
| const FloatSize scale(phase_and_size.Width() / intrinsic_size.Width(), |
| phase_and_size.Height() / intrinsic_size.Height()); |
| return FloatRect((subset.X() - phase_and_size.X()) / scale.Width(), |
| (subset.Y() - phase_and_size.Y()) / scale.Height(), |
| subset.Width() / scale.Width(), |
| subset.Height() / scale.Height()); |
| } |
| |
| SkBitmap Image::AsSkBitmapForCurrentFrame( |
| RespectImageOrientationEnum should_respect_image_orientation) { |
| PaintImage paint_image = PaintImageForCurrentFrame(); |
| if (!paint_image) |
| return {}; |
| |
| if (should_respect_image_orientation == kRespectImageOrientation && |
| IsBitmapImage()) { |
| ImageOrientation orientation = |
| ToBitmapImage(this)->CurrentFrameOrientation(); |
| paint_image = DragImage::ResizeAndOrientImage(paint_image, orientation); |
| if (!paint_image) |
| return {}; |
| } |
| |
| sk_sp<SkImage> sk_image = paint_image.GetSkImage(); |
| if (!sk_image) |
| return {}; |
| |
| SkBitmap bitmap; |
| sk_image->asLegacyBitmap(&bitmap); |
| return bitmap; |
| } |
| |
| } // namespace blink |