Title: [277523] trunk
Revision
277523
Author
[email protected]
Date
2021-05-14 17:29:19 -0700 (Fri, 14 May 2021)

Log Message

Sampled Page Top Color: move logic out of `Document`
https://bugs.webkit.org/show_bug.cgi?id=225480
<rdar://problem/77984539>

Reviewed by Tim Horton.

Source/WebCore:

Having `determineSampledPageTopColor` be called in `enqueuePaintTimingEntryIfNeeded` as what
is basically a side effect is not great. The only reason it was there in the first place was
to take advantage of the logic that decided "is this the first contentful paint", but that
logic can be replicated elsewhere. Since the sampled page top color deals more with the page
as a whole instead of an individual `Document` (which includes subframes), it makes more
sense to have this logic be on a utility `PageColorSampler` that's used in `Page` instead.

Test: SampledPageTopColor.MainDocumentChange

* page/PageColorSampler.h: Added.
* page/PageColorSampler.cpp: Added.
(WebCore::isValidSampleLocation):
(WebCore::sampleColor):
(WebCore::colorDifference):
(WebCore::averageColor):
(WebCore::PageColorSampler::sampleTop):
* page/Page.h:
* page/Page.cpp:
(WebCore::Page::doAfterUpdateRendering):
(WebCore::Page::sampledPageTopColor const):
(WebCore::Page::didChangeMainDocument):

* dom/Document.h:
(WebCore::Document::sampledPageTopColor const): Deleted.
* dom/Document.cpp:
(WebCore::Document::enqueuePaintTimingEntryIfNeeded):
(WebCore::isValidPageSampleLocation): Deleted.
(WebCore::samplePageColor): Deleted.
(WebCore::colorDifference): Deleted.
(WebCore::averageColor): Deleted.
(WebCore::Document::determineSampledPageTopColor): Deleted.

* page/FrameView.h:
* page/FrameView.cpp:
(WebCore::FrameView::hasContentfulDescendants const): Added.
(WebCore::FrameView::hasContenfulDescendants const): Deleted.
Drive-by: Fix incorrect spelling.

* Source/WebCore/Headers.cmake:
* Source/WebCore/Sources.txt:
* Source/WebCore/WebCore.xcodeproj/project.pbxproj:

Tools:

* TestWebKitAPI/Tests/WebKitCocoa/SampledPageTopColor.mm:
(TEST.SampledPageTopColor.MainDocumentChange): Added.

Modified Paths

Added Paths

Diff

Modified: trunk/Source/WebCore/ChangeLog (277522 => 277523)


--- trunk/Source/WebCore/ChangeLog	2021-05-15 00:26:58 UTC (rev 277522)
+++ trunk/Source/WebCore/ChangeLog	2021-05-15 00:29:19 UTC (rev 277523)
@@ -1,3 +1,53 @@
+2021-05-14  Devin Rousso  <[email protected]>
+
+        Sampled Page Top Color: move logic out of `Document`
+        https://bugs.webkit.org/show_bug.cgi?id=225480
+        <rdar://problem/77984539>
+
+        Reviewed by Tim Horton.
+
+        Having `determineSampledPageTopColor` be called in `enqueuePaintTimingEntryIfNeeded` as what
+        is basically a side effect is not great. The only reason it was there in the first place was
+        to take advantage of the logic that decided "is this the first contentful paint", but that
+        logic can be replicated elsewhere. Since the sampled page top color deals more with the page
+        as a whole instead of an individual `Document` (which includes subframes), it makes more
+        sense to have this logic be on a utility `PageColorSampler` that's used in `Page` instead.
+
+        Test: SampledPageTopColor.MainDocumentChange
+
+        * page/PageColorSampler.h: Added.
+        * page/PageColorSampler.cpp: Added.
+        (WebCore::isValidSampleLocation):
+        (WebCore::sampleColor):
+        (WebCore::colorDifference):
+        (WebCore::averageColor):
+        (WebCore::PageColorSampler::sampleTop):
+        * page/Page.h:
+        * page/Page.cpp:
+        (WebCore::Page::doAfterUpdateRendering):
+        (WebCore::Page::sampledPageTopColor const):
+        (WebCore::Page::didChangeMainDocument):
+
+        * dom/Document.h:
+        (WebCore::Document::sampledPageTopColor const): Deleted.
+        * dom/Document.cpp:
+        (WebCore::Document::enqueuePaintTimingEntryIfNeeded):
+        (WebCore::isValidPageSampleLocation): Deleted.
+        (WebCore::samplePageColor): Deleted.
+        (WebCore::colorDifference): Deleted.
+        (WebCore::averageColor): Deleted.
+        (WebCore::Document::determineSampledPageTopColor): Deleted.
+
+        * page/FrameView.h:
+        * page/FrameView.cpp:
+        (WebCore::FrameView::hasContentfulDescendants const): Added.
+        (WebCore::FrameView::hasContenfulDescendants const): Deleted.
+        Drive-by: Fix incorrect spelling.
+
+        * Source/WebCore/Headers.cmake:
+        * Source/WebCore/Sources.txt:
+        * Source/WebCore/WebCore.xcodeproj/project.pbxproj:
+
 2021-05-14  Eric Carlson  <[email protected]>
 
         [GPUP] a media element with a data url and "crossorigin='anonymous'" doesn't load

