284 lines
11 KiB
C++
284 lines
11 KiB
C++
/*
|
|
* 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 "Element.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 "Logging.h"
|
|
#include "Node.h"
|
|
#include "Page.h"
|
|
#include "PixelBuffer.h"
|
|
#include "RegistrableDomain.h"
|
|
#include "RenderImage.h"
|
|
#include "RenderObject.h"
|
|
#include "RenderStyle.h"
|
|
#include "Settings.h"
|
|
#include "Styleable.h"
|
|
#include "WebAnimation.h"
|
|
#include <wtf/ListHashSet.h>
|
|
#include <wtf/OptionSet.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;
|
|
|
|
if (!is<Element>(node))
|
|
continue;
|
|
|
|
auto& element = downcast<Element>(node);
|
|
auto styleable = Styleable::fromElement(element);
|
|
|
|
// Skip nodes with animations as the sample may get an odd color if the animation is in-progress.
|
|
if (styleable.hasRunningTransitions())
|
|
return false;
|
|
if (auto* animations = styleable.animations()) {
|
|
for (auto& animation : *animations) {
|
|
if (!animation)
|
|
continue;
|
|
if (animation->playState() == WebAnimation::PlayState::Running)
|
|
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>(element) && downcast<HTMLCanvasElement>(element).renderingContext())
|
|
return false;
|
|
|
|
// Skip 3rd-party `<iframe>` as the content likely won't match the rest of the page.
|
|
if (is<HTMLIFrameElement>(element) && !areRegistrableDomainsEqual(downcast<HTMLIFrameElement>(element).location(), document.url()))
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static std::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 std::nullopt;
|
|
|
|
// FIXME: <https://webkit.org/b/225942> (Sampled Page Top Color: support sampling non-RGB values like P3)
|
|
auto colorSpace = DestinationColorSpace::SRGB();
|
|
|
|
ASSERT(document.view());
|
|
auto snapshot = snapshotFrameRect(document.view()->frame(), IntRect(location, IntSize(1, 1)), { { SnapshotFlags::ExcludeSelectionHighlighting, SnapshotFlags::PaintEverythingExcludingSelection }, PixelFormat::BGRA8, colorSpace });
|
|
if (!snapshot)
|
|
return std::nullopt;
|
|
|
|
auto pixelBuffer = snapshot->getPixelBuffer({ AlphaPremultiplication::Unpremultiplied, PixelFormat::BGRA8, colorSpace }, { { }, snapshot->logicalSize() });
|
|
if (!pixelBuffer)
|
|
return std::nullopt;
|
|
|
|
if (pixelBuffer->data().length() < 4)
|
|
return std::nullopt;
|
|
|
|
auto snapshotData = pixelBuffer->data().data();
|
|
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,
|
|
};
|
|
}
|
|
|
|
std::optional<Color> PageColorSampler::sampleTop(Page& page)
|
|
{
|
|
// If `std::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 std::nullopt;
|
|
|
|
auto frameView = makeRefPtr(page.mainFrame().view());
|
|
if (!frameView)
|
|
return std::nullopt;
|
|
|
|
// Don't take samples if the layer tree is still frozen.
|
|
if (frameView->needsLayout())
|
|
return std::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 std::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
|