diff --git a/components/BUILD.gn b/components/BUILD.gn index e181889fb8483..a0026c9f2e1fe 100644 --- a/components/BUILD.gn +++ b/components/BUILD.gn @@ -454,6 +454,10 @@ test("components_unittests") { ] } + if (use_aura) { + deps += [ "//components/eye_dropper:unit_tests" ] + } + if (toolkit_views && enable_extensions) { deps += [ "//components/ui_devtools/views:unit_tests" ] } diff --git a/components/eye_dropper/BUILD.gn b/components/eye_dropper/BUILD.gn index 5fa8421d77b65..cade657de2948 100644 --- a/components/eye_dropper/BUILD.gn +++ b/components/eye_dropper/BUILD.gn @@ -38,3 +38,15 @@ source_set("color_mixer") { "//ui/color", ] } + +source_set("unit_tests") { + testonly = true + sources = [ "eye_dropper_view_unittest.cc" ] + deps = [ + ":eye_dropper", + "//base", + "//skia", + "//testing/gtest", + "//third_party/webrtc_overrides:webrtc_component", + ] +} diff --git a/components/eye_dropper/eye_dropper_view.cc b/components/eye_dropper/eye_dropper_view.cc index 1bff11db91509..f7d04ce2f0e56 100644 --- a/components/eye_dropper/eye_dropper_view.cc +++ b/components/eye_dropper/eye_dropper_view.cc @@ -6,7 +6,6 @@ #include -#include "base/compiler_specific.h" #include "base/memory/raw_ptr.h" #include "base/time/time.h" #include "base/timer/timer.h" @@ -157,10 +156,7 @@ void EyeDropperView::ScreenCapturer::OnCaptureResult( // TODO(crbug.com/352187279): Support other pixel formats. CHECK_EQ(frame->pixel_format(), webrtc::FOURCC_ARGB); - frame_.allocN32Pixels(frame->size().width(), frame->size().height(), true); - UNSAFE_TODO(memcpy(frame_.getAddr32(0, 0), frame->data(), - frame->size().height() * frame->stride())); - frame_.setImmutable(); + frame_ = CreateBitmapFromDesktopFrame(*frame); // The captured frame is in full desktop coordinates. E.g. the top left // monitor should start from (0, 0), so we need to compute the correct @@ -209,6 +205,20 @@ int EyeDropperView::ScreenCapturer::original_offset_y() const { return original_offset_y_; } +// static +SkBitmap EyeDropperView::CreateBitmapFromDesktopFrame( + const webrtc::DesktopFrame& frame) { + SkBitmap bitmap; + bitmap.allocN32Pixels(frame.size().width(), frame.size().height(), true); + // The captured frame may have a stride larger than width * bytes-per-pixel, + // so copy through a pixmap that carries the source stride. Reusing the + // destination's image info keeps this a plain row-by-row byte copy. + bitmap.writePixels(SkPixmap(bitmap.info(), frame.data(), + static_cast(frame.stride()))); + bitmap.setImmutable(); + return bitmap; +} + class EyeDropperContentsView : public views::View { METADATA_HEADER(EyeDropperContentsView, views::View) diff --git a/components/eye_dropper/eye_dropper_view.h b/components/eye_dropper/eye_dropper_view.h index c0fc8762337c1..ab9355a1db371 100644 --- a/components/eye_dropper/eye_dropper_view.h +++ b/components/eye_dropper/eye_dropper_view.h @@ -8,11 +8,13 @@ #include #include +#include "base/gtest_prod_util.h" #include "base/memory/raw_ptr.h" #include "base/time/time.h" #include "build/build_config.h" #include "content/public/browser/eye_dropper.h" #include "content/public/browser/eye_dropper_listener.h" +#include "third_party/skia/include/core/SkBitmap.h" #include "third_party/webrtc/modules/desktop_capture/desktop_capturer.h" #include "ui/display/screen.h" #include "ui/events/event_handler.h" @@ -75,6 +77,8 @@ class EyeDropperView : public content::EyeDropper, private: friend class EyeDropperContentsView; + FRIEND_TEST_ALL_PREFIXES(EyeDropperViewTest, + CreateBitmapFromDesktopFrameWithPaddedStride); class ViewPositionHandler; class ScreenCapturer; @@ -101,6 +105,11 @@ class EyeDropperView : public content::EyeDropper, #endif }; + // Copies the pixels of `frame` into a newly-allocated SkBitmap, taking the + // frame's stride into account. + static SkBitmap CreateBitmapFromDesktopFrame( + const webrtc::DesktopFrame& frame); + void CaptureScreen(std::optional screen); void OnScreenCaptured(); diff --git a/components/eye_dropper/eye_dropper_view_unittest.cc b/components/eye_dropper/eye_dropper_view_unittest.cc new file mode 100644 index 0000000000000..6f4e6a7284a63 --- /dev/null +++ b/components/eye_dropper/eye_dropper_view_unittest.cc @@ -0,0 +1,93 @@ +// Copyright 2026 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/eye_dropper/eye_dropper_view.h" + +#include + +#include +#include + +#include "base/containers/span.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/skia/include/core/SkBitmap.h" +#include "third_party/skia/include/core/SkColor.h" +#include "third_party/webrtc/modules/desktop_capture/desktop_frame.h" + +namespace eye_dropper { +namespace { + +// A DesktopFrame whose backing buffer has a stride larger than the minimum +// required for its width. +class PaddedDesktopFrame : public webrtc::DesktopFrame { + public: + static std::unique_ptr Create(webrtc::DesktopSize size, + int stride) { + std::vector buffer(static_cast(stride) * size.height()); + return std::make_unique(size, stride, + std::move(buffer)); + } + + PaddedDesktopFrame(webrtc::DesktopSize size, + int stride, + std::vector buffer) + : webrtc::DesktopFrame(size, + stride, + buffer.data(), + /*shared_memory=*/nullptr), + buffer_(std::move(buffer)) {} + + base::span pixel(int x, int y) { + return base::span(buffer_).subspan( + static_cast(y) * stride() + x * kBytesPerPixel, + static_cast(kBytesPerPixel)); + } + + private: + std::vector buffer_; +}; + +} // namespace + +using EyeDropperViewTest = testing::Test; + +TEST_F(EyeDropperViewTest, CreateBitmapFromDesktopFrameWithPaddedStride) { + constexpr int kWidth = 4; + constexpr int kHeight = 2; + // A stride that is larger than the minimum required for `kWidth`, as can be + // produced by capturers that align rows. + constexpr int kStride = kWidth * webrtc::DesktopFrame::kBytesPerPixel + 64; + + auto frame = + PaddedDesktopFrame::Create(webrtc::DesktopSize(kWidth, kHeight), kStride); + + // Fill the visible pixels with a known pattern; the padding bytes are left + // unspecified and must not affect the result. + for (int y = 0; y < kHeight; ++y) { + for (int x = 0; x < kWidth; ++x) { + base::span pixel = frame->pixel(x, y); + pixel[0] = static_cast(x); + pixel[1] = static_cast(y); + pixel[2] = 0; + pixel[3] = 0xff; + } + } + + SkBitmap bitmap = EyeDropperView::CreateBitmapFromDesktopFrame(*frame); + + ASSERT_EQ(bitmap.width(), kWidth); + ASSERT_EQ(bitmap.height(), kHeight); + for (int y = 0; y < kHeight; ++y) { + for (int x = 0; x < kWidth; ++x) { + SkColor color = bitmap.getColor(x, y); + EXPECT_EQ(SkColorGetA(color), 0xffu); + // The green component is at the same byte offset for every kN32 byte + // order, so it can be checked directly to verify that each destination + // row was populated from the matching source row. + EXPECT_EQ(SkColorGetG(color), static_cast(y)); + } + } +} + +} // namespace eye_dropper