Modified: trunk/Source/WebCore/Headers.cmake (277522 => 277523)


--- trunk/Source/WebCore/Headers.cmake	2021-05-15 00:26:58 UTC (rev 277522)
+++ trunk/Source/WebCore/Headers.cmake	2021-05-15 00:29:19 UTC (rev 277523)
@@ -906,6 +906,7 @@
     page/NavigatorIsLoggedIn.h
     page/PDFImageCachingPolicy.h
     page/Page.h
+    page/PageColorSampler.h
     page/PageConfiguration.h
     page/PageConsoleClient.h
     page/PageGroup.h

Modified: trunk/Source/WebCore/Sources.txt (277522 => 277523)


--- trunk/Source/WebCore/Sources.txt	2021-05-15 00:26:58 UTC (rev 277522)
+++ trunk/Source/WebCore/Sources.txt	2021-05-15 00:29:19 UTC (rev 277523)
@@ -1720,6 +1720,7 @@
 page/NavigatorIsLoggedIn.cpp
 page/OriginAccessEntry.cpp
 page/Page.cpp
+page/PageColorSampler.cpp
 page/PageConfiguration.cpp
 page/PageConsoleClient.cpp
 page/PageDebuggable.cpp

Modified: trunk/Source/WebCore/WebCore.xcodeproj/project.pbxproj (277522 => 277523)


--- trunk/Source/WebCore/WebCore.xcodeproj/project.pbxproj	2021-05-15 00:26:58 UTC (rev 277522)
+++ trunk/Source/WebCore/WebCore.xcodeproj/project.pbxproj	2021-05-15 00:29:19 UTC (rev 277523)
@@ -2987,6 +2987,7 @@
 		95A1E2E925CC75A900268C8E /* MediaControlsContextMenuItem.h in Headers */ = {isa = PBXBuildFile; fileRef = 95A1E2E725CC75A800268C8E /* MediaControlsContextMenuItem.h */; settings = {ATTRIBUTES = (Private, ); }; };
 		95AA571025EEED32004E9283 /* InspectorCanvasCallTracer.h in Headers */ = {isa = PBXBuildFile; fileRef = 95AA570E25EEED31004E9283 /* InspectorCanvasCallTracer.h */; };
 		95DF9D25252BEDB2000D7F46 /* PointerCharacteristics.h in Headers */ = {isa = PBXBuildFile; fileRef = 95DF9D23252BED99000D7F46 /* PointerCharacteristics.h */; settings = {ATTRIBUTES = (Private, ); }; };
+		95F45D27264DFBF100D0B8B8 /* PageColorSampler.h in Headers */ = {isa = PBXBuildFile; fileRef = 95F45D26264DFBEB00D0B8B8 /* PageColorSampler.h */; };
 		9705997A107D975200A50A7C /* PolicyChecker.h in Headers */ = {isa = PBXBuildFile; fileRef = 97059976107D975200A50A7C /* PolicyChecker.h */; settings = {ATTRIBUTES = (Private, ); }; };
 		970B728A144FFAC600F00A37 /* EventInterfaces.h in Headers */ = {isa = PBXBuildFile; fileRef = 970B7289144FFAC600F00A37 /* EventInterfaces.h */; settings = {ATTRIBUTES = (Private, ); }; };
 		970B72A6145008EB00F00A37 /* EventHeaders.h in Headers */ = {isa = PBXBuildFile; fileRef = 970B72A5145008EB00F00A37 /* EventHeaders.h */; };
@@ -11937,6 +11938,8 @@
 		95AA570D25EEED30004E9283 /* InspectorCanvasCallTracer.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = InspectorCanvasCallTracer.cpp; sourceTree = "<group>"; };
 		95AA570E25EEED31004E9283 /* InspectorCanvasCallTracer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = InspectorCanvasCallTracer.h; sourceTree = "<group>"; };
 		95DF9D23252BED99000D7F46 /* PointerCharacteristics.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PointerCharacteristics.h; sourceTree = "<group>"; };
+		95F45D24264DFBEB00D0B8B8 /* PageColorSampler.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = PageColorSampler.cpp; sourceTree = "<group>"; };
+		95F45D26264DFBEB00D0B8B8 /* PageColorSampler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PageColorSampler.h; sourceTree = "<group>"; };
 		97059975107D975200A50A7C /* PolicyChecker.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = PolicyChecker.cpp; sourceTree = "<group>"; };
 		97059976107D975200A50A7C /* PolicyChecker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PolicyChecker.h; sourceTree = "<group>"; };
 		970B7289144FFAC600F00A37 /* EventInterfaces.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = EventInterfaces.h; path = DerivedSources/WebCore/EventInterfaces.h; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -22402,6 +22405,8 @@
 				00146289103CD1DE000B20DB /* OriginAccessEntry.h */,
 				65FEA86809833ADE00BED4AB /* Page.cpp */,
 				65A21467097A329100B9050A /* Page.h */,
+				95F45D24264DFBEB00D0B8B8 /* PageColorSampler.cpp */,
+				95F45D26264DFBEB00D0B8B8 /* PageColorSampler.h */,
 				CD5E5B601A15F156000C609E /* PageConfiguration.cpp */,
 				CD5E5B5E1A15CE54000C609E /* PageConfiguration.h */,
 				DAACB3D916F2416400666135 /* PageConsoleClient.cpp */,
