blob: 7874aae9a70a01422335194a6c4ab47402921d23 [file]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/core/canvas_interventions/noise_helper.h"
#include <array>
#include <cstddef>
#include <cstdint>
#include "base/check_op.h"
#include "base/containers/span.h"
#include "base/containers/span_writer.h"
#include "base/numerics/byte_conversions.h"
#include "base/numerics/safe_conversions.h"
#include "third_party/blink/renderer/core/canvas_interventions/noise_hash.h"
namespace blink {
namespace {
constexpr uint8_t kMaxClosePixelDelta = 10u;
constexpr uint8_t kMaxNoisePerChannel = 3u;
constexpr uint8_t kChannelsPerPixel = 4u;
constexpr std::array<uint8_t, 4u> kEmptyPixel({0, 0, 0, 0});
struct PixelLocation {
int x;
int y;
};
void ClampPixelLocation(PixelLocation* location,
const int width,
const int height) {
CHECK(width >= 1 && height >= 1);
location->x = std::min(std::max(0, location->x), width - 1);
location->y = std::min(std::max(0, location->y), height - 1);
}
// Returns two random pixel locations; one close to the offset and one
// randomly selected from the entire canvas.
const std::pair<PixelLocation, PixelLocation> GetRandomPixelLocations(
NoiseHash& token_hash,
const PixelLocation offset,
const int width,
const int height) {
std::pair<PixelLocation, PixelLocation> pixel_locations;
// Uses 2 * log2(kMaxClosePixelDelta*2+1) = 8 bits from hash.
pixel_locations.first = {
offset.x + token_hash.GetValueBelow(kMaxClosePixelDelta * 2) -
kMaxClosePixelDelta + 1,
offset.y + token_hash.GetValueBelow(kMaxClosePixelDelta * 2) -
kMaxClosePixelDelta + 1};
// x and y might be negative or go beyond the width or height here, so we need
// to clamp them.
ClampPixelLocation(&pixel_locations.first, width, height);
// Uses max 2 * log2(kMaximumCanvasSize) = 40 bits from hash.
pixel_locations.second = {token_hash.GetValueBelow(width),
token_hash.GetValueBelow(height)};
// Used at most 48 bits from hash
return pixel_locations;
}
void NoisePixel(base::span<uint8_t> pixel, NoiseHash& token_hash) {
base::SpanWriter writer(base::as_writable_bytes(pixel));
for (int i = 0; i < kChannelsPerPixel; ++i) {
int channel_value = pixel[i];
// Clamp min- and maxNoisedVal to [0, 255] and [1, 255] for the alpha
// channel if it was non-zero before.
int lowerLimit = (i == kChannelsPerPixel - 1 && channel_value > 0) ? 1 : 0;
int minNoisedVal = channel_value <= kMaxNoisePerChannel
? lowerLimit
: channel_value - kMaxNoisePerChannel;
int maxNoisedVal = channel_value >= 255 - kMaxNoisePerChannel
? 255
: channel_value + kMaxNoisePerChannel;
int noise = token_hash.GetValueBelow(
std::min(kMaxNoisePerChannel * 2 + 1, maxNoisedVal - minNoisedVal + 1));
writer.WriteU8LittleEndian(
base::checked_cast<uint8_t>(minNoisedVal + noise));
}
}
base::span<uint8_t, 4u> GetPixelAt(const int x,
const int y,
const int width,
const base::span<uint8_t> pixels) {
return pixels
.subspan(static_cast<size_t>((x + y * width) * kChannelsPerPixel))
.first<4u>();
}
uint64_t GetValueFromPixelLocations(
const std::pair<PixelLocation, PixelLocation> locations,
const base::span<uint8_t> pixels,
int width) {
uint64_t result = 0u;
result |= base::U32FromLittleEndian(
GetPixelAt(locations.first.x, locations.first.y, width, pixels));
result <<= 32u;
result |= base::U32FromLittleEndian(
GetPixelAt(locations.second.x, locations.second.y, width, pixels));
return result;
}
void CopyPixelValue(const base::span<uint8_t> from_pixel,
base::span<uint8_t> to_pixel) {
base::SpanWriter writer(base::as_writable_bytes(to_pixel));
writer.Write(from_pixel.first<kChannelsPerPixel>());
}
} // namespace
void NoisePixels(const NoiseHash& token_hash,
base::span<uint8_t> pixels,
const int width,
const int height) {
CHECK_EQ(pixels.size(),
static_cast<size_t>(width * height * kChannelsPerPixel));
const size_t row_size = width * kChannelsPerPixel;
std::vector<uint8_t> row1(row_size);
std::vector<uint8_t> row2(row_size);
base::span<uint8_t> unnoised_previous_row(row1);
base::span<uint8_t> unnoised_current_row(row2);
for (int y = 0; y < height; ++y) {
unnoised_current_row.copy_from(pixels.subspan(y * row_size, row_size));
for (int x = 0; x < width; ++x) {
auto pixel = GetPixelAt(x, y, width, pixels);
if (pixel == kEmptyPixel) {
continue;
}
if (y > 0 && x > 0 &&
GetPixelAt(x - 1, 0, width, unnoised_previous_row) == pixel) {
// same top-left pixel, copy.
CopyPixelValue(GetPixelAt(x - 1, y - 1, width, pixels), pixel);
} else if (y > 0 &&
GetPixelAt(x, 0, width, unnoised_previous_row) == pixel) {
// same top pixel, copy.
CopyPixelValue(GetPixelAt(x, y - 1, width, pixels), pixel);
} else if (y > 0 && x < width - 1 &&
GetPixelAt(x + 1, 0, width, unnoised_previous_row) == pixel) {
// same top-right pixel, copy.
CopyPixelValue(GetPixelAt(x + 1, y - 1, width, pixels), pixel);
} else if (x > 0 &&
GetPixelAt(x - 1, 0, width, unnoised_current_row) == pixel) {
// same left pixel, copy.
CopyPixelValue(GetPixelAt(x - 1, y, width, pixels), pixel);
} else {
// otherwise, noise the pixel.
NoiseHash hash_copy = token_hash;
hash_copy.Update(base::U32FromLittleEndian(pixel));
// GetRandomPixelLocations consumes at most 46 bits from hash.
const std::pair<PixelLocation, PixelLocation> other_pixels =
GetRandomPixelLocations(hash_copy, {x, y}, width, height);
hash_copy.Update(
GetValueFromPixelLocations(other_pixels, pixels, width));
// NoisePixel consumes 12 bits from hash
NoisePixel(pixel, hash_copy);
}
}
// Previous can be overwritten (new current). Current is the now previous.
std::swap(unnoised_previous_row, unnoised_current_row);
}
}
} // namespace blink