[go: nahoru, domu]

[Paint Preview] Blink Implementation of Paint Preview

This CL implements the required code to capture paint previews in Blink.
The majority of the code is shared with printing; however, new flags are
introduced to handle the differences between printing and painting of
previews. This CL *does not* introduce support for OOP iframe paint
previews and scrolling them (to be handled in a separate CL).

Desired Behaviors;
- Paint Previews should look exactly like the current webpage and should
  not reformat in the way printing does. This includes ignoring print
  display tags and the simplifications used in painting.
- Paint Previews should re-layout to be the full contents of the page
  not just the viewport. This is achieved by printing to a canvas the
  size of the document (single page).
- Paint Previews should print to a single Skia Picture (per frame)
  rather than a Metafile Skia Document.

Design doc: go/fdt-design
Part of landing:
https://chromium-review.googlesource.com/c/chromium/src/+/1786583

Bug: 1001109
Change-Id: If0e947befea8ecc78b2cbee950e8c1cbd8e1281e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1844057
Reviewed-by: Elly Fong-Jones <ellyjones@chromium.org>
Reviewed-by: Philip Rogers <pdr@chromium.org>
Commit-Queue: Calder Kitagawa <ckitagawa@chromium.org>
Cr-Commit-Position: refs/heads/master@{#713822}
diff --git a/cc/paint/paint_canvas.h b/cc/paint/paint_canvas.h
index 32918efd..31a736d 100644
--- a/cc/paint/paint_canvas.h
+++ b/cc/paint/paint_canvas.h
@@ -18,6 +18,10 @@
 class MetafileSkia;
 }  // namespace printing
 
+namespace paint_preview {
+class PaintPreviewTracker;
+}  // namespace paint_preview
+
 namespace cc {
 class SkottieWrapper;
 class PaintFlags;
@@ -191,12 +195,19 @@
   void SetPrintingMetafile(printing::MetafileSkia* metafile) {
     metafile_ = metafile;
   }
+  paint_preview::PaintPreviewTracker* GetPaintPreviewTracker() const {
+    return tracker_;
+  }
+  void SetPaintPreviewTracker(paint_preview::PaintPreviewTracker* tracker) {
+    tracker_ = tracker;
+  }
 
   // Subclasses can override to handle custom data.
   virtual void recordCustomData(uint32_t id) {}
 
  private:
   printing::MetafileSkia* metafile_ = nullptr;
+  paint_preview::PaintPreviewTracker* tracker_ = nullptr;
 };
 
 class CC_PAINT_EXPORT PaintCanvasAutoRestore {
diff --git a/components/BUILD.gn b/components/BUILD.gn
index 7e247c6..800f4a7c 100644
--- a/components/BUILD.gn
+++ b/components/BUILD.gn
@@ -568,6 +568,7 @@
       "//testing/gmock",
       "//testing/gtest",
       "//ui/base",
+      "//ui/native_theme:native_theme",
       "//url",
     ]
 
diff --git a/components/paint_preview/renderer/DEPS b/components/paint_preview/renderer/DEPS
index 53de874..497159a7 100644
--- a/components/paint_preview/renderer/DEPS
+++ b/components/paint_preview/renderer/DEPS
@@ -3,4 +3,5 @@
   "+content/public/renderer",
   "+content/public/test",
   "+third_party/blink/public",
+  "+ui/native_theme/native_theme_features.h",
 ]
diff --git a/components/paint_preview/renderer/paint_preview_recorder_browsertest.cc b/components/paint_preview/renderer/paint_preview_recorder_browsertest.cc
index c62e7aa..327d1389 100644
--- a/components/paint_preview/renderer/paint_preview_recorder_browsertest.cc
+++ b/components/paint_preview/renderer/paint_preview_recorder_browsertest.cc
@@ -7,11 +7,17 @@
 #include "base/files/file.h"
 #include "base/files/file_path.h"
 #include "base/files/scoped_temp_dir.h"
+#include "base/test/scoped_feature_list.h"
+#include "base/threading/thread_restrictions.h"
+#include "components/paint_preview/common/file_stream.h"
 #include "components/paint_preview/common/mojom/paint_preview_recorder.mojom.h"
 #include "content/public/renderer/render_frame.h"
 #include "content/public/renderer/render_view.h"
 #include "content/public/test/render_view_test.h"
 #include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/public/web/web_local_frame.h"