@@ -34356,6 +34361,7 @@
 				8326BF8E24D35C33001F8A85 /* OverSampleType.h in Headers */,
 				65A21468097A329100B9050A /* Page.h in Headers */,
 				91278D5E21DEDAD600B57184 /* PageAuditAgent.h in Headers */,
+				95F45D27264DFBF100D0B8B8 /* PageColorSampler.h in Headers */,
 				CD5E5B5F1A15CE54000C609E /* PageConfiguration.h in Headers */,
 				A5B81CC21FAA44BC0037D1E6 /* PageConsoleAgent.h in Headers */,
 				DAED203116F244480070EC0F /* PageConsoleClient.h in Headers */,

Modified: trunk/Source/WebCore/dom/Document.cpp (277522 => 277523)


--- trunk/Source/WebCore/dom/Document.cpp	2021-05-15 00:26:58 UTC (rev 277522)
+++ trunk/Source/WebCore/dom/Document.cpp	2021-05-15 00:29:19 UTC (rev 277523)
@@ -82,7 +82,6 @@
 #include "Frame.h"
 #include "FrameLoader.h"
 #include "FrameLoaderClient.h"
-#include "FrameSnapshotting.h"
 #include "FrameView.h"
 #include "FullscreenManager.h"
 #include "GCReachableRef.h"
@@ -125,7 +124,6 @@
 #include "IDBOpenDBRequest.h"
 #include "IdleCallbackController.h"
 #include "ImageBitmapRenderingContext.h"
-#include "ImageBuffer.h"
 #include "ImageLoader.h"
 #include "ImageOverlayController.h"
 #include "InspectorInstrumentation.h"
@@ -178,11 +176,9 @@
 #include "Range.h"
 #include "RealtimeMediaSourceCenter.h"
 #include "RenderChildIterator.h"
-#include "RenderImage.h"
 #include "RenderInline.h"
 #include "RenderLayerCompositor.h"
 #include "RenderLineBreak.h"
-#include "RenderStyle.h"
 #include "RenderTreeUpdater.h"
 #include "RenderView.h"
 #include "RenderWidget.h"
@@ -3273,7 +3269,7 @@
     if (!view()->isVisuallyNonEmpty() || view()->needsLayout())
         return;
 
-    if (!view()->hasContenfulDescendants())
+    if (!view()->hasContentfulDescendants())
         return;
 
     if (!ContentfulPaintChecker::qualifiesForContentfulPaint(*view()))
@@ -3281,8 +3277,6 @@
 
     domWindow()->performance().reportFirstContentfulPaint();
     m_didEnqueueFirstContentfulPaint = true;
-
-    determineSampledPageTopColor();
 }
 
 ExceptionOr<void> Document::write(Document* responsibleDocument, SegmentedString&& text)
@@ -3903,197 +3897,6 @@
         page->chrome().client().themeColorChanged();
 }
 
