blob: a98f04eab6c578ed5e7b6a49d272b4357049a483 [file] [log] [blame]
/*
* Copyright (c) 2023 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 "CanvasNoiseInjection.h"
#include "ByteArrayPixelBuffer.h"
#include "FloatRect.h"
#include "ImageBuffer.h"
#include "PixelBuffer.h"
#include <algorithm>
namespace WebCore {
void CanvasNoiseInjection::updateDirtyRect(const IntRect& rect)
{
m_postProcessDirtyRect.unite(rect);
}
void CanvasNoiseInjection::clearDirtyRect()
{
m_postProcessDirtyRect = { };
}
static inline bool isIndexInBounds(int size, int index)
{
ASSERT(index <= size);
return index < size;
}
static inline bool setTightnessBounds(std::span<uint8_t> bytes, std::array<int, 4>& tightestBoundingDiff, int index1, int index2, int index3)
{
int boundingRedDiff = std::abs(static_cast<int>(bytes[index1]) - static_cast<int>(bytes[index3]));
int boundingGreenDiff = std::abs(static_cast<int>(bytes[index1 + 1]) - static_cast<int>(bytes[index3 + 1]));
int boundingBlueDiff = std::abs(static_cast<int>(bytes[index1 + 2]) - static_cast<int>(bytes[index3 + 2]));
int boundingAlphaDiff = std::abs(static_cast<int>(bytes[index1 + 3]) - static_cast<int>(bytes[index3 + 3]));
int neighborRedDiff1 = std::abs(static_cast<int>(bytes[index1]) - static_cast<int>(bytes[index2]));
int neighborGreenDiff1 = std::abs(static_cast<int>(bytes[index1 + 1]) - static_cast<int>(bytes[index2 + 1]));
int neighborBlueDiff1 = std::abs(static_cast<int>(bytes[index1 + 2]) - static_cast<int>(bytes[index2 + 2]));
int neighborAlphaDiff1 = std::abs(static_cast<int>(bytes[index1 + 3]) - static_cast<int>(bytes[index2 + 3]));
int neighborRedDiff2 = std::abs(static_cast<int>(bytes[index2]) - static_cast<int>(bytes[index3]));
int neighborGreenDiff2 = std::abs(static_cast<int>(bytes[index2 + 1]) - static_cast<int>(bytes[index3 + 1]));
int neighborBlueDiff2 = std::abs(static_cast<int>(bytes[index2 + 2]) - static_cast<int>(bytes[index3 + 2]));
int neighborAlphaDiff2 = std::abs(static_cast<int>(bytes[index2 + 3]) - static_cast<int>(bytes[index3 + 3]));
bool updatedTightnessBounds { false };
if (boundingRedDiff <= 1 && boundingGreenDiff <= 1 && boundingBlueDiff <= 1 && boundingAlphaDiff <= 1
&& neighborRedDiff1 <= 1 && neighborGreenDiff1 <= 1 && neighborBlueDiff1 <= 1 && neighborAlphaDiff1 <= 1
&& neighborRedDiff2 <= 1 && neighborGreenDiff2 <= 1 && neighborBlueDiff2 <= 1 && neighborAlphaDiff2 <= 1) {
tightestBoundingDiff[0] = 0;
tightestBoundingDiff[1] = 0;
tightestBoundingDiff[2] = 0;
tightestBoundingDiff[3] = 0;
updatedTightnessBounds = true;
} else if (boundingRedDiff < tightestBoundingDiff[0]
&& boundingGreenDiff < tightestBoundingDiff[1]
&& boundingBlueDiff < tightestBoundingDiff[2]
&& boundingAlphaDiff < tightestBoundingDiff[3]) {
tightestBoundingDiff[0] = boundingRedDiff;
tightestBoundingDiff[1] = boundingGreenDiff;
tightestBoundingDiff[2] = boundingBlueDiff;
tightestBoundingDiff[3] = boundingAlphaDiff;
updatedTightnessBounds = true;
}
return updatedTightnessBounds;
}
static std::pair<std::array<int, 4>, std::array<int, 4>> boundingNeighbors(int index, std::span<uint8_t> bytes, const IntSize& size)
{
constexpr auto bytesPerPixel = 4U;
auto bufferSize = bytes.size_bytes();
auto pixelIndex = index / bytesPerPixel;
bool isInTopRow = pixelIndex < static_cast<size_t>(size.width());
bool isInBottomRow = pixelIndex > static_cast<size_t>((size.height() - 1) * size.width());
bool isInLeftColumn = !(pixelIndex % size.width());
bool isInRightColumn = (pixelIndex % size.width()) == static_cast<unsigned>(size.width()) - 1;
bool isInTopLeftCorner = isInTopRow && isInLeftColumn;
bool isInBottomLeftCorner = isInBottomRow && isInLeftColumn;
bool isInTopRightCorner = isInTopRow && isInRightColumn;
bool isInBotomRightCorner = isInBottomRow && isInRightColumn;
constexpr auto leftOffset = -bytesPerPixel;
constexpr auto rightOffset = bytesPerPixel;
const auto aboveOffset = -size.width() * bytesPerPixel;
const auto belowOffset = size.width() * bytesPerPixel;
const auto aboveLeftOffset = aboveOffset + leftOffset;
const auto aboveRightOffset = aboveOffset + rightOffset;
const auto belowLeftOffset = belowOffset + leftOffset;
const auto belowRightOffset = belowOffset + rightOffset;
const int leftIndex = index + leftOffset;
const int rightIndex = index + rightOffset;
const int aboveIndex = index + aboveOffset;
const int belowIndex = index + belowOffset;
const int aboveLeftIndex = index + aboveLeftOffset;
const int aboveRightIndex = index + aboveRightOffset;
const int belowLeftIndex = index + belowLeftOffset;
const int belowRightIndex = index + belowRightOffset;
const auto areColorsRelated = [&bytes, bufferSize](int index1, int index2, int index3) {
constexpr auto maxDistanceThreshold = 8;
if (!isIndexInBounds(bufferSize, index1) || !isIndexInBounds(bufferSize, index1 + 3))
return false;
if (!isIndexInBounds(bufferSize, index2) || !isIndexInBounds(bufferSize, index2 + 3))
return false;
if (!isIndexInBounds(bufferSize, index3) || !isIndexInBounds(bufferSize, index3 + 3))
return false;
bool isColorNearBoundingColor1 = std::abs(static_cast<int>(bytes[index1]) - static_cast<int>(bytes[index2])) <= maxDistanceThreshold
&& std::abs(static_cast<int>(bytes[index1 + 1]) - static_cast<int>(bytes[index2 + 1])) <= maxDistanceThreshold
&& std::abs(static_cast<int>(bytes[index1 + 2]) - static_cast<int>(bytes[index2 + 2])) <= maxDistanceThreshold
&& std::abs(static_cast<int>(bytes[index1 + 3]) - static_cast<int>(bytes[index2 + 3])) <= maxDistanceThreshold;
bool isColorNearBoundingColor2 = std::abs(static_cast<int>(bytes[index3]) - static_cast<int>(bytes[index2])) <= maxDistanceThreshold
&& std::abs(static_cast<int>(bytes[index3 + 1]) - static_cast<int>(bytes[index2 + 1])) <= maxDistanceThreshold
&& std::abs(static_cast<int>(bytes[index3 + 2]) - static_cast<int>(bytes[index2 + 2])) <= maxDistanceThreshold
&& std::abs(static_cast<int>(bytes[index3 + 3]) - static_cast<int>(bytes[index2 + 3])) <= maxDistanceThreshold;
return isColorNearBoundingColor1 || isColorNearBoundingColor2;
};
const auto compareColorsAndSetBounds = [&areColorsRelated](const auto& bytes, auto& tightestBoundingColors, auto& tightestBoundingDiff, auto colorIndex, auto neighborIndex1, auto neighborIndex2) {
if (areColorsRelated(neighborIndex1, colorIndex, neighborIndex2)) {
if (setTightnessBounds(bytes, tightestBoundingDiff, neighborIndex1, colorIndex, neighborIndex2)) {
if (std::ranges::all_of(tightestBoundingDiff, [](auto& item) { return !item; })) {
tightestBoundingColors = {
{ bytes[colorIndex], bytes[colorIndex + 1], bytes[colorIndex + 2], bytes[colorIndex + 3] },
{ bytes[colorIndex], bytes[colorIndex + 1], bytes[colorIndex + 2], bytes[colorIndex + 3] }
};
} else {
tightestBoundingColors = {
{ bytes[neighborIndex1], bytes[neighborIndex1 + 1], bytes[neighborIndex1 + 2], bytes[neighborIndex1 + 3] },
{ bytes[neighborIndex2], bytes[neighborIndex2 + 1], bytes[neighborIndex2 + 2], bytes[neighborIndex2 + 3] }
};
}
}
}
return tightestBoundingColors;
};
std::pair<std::array<int, 4>, std::array<int, 4>> tightestBoundingColors {
{ 0, 0, 0, 0 },
{ 255, 255, 255, 255 }
};
std::array<int, 4> tightestBoundingDiff { 255, 255, 255, 255 };
if (isInTopLeftCorner || isInBottomLeftCorner || isInTopRightCorner || isInBotomRightCorner)
return tightestBoundingColors;
if (isInTopRow || isInBottomRow)
return compareColorsAndSetBounds(bytes, tightestBoundingColors, tightestBoundingDiff, index, leftIndex, rightIndex);
if (isInLeftColumn || isInRightColumn)
return compareColorsAndSetBounds(bytes, tightestBoundingColors, tightestBoundingDiff, index, aboveIndex, belowIndex);
compareColorsAndSetBounds(bytes, tightestBoundingColors, tightestBoundingDiff, index, leftIndex, rightIndex);
compareColorsAndSetBounds(bytes, tightestBoundingColors, tightestBoundingDiff, index, aboveIndex, belowIndex);
compareColorsAndSetBounds(bytes, tightestBoundingColors, tightestBoundingDiff, index, aboveLeftIndex, belowRightIndex);
compareColorsAndSetBounds(bytes, tightestBoundingColors, tightestBoundingDiff, index, aboveRightIndex, belowLeftIndex);
return tightestBoundingColors;
}
void CanvasNoiseInjection::postProcessDirtyCanvasBuffer(ImageBuffer* imageBuffer, NoiseInjectionHashSalt salt, CanvasNoiseInjectionPostProcessArea postProcessArea)
{
if (m_postProcessDirtyRect.isEmpty() && postProcessArea == CanvasNoiseInjectionPostProcessArea::DirtyRect)
return;
if (!imageBuffer)
return;
auto dirtyRect = postProcessArea == CanvasNoiseInjectionPostProcessArea::DirtyRect ? m_postProcessDirtyRect : IntRect(IntPoint::zero(), imageBuffer->truncatedLogicalSize());
PixelBufferFormat format { AlphaPremultiplication::Unpremultiplied, PixelFormat::RGBA8, imageBuffer->colorSpace() };
auto pixelBuffer = imageBuffer->getPixelBuffer(format, dirtyRect);
if (!is<ByteArrayPixelBuffer>(pixelBuffer))
return;
if (postProcessPixelBufferResults(*pixelBuffer, salt)) {
imageBuffer->putPixelBuffer(*pixelBuffer, { IntPoint::zero(), dirtyRect.size() }, dirtyRect.location());
m_postProcessDirtyRect = { };
}
}
static std::pair<int, int> lowerAndUpperBound(int component1, int component2, int component3)
{
if (component1 <= component3) {
if (component1 == component2 || component2 == component3)
return { component2, component2 };
if (component1 <= component2 && component2 <= component3)
return { component1, component3 };
if (component1 >= component2 && component2 <= component3)
return { component2, component1 };
if (component1 <= component2 && component2 >= component3)
return { component3, component2 };
} else if (component1 > component3) {
if (component1 < component2 && component2 > component3)
return { component3, component1 };
if (component1 > component2 && component2 < component3)
return { component2, component3 };
if (component1 < component2 && component2 > component3)
return { component1, component2 };
}
return { component2, component2 };
}
static void adjustNeighborColorBounds(std::array<int, 4>& neighborColor1, const std::array<int, 4>& color, std::array<int, 4>& neighborColor2)
{
auto [redLowerBound, redUpperBound] = lowerAndUpperBound(neighborColor1[0], color[0], neighborColor2[0]);
auto [greenLowerBound, greenUpperBound] = lowerAndUpperBound(neighborColor1[1], color[1], neighborColor2[1]);
auto [blueLowerBound, blueUpperBound] = lowerAndUpperBound(neighborColor1[2], color[2], neighborColor2[2]);
auto [alphaLowerBound, alphaUpperBound] = lowerAndUpperBound(neighborColor1[3], color[3], neighborColor2[3]);
neighborColor1[0] = redLowerBound;
neighborColor2[0] = redUpperBound;
neighborColor1[1] = greenLowerBound;
neighborColor2[1] = greenUpperBound;
neighborColor1[2] = blueLowerBound;
neighborColor2[2] = blueUpperBound;
neighborColor1[3] = alphaLowerBound;
neighborColor2[3] = alphaUpperBound;
}
bool CanvasNoiseInjection::postProcessPixelBufferResults(PixelBuffer& pixelBuffer, NoiseInjectionHashSalt salt) const
{
ASSERT(pixelBuffer.format().pixelFormat == PixelFormat::RGBA8);
constexpr int bytesPerPixel = 4;
auto bytes = pixelBuffer.bytes();
bool wasPixelBufferModified { false };
// Salt value 0 is used for testing.
if (!salt)
return true;
for (size_t i = 0; i < bytes.size_bytes(); i += bytesPerPixel) {
auto& redChannel = bytes[i];
auto& greenChannel = bytes[i + 1];
auto& blueChannel = bytes[i + 2];
auto& alphaChannel = bytes[i + 3];
bool isBlack { !redChannel && !greenChannel && !blueChannel };
if (!alphaChannel)
continue;
const auto clampedColorComponentOffset = [](int colorComponentOffset, int originalComponentValue, int minValue, int maxValue) {
if (originalComponentValue > maxValue)
maxValue = 255;
if (originalComponentValue < minValue)
minValue = 0;
if (colorComponentOffset + originalComponentValue > maxValue)
colorComponentOffset = maxValue - originalComponentValue;
else if (colorComponentOffset + originalComponentValue < minValue)
colorComponentOffset = minValue - originalComponentValue;
return colorComponentOffset;
};
std::array<int, 4> lowerBoundColor { 0, 0, 0, 0 };
std::array<int, 4> upperBoundColor { 255, 255, 255, 255 };
auto [neighborColor1, neighborColor2] = boundingNeighbors(i, bytes, pixelBuffer.size());
// +/- 1 is roughly ~0.5% of the 255 max value.
// +/- 3 is roughly ~1% of the 255 max value.
int maxNoiseValue = 3;
if (neighborColor1 != neighborColor2 && neighborColor1 != lowerBoundColor && neighborColor2 != upperBoundColor) {
maxNoiseValue = 1;
adjustNeighborColorBounds(neighborColor1, { redChannel, greenChannel, blueChannel, alphaChannel }, neighborColor2);
}
lowerBoundColor = neighborColor1;
upperBoundColor = neighborColor2;
const uint64_t pixelHash = computeHash(salt, redChannel, greenChannel, blueChannel, alphaChannel);
auto noiseValue = static_cast<int8_t>(((pixelHash * 2 * maxNoiseValue) / std::numeric_limits<uint32_t>::max()) - maxNoiseValue);
// If alpha is non-zero and the color channels are zero, then only tweak the alpha channel's value;
if (isBlack)
alphaChannel += clampedColorComponentOffset(noiseValue, alphaChannel, lowerBoundColor[3], upperBoundColor[3]);
else {
// If alpha and any of the color channels are non-zero, then tweak all of the channels;
redChannel += clampedColorComponentOffset(noiseValue, redChannel, lowerBoundColor[0], upperBoundColor[0]);
greenChannel += clampedColorComponentOffset(noiseValue, greenChannel, lowerBoundColor[1], upperBoundColor[1]);
blueChannel += clampedColorComponentOffset(noiseValue, blueChannel, lowerBoundColor[2], upperBoundColor[2]);
alphaChannel += clampedColorComponentOffset(noiseValue, alphaChannel, lowerBoundColor[3], upperBoundColor[3]);
}
wasPixelBufferModified = true;
}
return wasPixelBufferModified;
}
} // namespace WebCore