+#include "third_party/skia/include/core/SkPicture.h"
+#include "ui/native_theme/native_theme_features.h"
 
 namespace paint_preview {
 
@@ -38,6 +44,12 @@
 
   void SetUp() override {
     ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
+
+    // TODO(crbug/1022398): This is required to bypass a seemingly unrelated
+    // DCHECK for |use_overlay_scrollbars_| in NativeThemeAura on ChromeOS when
+    // painting scrollbars when first calling LoadHTML().
+    feature_list_.InitAndDisableFeature(features::kOverlayScrollbar);
+
     RenderViewTest::SetUp();
   }
 
@@ -49,14 +61,155 @@
 
  private:
   base::ScopedTempDir temp_dir_;
+  base::test::ScopedFeatureList feature_list_;
 };
 
-TEST_F(PaintPreviewRecorderRenderViewTest, TestCaptureMainFrame) {
+TEST_F(PaintPreviewRecorderRenderViewTest, TestCaptureMainFrameAndClipping) {
+  LoadHTML(
+      "<body>"
+      "  <div style='width: 600px; height: 80vh; "
+      "              background-color: #ff0000'>&nbsp;</div>"
+      "  <a style='display:inline-block' href='http://www.google.com'>Foo</a>"
+      "  <div style='width: 100px; height: 600px; "
+      "              background-color: #000000'>&nbsp;</div>"
+      "  <div style='overflow: hidden; width: 100px; height: 100px;"
+      "              background: orange;'>"
+      "    <div style='width: 500px; height: 500px;"
+      "                background: yellow;'></div>"
+      "  </div>"
+      "</body>");
+  base::FilePath skp_path = MakeTestFilePath("test.skp");
+
+  mojom::PaintPreviewCaptureParamsPtr params =
+      mojom::PaintPreviewCaptureParams::New();
+  auto token = base::UnguessableToken::Create();
+  params->guid = token;
+  params->clip_rect = gfx::Rect();
+  params->is_main_frame = true;
+  base::File skp_file(skp_path,
+                      base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE);
+  params->file = std::move(skp_file);
+
+  auto out_response = mojom::PaintPreviewCaptureResponse::New();
+  content::RenderFrame* frame = GetFrame();
+  int routing_id = frame->GetRoutingID();
+  PaintPreviewRecorderImpl paint_preview_recorder(frame);
+  paint_preview_recorder.CapturePaintPreview(
+      std::move(params),
+      base::BindOnce(&OnCaptureFinished, mojom::PaintPreviewStatus::kOk,
+                     base::Unretained(&out_response)));
+
+  // Here id() is just the routing ID.
+  EXPECT_EQ(static_cast<int>(out_response->id), routing_id);
+  EXPECT_EQ(out_response->content_id_proxy_id_map.size(), 0U);
+
+  EXPECT_EQ(out_response->links.size(), 1U);
+  EXPECT_EQ(out_response->links[0]->url, GURL("http://www.google.com/"));
+  // Relaxed checks on dimensions and no checks on positions. This is not
+  // intended to test the rendering behavior of the page only that a link
+  // was captured and has a bounding box.
+  EXPECT_GT(out_response->links[0]->rect.width(), 0);
+  EXPECT_GT(out_response->links[0]->rect.height(), 0);
+
+  sk_sp<SkPicture> pic;
+  {
+    base::ScopedAllowBlockingForTesting scope;
+    FileRStream rstream(base::File(
+        skp_path, base::File::FLAG_OPEN_ALWAYS | base::File::FLAG_READ));
+    pic = SkPicture::MakeFromStream(&rstream, nullptr);
+  }
+  // The min page height is the sum of the three top level divs of 800. The min
+  // width is that of the widest div at 600.
+  EXPECT_GE(pic->cullRect().height(), 800);
+  EXPECT_GE(pic->cullRect().width(), 600);
+  SkBitmap bitmap;
+  ASSERT_TRUE(bitmap.tryAllocN32Pixels(pic->cullRect().width(),
+                                       pic->cullRect().height()));
+  SkCanvas canvas(bitmap);
+  canvas.drawPicture(pic);
+  // This should be inside the top right corner of the first top level div.
+  // Success means there was no horizontal clipping as this region is red,
+  // matching the div.
+  EXPECT_EQ(bitmap.getColor(600, 50), 0xFFFF0000U);
+  // This should be inside the bottom of the second top level div. Success means
+  // there was no vertical clipping as this region is black matching the div. If
+  // the yellow div within the orange div overflowed then this would be yellow
+  // and fail.
+  EXPECT_EQ(bitmap.getColor(50, pic->cullRect().height() - 150), 0xFF000000U);
+  // This should be for the white background in the bottom right. This checks
+  // that the background is not clipped.
+  EXPECT_EQ(bitmap.getColor(pic->cullRect().width() - 50,
+                            pic->cullRect().height() - 50),
+            0xFFFFFFFFU);
+}
+
+TEST_F(PaintPreviewRecorderRenderViewTest, TestCaptureFragment) {
+  // Use position absolute position to check that the captured link dimensions
+  // match what is specified.
   LoadHTML(
       "<body style='min-height:1000px;'>"
-      "  <div style='width: 100px; height: 100px; "
-      "              background-color: #000000'>&nbsp;</div>"
-      "  <p><a href='https://www.foo.com'>Foo</a></p>"
+      "  <a style='position: absolute; left: -15px; top: 0px; width: 20px; "
+      "   height: 30px;' href='#fragment'>Foo</a>"
+      "  <h1 id='fragment'>I'm a fragment</h1>"
+      "</body>");
+  base::FilePath skp_path = MakeTestFilePath("test.skp");
+
+  mojom::PaintPreviewCaptureParamsPtr params =
+      mojom::PaintPreviewCaptureParams::New();
+  auto token = base::UnguessableToken::Create();
+  params->guid = token;
+  params->clip_rect = gfx::Rect();
+  params->is_main_frame = true;
+  base::File skp_file(skp_path,
+                      base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE);
+  params->file = std::move(skp_file);
+
+  auto out_response = mojom::PaintPreviewCaptureResponse::New();
+  content::RenderFrame* frame = GetFrame();
+  int routing_id = frame->GetRoutingID();
+  PaintPreviewRecorderImpl paint_preview_recorder(frame);
+  paint_preview_recorder.CapturePaintPreview(
+      std::move(params),
+      base::BindOnce(&OnCaptureFinished, mojom::PaintPreviewStatus::kOk,
+                     &out_response));
+  // Here id() is just the routing ID.
+  EXPECT_EQ(static_cast<int>(out_response->id), routing_id);
+  EXPECT_EQ(out_response->content_id_proxy_id_map.size(), 0U);
+
+  EXPECT_EQ(out_response->links.size(), 1U);
+  EXPECT_EQ(out_response->links[0]->url, GURL("fragment"));
+  EXPECT_EQ(out_response->links[0]->rect.x(), -15);
+  EXPECT_EQ(out_response->links[0]->rect.y(), 0);
+  EXPECT_EQ(out_response->links[0]->rect.width(), 20);
+  EXPECT_EQ(out_response->links[0]->rect.height(), 30);
+}
+
+TEST_F(PaintPreviewRecorderRenderViewTest, TestCaptureInvalidFile) {
+  LoadHTML("<body></body>");
+
+  mojom::PaintPreviewCaptureParamsPtr params =
+      mojom::PaintPreviewCaptureParams::New();
+  auto token = base::UnguessableToken::Create();
+  params->guid = token;
+  params->clip_rect = gfx::Rect();
+  params->is_main_frame = true;
+  base::File skp_file;  // Invalid file.
+  params->file = std::move(skp_file);
+
+  content::RenderFrame* frame = GetFrame();
+  PaintPreviewRecorderImpl paint_preview_recorder(frame);
+  paint_preview_recorder.CapturePaintPreview(
+      std::move(params),
+      base::BindOnce(&OnCaptureFinished,
+                     mojom::PaintPreviewStatus::kCaptureFailed, nullptr));
+}
+
+TEST_F(PaintPreviewRecorderRenderViewTest, TestCaptureMainFrameAndLocalFrame) {
+  LoadHTML(
+      "<body style='min-height:1000px;'>"
+      "  <iframe style='width: 500px, height: 500px'"
+      "          srcdoc=\"<div style='width: 100px; height: 100px;"
+      "          background-color: #000000'>&nbsp;</div>\"></iframe>"
       "</body>");
   base::FilePath skp_path = MakeTestFilePath("test.skp");
 
@@ -81,29 +234,40 @@
   // Here id() is just the routing ID.
   EXPECT_EQ(static_cast<int>(out_response->id), routing_id);
   EXPECT_EQ(out_response->content_id_proxy_id_map.size(), 0U);
-
-  // NOTE: should be non-zero once the Blink implementation is hooked up.
-  EXPECT_EQ(out_response->links.size(), 0U);
 }
 
-TEST_F(PaintPreviewRecorderRenderViewTest, TestCaptureInvalidFile) {
-  LoadHTML("<body></body>");
+TEST_F(PaintPreviewRecorderRenderViewTest, TestCaptureLocalFrame) {
+  LoadHTML(
+      "<body style='min-height:1000px;'>"
+      "  <iframe style='width: 500px, height: 500px'"
+      "          srcdoc=\"<div style='width: 100px; height: 100px;"
+      "          background-color: #000000'>&nbsp;</div>\"></iframe>"
+      "</body>");
+  base::FilePath skp_path = MakeTestFilePath("test.skp");
 
   mojom::PaintPreviewCaptureParamsPtr params =
       mojom::PaintPreviewCaptureParams::New();
   auto token = base::UnguessableToken::Create();
   params->guid = token;
   params->clip_rect = gfx::Rect();
-  params->is_main_frame = true;
-  base::File skp_file;  // Invalid file.
+  params->is_main_frame = false;
+  base::File skp_file(skp_path,
+                      base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE);
   params->file = std::move(skp_file);
 
-  content::RenderFrame* frame = GetFrame();
-  PaintPreviewRecorderImpl paint_preview_recorder(frame);
+  auto out_response = mojom::PaintPreviewCaptureResponse::New();
+  auto* child_frame = content::RenderFrame::FromWebFrame(
+      GetFrame()->GetWebFrame()->FirstChild()->ToWebLocalFrame());
+  ASSERT_TRUE(child_frame);
+  int routing_id = child_frame->GetRoutingID();
+  PaintPreviewRecorderImpl paint_preview_recorder(child_frame);
   paint_preview_recorder.CapturePaintPreview(
       std::move(params),
-      base::BindOnce(&OnCaptureFinished,
-                     mojom::PaintPreviewStatus::kCaptureFailed, nullptr));
+      base::BindOnce(&OnCaptureFinished, mojom::PaintPreviewStatus::kOk,
+                     base::Unretained(&out_response)));
+  // Here id() is just the routing ID.
+  EXPECT_EQ(static_cast<int>(out_response->id), routing_id);
+  EXPECT_EQ(out_response->content_id_proxy_id_map.size(), 0U);
 }
 
 }  // namespace paint_preview