-static bool isValidPageSampleLocation(Document& document, const IntPoint& location)
-{
-    // FIXME: <https://webkit.org/b/225167> (Sampled Page Top Color: hook into painting logic instead of taking snapshots)
-
-    constexpr OptionSet<HitTestRequest::Type> hitTestRequestTypes { HitTestRequest::Type::ReadOnly, HitTestRequest::Type::IgnoreCSSPointerEventsProperty, HitTestRequest::Type::DisallowUserAgentShadowContent, HitTestRequest::Type::CollectMultipleElements, HitTestRequest::Type::IncludeAllElementsUnderPoint };
-    HitTestResult hitTestResult(location);
-    document.hitTest(hitTestRequestTypes, hitTestResult);
-
-    for (auto& hitTestNode : hitTestResult.listBasedTestResult()) {
-        auto& node = hitTestNode.get();
-
-        auto* renderer = node.renderer();
-        if (!renderer)
-            return false;
-
-        // Skip images (both `<img>` and CSS `background-image`) as they're likely not a solid color.
-        if (is<RenderImage>(renderer) || renderer->style().hasBackgroundImage())
-            return false;
-
-        // Skip nodes with animations as the sample may get an odd color if the animation is in-progress.
-        if (renderer->style().hasTransitions() || renderer->style().hasAnimations())
-            return false;
-
-        // Skip `<canvas>` but only if they've been drawn into. Guess this by seeing if there's already
-        // a `CanvasRenderingContext`, which is only created by _javascript_.
-        if (is<HTMLCanvasElement>(node) && downcast<HTMLCanvasElement>(node).renderingContext())
-            return false;
-
-        // Skip 3rd-party `<iframe>` as the content likely won't match the rest of the page.
-        if (is<HTMLIFrameElement>(node) && !areRegistrableDomainsEqual(downcast<HTMLIFrameElement>(node).location(), document.url()))
-            return false;
-    }
-
-    return true;
-}
-
-static Optional<Lab<float>> samplePageColor(Document& document, IntPoint&& location)
-{
-    // FIXME: <https://webkit.org/b/225167> (Sampled Page Top Color: hook into painting logic instead of taking snapshots)
-
-    if (!isValidPageSampleLocation(document, location))
-        return WTF::nullopt;
-
-    ASSERT(document.view());
-    auto snapshot = snapshotFrameRect(document.view()->frame(), IntRect(location, IntSize(1, 1)), SnapshotOptionsExcludeSelectionHighlighting | SnapshotOptionsPaintEverythingExcludingSelection);
-    if (!snapshot)
-        return WTF::nullopt;
-
-    auto snapshotData = snapshot->toBGRAData();
-    return convertColor<Lab<float>>(SRGBA<uint8_t> { snapshotData[2], snapshotData[1], snapshotData[0], snapshotData[3] });
-}
-
-static double colorDifference(Lab<float>& lhs, Lab<float>& rhs)
-{
-    return sqrt(pow(rhs.lightness - lhs.lightness, 2) + pow(rhs.a - lhs.a, 2) + pow(rhs.b - lhs.b, 2));
-}
-
-static Lab<float> averageColor(Lab<float> colors[], size_t count)
-{
-    float totalLightness = 0;
-    float totalA = 0;
-    float totalB = 0;
-    for (size_t i = 0; i < count; ++i) {
-        totalLightness += colors[i].lightness;
-        totalA += colors[i].a;
-        totalB += colors[i].b;
-    }
-    return {
-        totalLightness / count,
-        totalA / count,
-        totalB / count,
-        1,
-    };
-}
-
-void Document::determineSampledPageTopColor()
-{
-    if (m_sampledPageTopColor.isValid())
-        return;
-
-    auto maxDifference = settings().sampledPageTopColorMaxDifference();
-    if (maxDifference <= 0)
-        return;
-
-    if (!isTopDocument())
-        return;
-
-    auto* frameView = view();
-    if (!frameView)
-        return;
-
-    auto notifyDidSamplePageTopColorOnScopeExit = makeScopeExit([&] {
-        if (auto* page = this->page())
-            page->chrome().client().sampledPageTopColorChanged();
-    });
-
-    // Decrease the width by one pixel so that the last snapshot is within bounds and not off-by-one.
-    auto frameWidth = frameView->contentsWidth() - 1;
-
-    constexpr auto numSnapshots = 5;
-    size_t nonMatchingColorIndex = numSnapshots;
-
-    Lab<float> snapshots[numSnapshots];
-    double differences[numSnapshots - 1];
-
-    auto shouldStopAfterFindingNonMatchingColor = [&] (size_t i) -> bool {
-        // Bail if the non-matching color is not the first or last snapshot, or there already is an non-matching color.
-        if ((i && i < numSnapshots - 1) || nonMatchingColorIndex != numSnapshots)
-            return true;
-
-        nonMatchingColorIndex = i;
-        return false;
-    };
-
-    for (size_t i = 0; i < numSnapshots; ++i) {
-        auto snapshot = samplePageColor(*this, IntPoint(frameWidth * i / (numSnapshots - 1), 0));
-        if (!snapshot) {
-            if (shouldStopAfterFindingNonMatchingColor(i))
-                return;
-            continue;
-        }
-
-        snapshots[i] = *snapshot;
-
-        if (i) {
-            // Each `difference` item compares `i` with `i - 1` so if the first comparison (`i == 1`)
-            // is too large of a difference, we should treat `i - 1` (i.e. `0`) as the problem since
-            // we only allow for non-matching colors being the first or last sampled color.
-            auto effectiveNonMatchingColorIndex = i == 1 ? 0 : i;
-
-            differences[i - 1] = colorDifference(snapshots[i - 1], snapshots[i]);
-            if (differences[i - 1] > maxDifference) {
-                if (shouldStopAfterFindingNonMatchingColor(effectiveNonMatchingColorIndex))
-                    return;
-                continue;
-            }
-
-            double cumuluativeDifference = 0;
-            for (size_t j = 0; j < i; ++j) {
-                if (j == nonMatchingColorIndex)
-                    continue;
-                cumuluativeDifference += differences[j];
-            }
-            if (cumuluativeDifference > maxDifference) {
-                if (shouldStopAfterFindingNonMatchingColor(effectiveNonMatchingColorIndex)) {
-                    // If we haven't already identified a non-matching snapshot and the difference between the first
-                    // and second snapshots or the second-to-last and last snapshots is less than the maximum, mark
-                    // the first/last snapshot as non-matching to give a chance for the rest of the snapshots to match.
-                    if (nonMatchingColorIndex == numSnapshots && (!i || i == numSnapshots - 1) && cumuluativeDifference - differences[i - 1] <= maxDifference) {
-                        nonMatchingColorIndex = effectiveNonMatchingColorIndex;
-                        continue;
-                    }
-                    return;
-                }
-                continue;
-            }
-        }
-    }
-
-    // Decrease the height by one pixel so that the last snapshot is within bounds and not off-by-one.
-    auto minHeight = settings().sampledPageTopColorMinHeight() - 1;
-    if (minHeight > 0) {
-        if (nonMatchingColorIndex) {
-            if (auto leftMiddleSnapshot = samplePageColor(*this, IntPoint(0, minHeight))) {
-                if (colorDifference(*leftMiddleSnapshot, snapshots[0]) > maxDifference)
-                    return;
-            }
-        }
-
-        if (nonMatchingColorIndex != numSnapshots - 1) {
-            if (auto rightMiddleSnapshot = samplePageColor(*this, IntPoint(frameWidth, minHeight))) {
-                if (colorDifference(*rightMiddleSnapshot, snapshots[numSnapshots - 1]) > maxDifference)
-                    return;
-            }
-        }
-    }
-
-    auto snapshotsToAverage = snapshots;
-    auto validSnapshotCount = numSnapshots;
-    if (!nonMatchingColorIndex) {
-        // Skip the first snapshot by moving the pointer that indicates where the snapshot array
-        // starts and decreasing the count of snapshots to average.
-        ++snapshotsToAverage;
-        --validSnapshotCount;
-    } else if (nonMatchingColorIndex == numSnapshots - 1) {
-        // Skip the last snapshot by decreasing the count of snapshots to average.
-        --validSnapshotCount;
-    }
-    m_sampledPageTopColor = averageColor(snapshotsToAverage, validSnapshotCount);
-}
-
 #if ENABLE(DARK_MODE_CSS)
 static void processColorSchemeString(StringView colorScheme, const WTF::Function<void(StringView key)>& callback)
 {

Modified: trunk/Source/WebCore/dom/Document.h (277522 => 277523)


--- trunk/Source/WebCore/dom/Document.h	2021-05-15 00:26:58 UTC (rev 277522)
+++ trunk/Source/WebCore/dom/Document.h	2021-05-15 00:29:19 UTC (rev 277523)
@@ -744,8 +744,6 @@
 
     const Color& themeColor();
 
-    const Color& sampledPageTopColor() const { return m_sampledPageTopColor; }
-
     void setTextColor(const Color& color) { m_textColor = color; }
     const Color& textColor() const { return m_textColor; }
 
@@ -1677,8 +1675,6 @@
     WeakPtr<HTMLMetaElement> determineActiveThemeColorMetaElement();
     void themeColorChanged();
 
-    void determineSampledPageTopColor();
-
     void invalidateAccessKeyCacheSlowCase();
     void buildAccessKeyCache();
 
@@ -1802,8 +1798,6 @@
     WeakPtr<HTMLMetaElement> m_activeThemeColorMetaElement;
     Color m_applicationManifestThemeColor;
 
-    Color m_sampledPageTopColor;
-
     Color m_textColor { Color::black };
     Color m_linkColor;
     Color m_visitedLinkColor;

Modified: trunk/Source/WebCore/page/FrameView.cpp (277522 => 277523)


--- trunk/Source/WebCore/page/FrameView.cpp	2021-05-15 00:26:58 UTC (rev 277522)
+++ trunk/Source/WebCore/page/FrameView.cpp	2021-05-15 00:29:19 UTC (rev 277523)
@@ -4617,7 +4617,7 @@
         frame().loader().didReachVisuallyNonEmptyState();
 }
 
-bool FrameView::hasContenfulDescendants() const
+bool FrameView::hasContentfulDescendants() const
 {
     return m_visuallyNonEmptyCharacterCount || m_visuallyNonEmptyPixelCount;
 }

Modified: trunk/Source/WebCore/page/FrameView.h (277522 => 277523)


--- trunk/Source/WebCore/page/FrameView.h	2021-05-15 00:26:58 UTC (rev 277522)
+++ trunk/Source/WebCore/page/FrameView.h	2021-05-15 00:29:19 UTC (rev 277523)
@@ -413,7 +413,7 @@
     void incrementVisuallyNonEmptyCharacterCount(const String&);
     void incrementVisuallyNonEmptyPixelCount(const IntSize&);
     bool isVisuallyNonEmpty() const { return m_contentQualifiesAsVisuallyNonEmpty; }
-    bool hasContenfulDescendants() const;
+    bool hasContentfulDescendants() const;
     void checkAndDispatchDidReachVisuallyNonEmptyState();
 
     WEBCORE_EXPORT void enableFixedWidthAutoSizeMode(bool enable, const IntSize& minSize);

Modified: trunk/Source/WebCore/page/Page.cpp (277522 => 277523)


--- trunk/Source/WebCore/page/Page.cpp	2021-05-15 00:26:58 UTC (rev 277522)
+++ trunk/Source/WebCore/page/Page.cpp	2021-05-15 00:29:19 UTC (rev 277523)
@@ -87,6 +87,7 @@
 #include "MediaCanStartListener.h"
 #include "MediaRecorderProvider.h"
 #include "Navigator.h"
+#include "PageColorSampler.h"
 #include "PageConfiguration.h"
 #include "PageConsoleClient.h"
 #include "PageDebuggable.h"
@@ -1672,6 +1673,12 @@
         ASSERT(!frameView || !frameView->needsLayout());
     }
 #endif
+
+    if (!m_sampledPageTopColor) {
+        m_sampledPageTopColor = PageColorSampler::sampleTop(*this);
+        if (m_sampledPageTopColor)
+            chrome().client().sampledPageTopColorChanged();
+    }
 }
 
 void Page::finalizeRenderingUpdate(OptionSet<FinalizeRenderingUpdateFlags> flags)
@@ -2556,11 +2563,7 @@
 
 Color Page::sampledPageTopColor() const
 {
-    auto* document = mainFrame().document();
-    if (!document)
-        return { };
-
-    return document->sampledPageTopColor();
+    return m_sampledPageTopColor.valueOr(Color());
 }
 
 void Page::setUnderPageBackgroundColorOverride(Color&& underPageBackgroundColorOverride)
@@ -3285,6 +3288,11 @@
     m_rtcController.reset(m_shouldEnableICECandidateFilteringByDefault);
 #endif
     m_pointerCaptureController->reset();
+
+    if (m_sampledPageTopColor) {
+        m_sampledPageTopColor = WTF::nullopt;
+        chrome().client().sampledPageTopColorChanged();
+    }
 }
 
 RenderingUpdateScheduler& Page::renderingUpdateScheduler()