diff --git a/components/paint_preview/renderer/paint_preview_recorder_impl.cc b/components/paint_preview/renderer/paint_preview_recorder_impl.cc
index 3b236b3..d807c8ec 100644
--- a/components/paint_preview/renderer/paint_preview_recorder_impl.cc
+++ b/components/paint_preview/renderer/paint_preview_recorder_impl.cc
@@ -101,15 +101,18 @@
   }
 
   cc::PaintRecorder recorder;
-  recorder.beginRecording(bounds.width(), bounds.height());
   PaintPreviewTracker tracker(params->guid, routing_id_, is_main_frame_);
-  // TODO(crbug/1008885): Create a method on |canvas| to inject |tracker_| to
-  // propagate to graphics contexts and inner canvases
-  // TODO(crbug/1001109): Create a method on |frame| to execute the capture
-  // within Blink.
+  cc::PaintCanvas* canvas =
+      recorder.beginRecording(bounds.width(), bounds.height());
+  canvas->SetPaintPreviewTracker(&tracker);
+  bool success = frame->CapturePaintPreview(bounds, canvas);
 
   // Restore to before out-of-lifecycle paint phase.
   frame->DispatchAfterPrintEvent();
+  if (!success) {
+    *status = mojom::PaintPreviewStatus::kCaptureFailed;
+    return;
+  }
 
   // TODO(crbug/1011896): Determine if making this async would be beneficial.
   *status = FinishRecording(recorder.finishRecordingAsPicture(), bounds,
diff --git a/third_party/blink/public/web/web_local_frame.h b/third_party/blink/public/web/web_local_frame.h
index ebf2ca9..bac8761 100644
--- a/third_party/blink/public/web/web_local_frame.h
+++ b/third_party/blink/public/web/web_local_frame.h
@@ -701,6 +701,12 @@
   virtual bool GetPrintPresetOptionsForPlugin(const WebNode&,
                                               WebPrintPresetOptions*) = 0;
 
+  // Paint Preview ------------------------------------------------------------
+
+  // Captures a full frame paint preview of the WebFrame including subframes.
+  virtual bool CapturePaintPreview(const WebRect& bounds,
+                                   cc::PaintCanvas* canvas) = 0;
+
   // Focus --------------------------------------------------------------
 
   // Advance the focus of the WebView to next text input element from current
diff --git a/third_party/blink/renderer/core/dom/document.cc b/third_party/blink/renderer/core/dom/document.cc
index 99aa1671..ba7f272 100644
--- a/third_party/blink/renderer/core/dom/document.cc
+++ b/third_party/blink/renderer/core/dom/document.cc
@@ -1047,6 +1047,7 @@
       http_refresh_scheduler_(MakeGarbageCollected<HttpRefreshScheduler>(this)),
       well_formed_(false),
       printing_(kNotPrinting),
+      is_painting_preview_(false),
       compatibility_mode_(kNoQuirksMode),
       compatibility_mode_locked_(false),
       last_focus_type_(kWebFocusTypeNone),
@@ -3555,6 +3556,10 @@
   }
 }
 