Modified: trunk/Source/WebCore/page/Page.h (277522 => 277523)


--- trunk/Source/WebCore/page/Page.h	2021-05-15 00:26:58 UTC (rev 277522)
+++ trunk/Source/WebCore/page/Page.h	2021-05-15 00:29:19 UTC (rev 277523)
@@ -22,6 +22,7 @@
 
 #include "ActivityState.h"
 #include "AnimationFrameRate.h"
+#include "Color.h"
 #include "DisabledAdaptations.h"
 #include "Document.h"
 #include "FindOptions.h"
@@ -94,7 +95,6 @@
 class BackForwardController;
 class CacheStorageProvider;
 class Chrome;
-class Color;
 class ContextMenuController;
 class CookieJar;
 class DOMRectList;
@@ -1156,6 +1156,7 @@
     MonotonicTime m_lastRenderingUpdateTimestamp;
 
     Color m_underPageBackgroundColorOverride;
+    Optional<Color> m_sampledPageTopColor;
 
     const bool m_httpsUpgradeEnabled { true };
     mutable MediaSessionGroupIdentifier m_mediaSessionGroupIdentifier;

Added: trunk/Source/WebCore/page/PageColorSampler.cpp (0 => 277523)


--- trunk/Source/WebCore/page/PageColorSampler.cpp	                        (rev 0)
+++ trunk/Source/WebCore/page/PageColorSampler.cpp	2021-05-15 00:29:19 UTC (rev 277523)
@@ -0,0 +1,255 @@
+/*
+ * Copyright (C) 2021 Apple 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 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 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 "config.h"
+#include "PageColorSampler.h"
+
+#include "ContentfulPaintChecker.h"
+#include "Document.h"
+#include "Frame.h"
+#include "FrameSnapshotting.h"
+#include "FrameView.h"
+#include "HTMLCanvasElement.h"
+#include "HTMLIFrameElement.h"
+#include "HitTestRequest.h"
+#include "HitTestResult.h"
+#include "ImageBuffer.h"
+#include "IntPoint.h"
+#include "IntRect.h"
+#include "IntSize.h"
+#include "Node.h"
+#include "Page.h"
+#include "RegistrableDomain.h"
+#include "RenderImage.h"
+#include "RenderObject.h"
+#include "RenderStyle.h"
+#include "Settings.h"
+#include <wtf/ListHashSet.h>
+#include <wtf/OptionSet.h>
+#include <wtf/Optional.h>
+#include <wtf/Ref.h>
+#include <wtf/RefPtr.h>
+#include <wtf/URL.h>
+
+namespace WebCore {
+
+static bool isValidSampleLocation(Document& document, const IntPoint& location)
+{
+    // FIXME: <https://webkit.org/b/225167> (Sampled Page Top Color: hook into painting logic instead of taking snapshots)
+
+    constexpr OptionSet<HitTestRequest::Type> hitTestRequestTypes { HitTestRequest::Type::ReadOnly, HitTestRequest::Type::IgnoreCSSPointerEventsProperty, HitTestRequest::Type::DisallowUserAgentShadowContent, HitTestRequest::Type::CollectMultipleElements, HitTestRequest::Type::IncludeAllElementsUnderPoint };
+    HitTestResult hitTestResult(location);
+    document.hitTest(hitTestRequestTypes, hitTestResult);
+
+    for (auto& hitTestNode : hitTestResult.listBasedTestResult()) {
+        auto& node = hitTestNode.get();
+
+        auto* renderer = node.renderer();
+        if (!renderer)
+            return false;
+
+        // Skip images (both `<img>` and CSS `background-image`) as they're likely not a solid color.
+        if (is<RenderImage>(renderer) || renderer->style().hasBackgroundImage())
+            return false;
+
+        // Skip nodes with animations as the sample may get an odd color if the animation is in-progress.
+        if (renderer->style().hasTransitions() || renderer->style().hasAnimations())
+            return false;
+
+        // Skip `<canvas>` but only if they've been drawn into. Guess this by seeing if there's already
+        // a `CanvasRenderingContext`, which is only created by _javascript_.
+        if (is<HTMLCanvasElement>(node) && downcast<HTMLCanvasElement>(node).renderingContext())
+            return false;
+
+        // Skip 3rd-party `<iframe>` as the content likely won't match the rest of the page.
+        if (is<HTMLIFrameElement>(node) && !areRegistrableDomainsEqual(downcast<HTMLIFrameElement>(node).location(), document.url()))
+            return false;
+    }
+
+    return true;
+}
+
+static Optional<Lab<float>> sampleColor(Document& document, IntPoint&& location)
+{
+    // FIXME: <https://webkit.org/b/225167> (Sampled Page Top Color: hook into painting logic instead of taking snapshots)
+
+    if (!isValidSampleLocation(document, location))
+        return WTF::nullopt;
+
+    ASSERT(document.view());
+    auto snapshot = snapshotFrameRect(document.view()->frame(), IntRect(location, IntSize(1, 1)), SnapshotOptionsExcludeSelectionHighlighting | SnapshotOptionsPaintEverythingExcludingSelection);
+    if (!snapshot)
+        return WTF::nullopt;
+
+    auto snapshotData = snapshot->toBGRAData();
+    return convertColor<Lab<float>>(SRGBA<uint8_t> { snapshotData[2], snapshotData[1], snapshotData[0], snapshotData[3] });
+}
+
+static double colorDifference(Lab<float>& lhs, Lab<float>& rhs)
+{
+    return sqrt(pow(rhs.lightness - lhs.lightness, 2) + pow(rhs.a - lhs.a, 2) + pow(rhs.b - lhs.b, 2));
+}
+
+static Lab<float> averageColor(Lab<float> colors[], size_t count)
+{
+    float totalLightness = 0;
+    float totalA = 0;
+    float totalB = 0;
+    for (size_t i = 0; i < count; ++i) {
+        totalLightness += colors[i].lightness;
+        totalA += colors[i].a;
+        totalB += colors[i].b;
+    }
+    return {
+        totalLightness / count,
+        totalA / count,
+        totalB / count,
+        1,
+    };
+}
+
+Optional<Color> PageColorSampler::sampleTop(Page& page)
+{
+    // If `WTF::nullopt` is returned then that means that no samples were taken (i.e. the `Page` is not ready yet).
+    // If an invalid `Color` is returned then that means that samples Were taken but they were too different.
+
+    auto maxDifference = page.settings().sampledPageTopColorMaxDifference();
+    if (maxDifference <= 0) {
+        // Pretend that the samples are too different so that this function is not called again.
+        return Color();
+    }
+
+    auto mainDocument = makeRefPtr(page.mainFrame().document());
+    if (!mainDocument)
+        return WTF::nullopt;
+
+    auto frameView = makeRefPtr(page.mainFrame().view());
+    if (!frameView)
+        return WTF::nullopt;
+
+    // Don't take samples if the layer tree is still frozen.
+    if (frameView->needsLayout())
+        return WTF::nullopt;
+
+    // Don't attempt to hit test or sample if we don't have any content yet.
+    if (!frameView->isVisuallyNonEmpty() || !frameView->hasContentfulDescendants() || !ContentfulPaintChecker::qualifiesForContentfulPaint(*frameView))
+        return WTF::nullopt;
+
+    // Decrease the width by one pixel so that the last sample is within bounds and not off-by-one.
+    auto frameWidth = frameView->contentsWidth() - 1;
+
+    constexpr auto numSamples = 5;
+    size_t nonMatchingColorIndex = numSamples;
+
+    Lab<float> samples[numSamples];
+    double differences[numSamples - 1];
+
+    auto shouldStopAfterFindingNonMatchingColor = [&] (size_t i) -> bool {
+        // Bail if the non-matching color is not the first or last sample, or there already is an non-matching color.
+        if ((i && i < numSamples - 1) || nonMatchingColorIndex != numSamples)
+            return true;
+
+        nonMatchingColorIndex = i;
+        return false;
+    };
+
+    for (size_t i = 0; i < numSamples; ++i) {
+        auto sample = sampleColor(*mainDocument, IntPoint(frameWidth * i / (numSamples - 1), 0));
+        if (!sample) {
+            if (shouldStopAfterFindingNonMatchingColor(i))
+                return Color();
+            continue;
+        }
+
+        samples[i] = *sample;
+
+        if (i) {
+            // Each `difference` item compares `i` with `i - 1` so if the first comparison (`i == 1`)
+            // is too large of a difference, we should treat `i - 1` (i.e. `0`) as the problem since
+            // we only allow for non-matching colors being the first or last sampled color.
+            auto effectiveNonMatchingColorIndex = i == 1 ? 0 : i;
+
+            differences[i - 1] = colorDifference(samples[i - 1], samples[i]);
+            if (differences[i - 1] > maxDifference) {
+                if (shouldStopAfterFindingNonMatchingColor(effectiveNonMatchingColorIndex))
+                    return Color();
+                continue;
+            }
+
+            double cumuluativeDifference = 0;
+            for (size_t j = 0; j < i; ++j) {
+                if (j == nonMatchingColorIndex)
+                    continue;
+                cumuluativeDifference += differences[j];
+            }
+            if (cumuluativeDifference > maxDifference) {
+                if (shouldStopAfterFindingNonMatchingColor(effectiveNonMatchingColorIndex)) {
+                    // If we haven't already identified a non-matching sample and the difference between the first
+                    // and second samples or the second-to-last and last samples is less than the maximum, mark
+                    // the first/last sample as non-matching to give a chance for the rest of the samples to match.
+                    if (nonMatchingColorIndex == numSamples && (!i || i == numSamples - 1) && cumuluativeDifference - differences[i - 1] <= maxDifference) {
+                        nonMatchingColorIndex = effectiveNonMatchingColorIndex;
+                        continue;
+                    }
+                    return Color();
+                }
+                continue;
+            }
+        }
+    }
+
+    // Decrease the height by one pixel so that the last sample is within bounds and not off-by-one.
+    auto minHeight = page.settings().sampledPageTopColorMinHeight() - 1;
+    if (minHeight > 0) {
+        if (nonMatchingColorIndex) {
+            if (auto leftMiddleSample = sampleColor(*mainDocument, IntPoint(0, minHeight))) {
+                if (colorDifference(*leftMiddleSample, samples[0]) > maxDifference)
+                    return Color();
+            }
+        }
+
+        if (nonMatchingColorIndex != numSamples - 1) {
+            if (auto rightMiddleSample = sampleColor(*mainDocument, IntPoint(frameWidth, minHeight))) {
+                if (colorDifference(*rightMiddleSample, samples[numSamples - 1]) > maxDifference)
+                    return Color();
+            }
+        }
+    }
+
+    auto samplesToAverage = samples;
+    auto validSampleCount = numSamples;
+    if (!nonMatchingColorIndex) {
+        // Skip the first sample by moving the pointer that indicates where the sample array
+        // starts and decreasing the count of samples to average.
+        ++samplesToAverage;
+        --validSampleCount;
+    } else if (nonMatchingColorIndex == numSamples - 1) {
+        // Skip the last sample by decreasing the count of samples to average.
+        --validSampleCount;
+    }
+    return Color(averageColor(samplesToAverage, validSampleCount));
+}
+
+} // namespace WebCore

Added: trunk/Source/WebCore/page/PageColorSampler.h (0 => 277523)


--- trunk/Source/WebCore/page/PageColorSampler.h	                        (rev 0)
+++ trunk/Source/WebCore/page/PageColorSampler.h	2021-05-15 00:29:19 UTC (rev 277523)
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2021 Apple 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 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 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.
+ */
+
+#pragma once
+
+#include "Color.h"
+#include <wtf/Forward.h>
+
+namespace WebCore {
+
+class Page;
+
+class PageColorSampler {
+public:
+    static Optional<Color> sampleTop(Page&);
+};
+
+} // namespace WebCore

Modified: trunk/Tools/ChangeLog (277522 => 277523)


--- trunk/Tools/ChangeLog	2021-05-15 00:26:58 UTC (rev 277522)
+++ trunk/Tools/ChangeLog	2021-05-15 00:29:19 UTC (rev 277523)
@@ -1,3 +1,14 @@
+2021-05-14  Devin Rousso  <[email protected]>
+
+        Sampled Page Top Color: move logic out of `Document`
+        https://bugs.webkit.org/show_bug.cgi?id=225480
+        <rdar://problem/77984539>
+
+        Reviewed by Tim Horton.
+
+        * TestWebKitAPI/Tests/WebKitCocoa/SampledPageTopColor.mm:
+        (TEST.SampledPageTopColor.MainDocumentChange): Added.
+
 2021-05-14  Chris Dumez  <[email protected]>
 
         Port WTF::FileSystem::realPath() to std::filesystem

Modified: trunk/Tools/TestWebKitAPI/Tests/WebKitCocoa/SampledPageTopColor.mm (277522 => 277523)


--- trunk/Tools/TestWebKitAPI/Tests/WebKitCocoa/SampledPageTopColor.mm	2021-05-15 00:26:58 UTC (rev 277522)
+++ trunk/Tools/TestWebKitAPI/Tests/WebKitCocoa/SampledPageTopColor.mm	2021-05-15 00:29:19 UTC (rev 277523)
@@ -350,3 +350,44 @@
     EXPECT_IN_RANGE(components[2], 1.00, 1.01);
     EXPECT_EQ(components[3], 1);
 }