+void Document::SetIsPaintingPreview(bool is_painting_preview) {
+  is_painting_preview_ = is_painting_preview;
+}
+
 // https://html.spec.whatwg.org/C/dynamic-markup-insertion.html#document-open-steps
 void Document::open(Document* entered_document,
                     ExceptionState& exception_state) {
diff --git a/third_party/blink/renderer/core/dom/document.h b/third_party/blink/renderer/core/dom/document.h
index 8261c70a..26b5c756 100644
--- a/third_party/blink/renderer/core/dom/document.h
+++ b/third_party/blink/renderer/core/dom/document.h
@@ -774,6 +774,12 @@
   }
   void SetPrinting(PrintingState);
 
+  bool IsPaintingPreview() const { return is_painting_preview_; }
+  bool IsCapturingLayout() const {
+    return printing_ == kPrinting || is_painting_preview_;
+  }
+  void SetIsPaintingPreview(bool);
+
   enum CompatibilityMode { kQuirksMode, kLimitedQuirksMode, kNoQuirksMode };
 
   void SetCompatibilityMode(CompatibilityMode);
@@ -1840,6 +1846,7 @@
   Member<CSSStyleSheet> elem_sheet_;
 
   PrintingState printing_;
+  bool is_painting_preview_;
 
   CompatibilityMode compatibility_mode_;
   // This is cheaper than making setCompatibilityMode virtual.
diff --git a/third_party/blink/renderer/core/frame/local_frame.cc b/third_party/blink/renderer/core/frame/local_frame.cc
index 24ea8a0..3640a0c 100644
--- a/third_party/blink/renderer/core/frame/local_frame.cc
+++ b/third_party/blink/renderer/core/frame/local_frame.cc
@@ -1287,6 +1287,18 @@
     frame_view->ScheduleAnimation();
 }
 
+bool LocalFrame::ClipsContent() const {
+  // A paint preview shouldn't clip to the viewport if it is the main frame or a
+  // root remote frame.
+  if (GetDocument()->IsPaintingPreview() && IsLocalRoot())
+    return false;
+
+  if (IsMainFrame())
+    return GetSettings()->GetMainFrameClipsContent();
+  // By default clip to viewport.
+  return true;
+}
+
 void LocalFrame::SetViewportIntersectionFromParent(
     const ViewportIntersectionState& intersection_state) {
   // We only schedule an update if the viewport intersection or occlusion state
diff --git a/third_party/blink/renderer/core/frame/local_frame.h b/third_party/blink/renderer/core/frame/local_frame.h
index a5e3023a..75cb56b5 100644
--- a/third_party/blink/renderer/core/frame/local_frame.h
+++ b/third_party/blink/renderer/core/frame/local_frame.h
@@ -411,6 +411,9 @@
   void WasHidden();
   void WasShown();
 
+  // Whether the frame clips its content to the frame's size.
+  bool ClipsContent() const;
+
   // For a navigation initiated from this LocalFrame with user gesture, record
   // the UseCounter AdClickNavigation if this frame is an adframe.
   //
diff --git a/third_party/blink/renderer/core/frame/local_frame_view.cc b/third_party/blink/renderer/core/frame/local_frame_view.cc
index a716d44..d7005d1 100644
--- a/third_party/blink/renderer/core/frame/local_frame_view.cc
+++ b/third_party/blink/renderer/core/frame/local_frame_view.cc
@@ -2342,7 +2342,7 @@
   DCHECK_EQ(target_state, DocumentLifecycle::kPaintClean);
   RunPaintLifecyclePhase();
   DCHECK(ShouldThrottleRendering() ||
-         (frame_->GetDocument()->Printing() &&
+         (frame_->GetDocument()->IsCapturingLayout() &&
           !RuntimeEnabledFeatures::PrintBrowserEnabled()) ||
          Lifecycle().GetState() == DocumentLifecycle::kPaintClean);
 }
@@ -2495,16 +2495,16 @@
 
 void LocalFrameView::RunPaintLifecyclePhase() {
   TRACE_EVENT0("blink,benchmark", "LocalFrameView::RunPaintLifecyclePhase");
-  // While printing a document, the paint walk is done by the printing component
-  // into a special canvas. There is no point doing a normal paint step (or
-  // animations update) when in this mode.
+  // While printing or capturing a paint preview of a document, the paint walk
+  // is done into a special canvas. There is no point doing a normal paint step
+  // (or animations update) when in this mode.
   //
   // RuntimeEnabledFeatures::PrintBrowserEnabled is a mode which runs the
   // browser normally, but renders every page as if it were being printed.
   // See crbug.com/667547
-  bool print_mode_enabled = frame_->GetDocument()->Printing() &&
-                            !RuntimeEnabledFeatures::PrintBrowserEnabled();
-  if (!print_mode_enabled)
+  bool is_capturing_layout = frame_->GetDocument()->IsCapturingLayout() &&
+                             !RuntimeEnabledFeatures::PrintBrowserEnabled();
+  if (!is_capturing_layout)
     PaintTree();
 
   if (!RuntimeEnabledFeatures::CompositeAfterPaintEnabled()) {
@@ -2513,7 +2513,7 @@
     }
   }
 
-  if (!print_mode_enabled) {
+  if (!is_capturing_layout) {
     bool needed_update = !paint_artifact_compositor_ ||
                          paint_artifact_compositor_->NeedsUpdate();
     PushPaintArtifactToCompositor();
@@ -3541,9 +3541,11 @@
   // with CompositeAfterPaint.
   DCHECK(!RuntimeEnabledFeatures::CompositeAfterPaintEnabled());
 
-  // Paint the whole rect if "MainFrameClipsContent" is false, meaning that
-  // WebPreferences::record_whole_document is true.
-  if (!frame_->GetSettings()->GetMainFrameClipsContent())
+  // Paint the whole rect if ClipsContent is false, meaning that the whole
+  // document should be recorded. This occurs if:
+  // - A paint preview is being captured.
+  // - WebPreferences::record_whole_document is true.
+  if (!frame_->ClipsContent())
     return;
 
   // By default we consider the bounds of the FrameView to be what is considered
diff --git a/third_party/blink/renderer/core/frame/web_local_frame_impl.cc b/third_party/blink/renderer/core/frame/web_local_frame_impl.cc
index 4c685ca..cd078c7 100644
--- a/third_party/blink/renderer/core/frame/web_local_frame_impl.cc
+++ b/third_party/blink/renderer/core/frame/web_local_frame_impl.cc
@@ -514,6 +514,60 @@
   WebPrintParams print_params_;
 };
 
+class PaintPreviewContext : public PrintContext {
+ public:
+  PaintPreviewContext(LocalFrame* frame) : PrintContext(frame, false) {}
+  ~PaintPreviewContext() override = default;
+
+  bool Capture(cc::PaintCanvas* canvas, FloatSize size) {
+    // This code is based on ChromePrintContext::SpoolSinglePage()/SpoolPage().
+    // It differs in that it:
+    //   1. Uses a different set of flags for painting and the graphics context.
+    //   2. Paints a single page of |size| rather than a specific page in a
+    //      reformatted document.
+    //   3. Does no scaling.
+    if (!GetFrame()->GetDocument() ||
+        !GetFrame()->GetDocument()->GetLayoutView())
+      return false;
+    GetFrame()->View()->UpdateLifecyclePhasesForPrinting();
+    if (!GetFrame()->GetDocument() ||
+        !GetFrame()->GetDocument()->GetLayoutView())
+      return false;
+    FloatRect bounds(0, 0, size.Width(), size.Height());
+    PaintRecordBuilder builder(nullptr, nullptr, nullptr,
+                               canvas->GetPaintPreviewTracker());
+    builder.Context().SetIsPaintingPreview(true);
+
+    LocalFrameView* frame_view = GetFrame()->View();
+    DCHECK(frame_view);
+    PropertyTreeState property_tree_state =
+        frame_view->GetLayoutView()->FirstFragment().LocalBorderBoxProperties();
+
+    // This calls BeginRecording on |builder| with dimensions specified by the
+    // CullRect.
+    frame_view->PaintContentsOutsideOfLifecycle(
+        builder.Context(),
+        kGlobalPaintNormalPhase | kGlobalPaintFlattenCompositingLayers |
+            kGlobalPaintAddUrlMetadata,
+        CullRect(RoundedIntRect(bounds)));
+    {
+      // Add anchors.
+      ScopedPaintChunkProperties scoped_paint_chunk_properties(
+          builder.Context().GetPaintController(), property_tree_state, builder,
+          DisplayItem::kPrintedContentDestinationLocations);
+      DrawingRecorder line_boundary_recorder(
+          builder.Context(), builder,
+          DisplayItem::kPrintedContentDestinationLocations);
+      OutputLinkedDestinations(builder.Context(), RoundedIntRect(bounds));
+    }
+    canvas->drawPicture(builder.EndRecording(property_tree_state));
+    return true;
+  }
+
+ private:
+  DISALLOW_COPY_AND_ASSIGN(PaintPreviewContext);
+};
+
 static WebDocumentLoader* DocumentLoaderForDocLoader(DocumentLoader* loader) {
   return loader ? WebDocumentLoaderImpl::FromDocumentLoader(loader) : nullptr;
 }
@@ -1548,6 +1602,21 @@
   return plugin_container->GetPrintPresetOptionsFromDocument(preset_options);
 }
 
+bool WebLocalFrameImpl::CapturePaintPreview(const WebRect& bounds,
+                                            cc::PaintCanvas* canvas) {
+  FloatSize float_bounds(bounds.width, bounds.height);
+  GetFrame()->GetDocument()->SetIsPaintingPreview(true);
+  ResourceCacheValidationSuppressor validation_suppressor(
+      GetFrame()->GetDocument()->Fetcher());
+  GetFrame()->View()->ForceLayoutForPagination(float_bounds, float_bounds, 1);
+  PaintPreviewContext* paint_preview_context =
+      MakeGarbageCollected<PaintPreviewContext>(GetFrame());
+  bool success = paint_preview_context->Capture(canvas, float_bounds);
+  GetFrame()->GetDocument()->SetIsPaintingPreview(false);
+  GetFrame()->EndPrinting();
+  return success;
+}
+
 bool WebLocalFrameImpl::HasCustomPageSizeStyle(int page_index) {
   return GetFrame()->GetDocument()->StyleForPage(page_index)->PageSizeType() !=
          EPageSizeType::kAuto;
diff --git a/third_party/blink/renderer/core/frame/web_local_frame_impl.h b/third_party/blink/renderer/core/frame/web_local_frame_impl.h
index ef0067c..e862362 100644
--- a/third_party/blink/renderer/core/frame/web_local_frame_impl.h
+++ b/third_party/blink/renderer/core/frame/web_local_frame_impl.h
@@ -297,6 +297,8 @@
   void DispatchAfterPrintEvent() override;
   bool GetPrintPresetOptionsForPlugin(const WebNode&,
                                       WebPrintPresetOptions*) override;
+  bool CapturePaintPreview(const WebRect& bounds,
+                           cc::PaintCanvas* canvas) override;
   void AdvanceFocusInForm(WebFocusType) override;
   bool ShouldSuppressKeyboardForFocusedElement() override;
   WebPerformance Performance() const override;
diff --git a/third_party/blink/renderer/core/layout/layout_view.cc b/third_party/blink/renderer/core/layout/layout_view.cc
index 82387ff4..9a16efd 100644
--- a/third_party/blink/renderer/core/layout/layout_view.cc
+++ b/third_party/blink/renderer/core/layout/layout_view.cc
@@ -647,8 +647,9 @@
       RETURN_SCROLLBAR_MODE(ScrollbarMode::kAlwaysOff);
   }
 
-  if (document.Printing()) {
-    // When printing, frame-level scrollbars are never displayed.
+  if (document.IsCapturingLayout()) {
+    // When capturing layout (e.g. printing), frame-level scrollbars are never
+    // displayed.
     // TODO(szager): Figure out the right behavior when printing an overflowing
     // iframe.  https://bugs.chromium.org/p/chromium/issues/detail?id=777528
     RETURN_SCROLLBAR_MODE(ScrollbarMode::kAlwaysOff);
diff --git a/third_party/blink/renderer/core/paint/frame_painter.cc b/third_party/blink/renderer/core/paint/frame_painter.cc
index f8ce906d..3ca2029 100644
--- a/third_party/blink/renderer/core/paint/frame_painter.cc
+++ b/third_party/blink/renderer/core/paint/frame_painter.cc
@@ -76,7 +76,7 @@
   PaintLayerFlags root_layer_paint_flags = 0;
   // This will prevent clipping the root PaintLayer to its visible content
   // rect when root layer scrolling is enabled.
-  if (document->Printing())
+  if (document->IsCapturingLayout())
     root_layer_paint_flags = kPaintLayerPaintingOverflowContents;
 
   PaintLayer* root_layer = layout_view->Layer();
diff --git a/third_party/blink/renderer/core/paint/paint_layer_painter.cc b/third_party/blink/renderer/core/paint/paint_layer_painter.cc
index 16c67f1..9c3a8b9 100644
--- a/third_party/blink/renderer/core/paint/paint_layer_painter.cc
+++ b/third_party/blink/renderer/core/paint/paint_layer_painter.cc
@@ -112,8 +112,8 @@
                                     const GraphicsContext& context,
                                     const PaintLayerPaintingInfo& painting_info,
                                     PaintLayerFlags paint_flags) {
-  // Caching is not needed during printing.
-  if (context.Printing())
+  // Caching is not needed during printing or painting previews.
+  if (context.Printing() || context.IsPaintingPreview())
     return false;
 
   if (context.GetPaintController().IsSkippingCache())
@@ -198,8 +198,7 @@
   // of the main frame.
   if (layer.GetLayoutObject().IsLayoutView()) {
     const auto* frame = layer.GetLayoutObject().GetFrame();
-    if (frame && frame->IsMainFrame() &&
-        !frame->GetSettings()->GetMainFrameClipsContent())
+    if (frame && frame->IsMainFrame() && !frame->ClipsContent())
       return true;
   }
   return false;
diff --git a/third_party/blink/renderer/core/paint/paint_property_tree_builder.cc b/third_party/blink/renderer/core/paint/paint_property_tree_builder.cc
index 3e59e20..cc6a0ea 100644
--- a/third_party/blink/renderer/core/paint/paint_property_tree_builder.cc
+++ b/third_party/blink/renderer/core/paint/paint_property_tree_builder.cc
@@ -1541,8 +1541,7 @@
 static bool CanOmitOverflowClip(const LayoutObject& object) {
   DCHECK(NeedsOverflowClip(object));
 
-  if (object.IsLayoutView() && object.GetFrame()->IsMainFrame() &&
-      !object.GetFrame()->GetSettings()->GetMainFrameClipsContent()) {
+  if (object.IsLayoutView() && !object.GetFrame()->ClipsContent()) {
     return true;
   }
 
diff --git a/third_party/blink/renderer/core/paint/view_painter.cc b/third_party/blink/renderer/core/paint/view_painter.cc
index 5d6bed5f..b643cc2 100644
--- a/third_party/blink/renderer/core/paint/view_painter.cc
+++ b/third_party/blink/renderer/core/paint/view_painter.cc
@@ -48,9 +48,12 @@
   // The background rect always includes at least the visible content size.
   PhysicalRect background_rect(layout_view_.BackgroundRect());
 
-  // When printing, paint the entire unclipped scrolling content area.
-  if (paint_info.IsPrinting())
+  // When printing or painting a preview, paint the entire unclipped scrolling
+  // content area.
+  if (paint_info.IsPrinting() ||
+      !layout_view_.GetFrameView()->GetFrame().ClipsContent()) {
     background_rect.Unite(layout_view_.DocumentRect());
+  }
 
   const DisplayItemClient* background_client = &layout_view_;
 
diff --git a/third_party/blink/renderer/platform/graphics/graphics_context.cc b/third_party/blink/renderer/platform/graphics/graphics_context.cc
index f2d047a..22074d10 100644
--- a/third_party/blink/renderer/platform/graphics/graphics_context.cc
+++ b/third_party/blink/renderer/platform/graphics/graphics_context.cc
@@ -306,6 +306,8 @@
   canvas_ = paint_recorder_.beginRecording(bounds);
   if (metafile_)
     canvas_->SetPrintingMetafile(metafile_);
+  if (tracker_)
+    canvas_->SetPaintPreviewTracker(tracker_);
 }
 
 namespace {
diff --git a/third_party/blink/renderer/platform/graphics/paint/paint_record_builder.h b/third_party/blink/renderer/platform/graphics/paint/paint_record_builder.h
index 42631cf..ca07051 100644
--- a/third_party/blink/renderer/platform/graphics/paint/paint_record_builder.h
+++ b/third_party/blink/renderer/platform/graphics/paint/paint_record_builder.h
@@ -19,10 +19,6 @@
 class PaintCanvas;
 }
 
-namespace paint_preview {
-class PaintPreviewTracker;
-}
-
 namespace blink {
 class GraphicsContext;
 class PaintController;