+
+TEST(SampledPageTopColor, MainDocumentChange)
+{
+    auto webView = createWebViewWithSampledPageTopColorMaxDifference(5);
+    EXPECT_NULL([webView _sampledPageTopColor]);
+
+    size_t notificationCount = 0;
+    auto sampledPageTopColorObserver = adoptNS([[TestKVOWrapper alloc] initWithObservable:webView.get() keyPath:@"_sampledPageTopColor" callback:[&] {
+        ++notificationCount;
+    }]);
+
+    [webView synchronouslyLoadHTMLStringAndWaitUntilAllImmediateChildFramesPaint:@"<body>Test"];
+    EXPECT_EQ(WebCore::Color([webView _sampledPageTopColor].CGColor), WebCore::Color::white);
+    EXPECT_EQ(notificationCount, 1UL);
+
+    notificationCount = 0;
+
+    [webView synchronouslyLoadHTMLStringAndWaitUntilAllImmediateChildFramesPaint:@"<body>Test"];
+    EXPECT_EQ(WebCore::Color([webView _sampledPageTopColor].CGColor), WebCore::Color::white);
+    // Depending on timing, a notification can be sent for when the main document changes and then
+    // when the new main document renders or both can be coalesced if rendering is fast enough.
+    EXPECT_TRUE(notificationCount == 0 || notificationCount == 2);
+
+    notificationCount = 0;
+
+    [webView synchronouslyLoadHTMLStringAndWaitUntilAllImmediateChildFramesPaint:@"<body>"];
+    EXPECT_NULL([webView _sampledPageTopColor]);
+    EXPECT_EQ(notificationCount, 1UL);
+
+    notificationCount = 0;
+
+    [webView synchronouslyLoadHTMLStringAndWaitUntilAllImmediateChildFramesPaint:@"<body>"];
+    EXPECT_NULL([webView _sampledPageTopColor]);
+    EXPECT_EQ(notificationCount, 0UL);
+
+    notificationCount = 0;
+
+    [webView synchronouslyLoadHTMLStringAndWaitUntilAllImmediateChildFramesPaint:@"<body>Test"];
+    EXPECT_EQ(WebCore::Color([webView _sampledPageTopColor].CGColor), WebCore::Color::white);
+    EXPECT_EQ(notificationCount, 1UL);
+}
_______________________________________________
webkit-changes mailing list
[email protected]
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to