blob: 4f74869cd5d0da5e332f26544865e1f2444464ae [file] [log] [blame]
// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// -----------------------------------------------------------------------------
//
// WP2ArgbBuffer
//
// Author: Skal (pascal.massimino@gmail.com)
#include <algorithm>
#include <cassert>
#include <cstdint>
#include <cstring> // for memcpy()
#include "src/common/color_precision.h"
#include "src/dsp/dsp.h"
#include "src/dsp/math.h"
#include "src/utils/utils.h"
#include "src/wp2/base.h"
#include "src/wp2/format_constants.h"
namespace WP2 {
static WP2Status CheckDimensions(uint32_t width, uint32_t height,
uint32_t stride, WP2SampleFormat format) {
WP2_CHECK_OK(width > 0 && height > 0, WP2_STATUS_BAD_DIMENSION);
WP2_CHECK_OK(width <= kMaxBufferDimension, WP2_STATUS_BAD_DIMENSION);
WP2_CHECK_OK(height <= kMaxBufferDimension, WP2_STATUS_BAD_DIMENSION);
WP2_CHECK_OK((uint64_t)width * height <= kMaxBufferArea,
WP2_STATUS_BAD_DIMENSION);
WP2_CHECK_OK((uint64_t)stride >= (uint64_t)WP2FormatBpp(format) * width,
WP2_STATUS_BAD_DIMENSION);
#if defined(WP2_MAX_ARGB_MEMORY)
WP2_CHECK_OK((uint64_t)stride * height <= WP2_MAX_ARGB_MEMORY,
WP2_STATUS_OUT_OF_MEMORY);
#endif
return WP2_STATUS_OK;
}
//------------------------------------------------------------------------------
Rectangle Rectangle::ClipWith(const Rectangle& r) const {
const uint32_t x_end = std::min(x + width, r.x + r.width);
const uint32_t y_end = std::min(y + height, r.y + r.height);
Rectangle clipped;
clipped.x = std::max(x, r.x);
clipped.y = std::max(y, r.y);
clipped.width = (x_end > clipped.x) ? x_end - clipped.x : 0;
clipped.height = (y_end > clipped.y) ? y_end - clipped.y : 0;
return clipped;
}
Rectangle Rectangle::MergeWith(const Rectangle& r) const {
Rectangle merged;
merged.x = std::min(x, r.x);
merged.y = std::min(y, r.y);
merged.width = std::max(x + width, r.x + r.width) - merged.x;
merged.height = std::max(y + height, r.y + r.height) - merged.y;
return merged;
}
void Rectangle::Exclude(const Rectangle& r, Rectangle* const top,
Rectangle* const bot, Rectangle* const lft,
Rectangle* const rgt) const {
const Rectangle excluded = r.ClipWith(*this);
*top = {x, y, width, excluded.y - y};
*bot = {x, excluded.y + excluded.height, width,
(y + height) - (excluded.y + excluded.height)};
*lft = {x, excluded.y, excluded.x - x, excluded.height};
*rgt = {excluded.x + excluded.width, excluded.y,
(x + width) - (excluded.x + excluded.width), excluded.height};
}
//------------------------------------------------------------------------------
ArgbBuffer::ArgbBuffer(WP2SampleFormat format_in) noexcept
: format_(format_in) {}
ArgbBuffer::ArgbBuffer(ArgbBuffer&& other) noexcept : format_(other.format()) {
TrivialMoveCtor(this, &other);
}
ArgbBuffer& ArgbBuffer::operator=(ArgbBuffer&& other) noexcept {
this->Deallocate();
TrivialMoveCtor(this, &other);
return *this;
}
void ArgbBuffer::Deallocate() {
if (!is_external_memory_) {
WP2Free(private_memory_);
private_memory_ = nullptr;
} else {
is_external_memory_ = false;
}
width_ = 0;
height_ = 0;
stride_ = 0;
metadata_.Clear();
pixels_ = nullptr;
is_slow_memory_ = false;
}
const void* ArgbBuffer::GetRow(uint32_t y) const {
return (WP2Formatbpc(format_) <= 8) ? (const void*)GetRow8(y)
: (const void*)GetRow16(y);
}
void* ArgbBuffer::GetRow(uint32_t y) {
return (WP2Formatbpc(format_) <= 8) ? (void*)GetRow8(y) : (void*)GetRow16(y);
}
const uint8_t* ArgbBuffer::GetRow8(uint32_t y) const {
assert(y < height_ && WP2Formatbpc(format_) <= 8);
return (const uint8_t*)pixels_ + y * stride_;
}
uint8_t* ArgbBuffer::GetRow8(uint32_t y) {
assert(y < height_ && WP2Formatbpc(format_) <= 8);
return (uint8_t*)pixels_ + y * stride_;
}
const uint16_t* ArgbBuffer::GetRow16(uint32_t y) const {
assert(y < height_ && WP2Formatbpc(format_) > 8);
return (uint16_t*)((const uint8_t*)pixels_ + y * stride_);
}
uint16_t* ArgbBuffer::GetRow16(uint32_t y) {
assert(y < height_ && WP2Formatbpc(format_) > 8);
return (uint16_t*)((uint8_t*)pixels_ + y * stride_);
}
const uint8_t* ArgbBuffer::GetPosition(uint32_t x, uint32_t y) const {
assert(y < height_ && x < width_);
return ((const uint8_t*)pixels_ + y * stride_ + x * WP2FormatBpp(format_));
}
uint8_t* ArgbBuffer::GetPosition(uint32_t x, uint32_t y) {
assert(y < height_ && x < width_);
return ((uint8_t*)pixels_ + y * stride_ + x * WP2FormatBpp(format_));
}
WP2Status ArgbBuffer::SetFormat(WP2SampleFormat format_in) {
if (format_ != format_in) {
WP2_CHECK_OK(IsEmpty(), WP2_STATUS_INVALID_PARAMETER);
format_ = format_in;
}
return WP2_STATUS_OK;
}
WP2Status ArgbBuffer::Resize(uint32_t new_width, uint32_t new_height) {
const uint32_t new_stride = new_width * WP2FormatBpp(format_);
WP2_CHECK_STATUS(CheckDimensions(new_width, new_height, new_stride, format_));
if (width_ == new_width && height_ == new_height) {
metadata_.Clear();
return WP2_STATUS_OK;
}
// For external memory, we can't reallocate the buffer nor change the layout.
WP2_CHECK_OK(!is_external_memory_, WP2_STATUS_BAD_DIMENSION);
if (width_ == new_height && height_ == new_width) {
// Changing layout is enough.
width_ = new_width;
height_ = new_height;
stride_ = new_stride;
metadata_.Clear();
return WP2_STATUS_OK;
}
Deallocate();
void* const new_pixels = WP2Malloc(new_height, new_stride);
WP2_CHECK_ALLOC_OK(new_pixels != nullptr);
pixels_ = new_pixels;
width_ = new_width;
height_ = new_height;
stride_ = new_stride;
is_external_memory_ = false;
private_memory_ = new_pixels;
is_slow_memory_ = false;
return WP2_STATUS_OK;
}
//------------------------------------------------------------------------------
// Import functions
WP2Status ArgbBuffer::CopyFrom(const ArgbBuffer& src) {
WP2_CHECK_OK(src.format() == format_, WP2_STATUS_INVALID_COLORSPACE);
if (src.IsEmpty()) {
Deallocate();
return WP2_STATUS_OK;
}
WP2_CHECK_STATUS(Resize(src.width_, src.height_));
const size_t dst_stride = src.width_ * WP2FormatBpp(format_);
for (uint32_t y = 0; y < height_; ++y) {
std::memcpy(GetRow(y), src.GetRow(y), dst_stride);
}
return WP2_STATUS_OK;
}
WP2Status ArgbBuffer::ConvertFrom(const ArgbBuffer& src) {
if (format_ == src.format()) return CopyFrom(src);
WP2ArgbConverterInit();
const WP2ArgbConverterF convert =
WP2ConversionFunction(src.format(), format_);
WP2_CHECK_OK(convert != nullptr, WP2_STATUS_INVALID_COLORSPACE);
WP2_CHECK_STATUS(Resize(src.width_, src.height_));
for (uint32_t y = 0; y < height_; ++y) {
convert(src.GetRow(y), width_, GetRow(y));
}
return WP2_STATUS_OK;
}
WP2Status ArgbBuffer::Import(WP2SampleFormat input_format, uint32_t new_width,
uint32_t new_height, const uint8_t* samples,
uint32_t samples_stride) {
WP2ArgbConverterInit();
const WP2ArgbConverterF convert =
WP2ConversionFunction(input_format, format_);
WP2_CHECK_OK(convert != nullptr, WP2_STATUS_INVALID_COLORSPACE);
WP2_CHECK_STATUS(Resize(new_width, new_height));
for (uint32_t y = 0; y < new_height; ++y) {
convert(samples, new_width, GetRow(y));
samples += samples_stride;
}
return WP2_STATUS_OK;
}
WP2Status ArgbBuffer::ImportRow(WP2SampleFormat input_format,
uint32_t row_index, const uint8_t* samples) {
WP2_CHECK_OK(samples != nullptr, WP2_STATUS_NULL_PARAMETER);
WP2_CHECK_OK(row_index <= height_, WP2_STATUS_INVALID_PARAMETER);
WP2ArgbConverterInit();
const WP2ArgbConverterF convert =
WP2ConversionFunction(input_format, format_);
WP2_CHECK_OK(convert != nullptr, WP2_STATUS_INVALID_COLORSPACE);
convert(samples, width_, GetRow(row_index));
return WP2_STATUS_OK;
}
void ArgbBuffer::Fill(const Rectangle& window, Argb32b color) {
assert(format_ == WP2_Argb_32 || format_ == WP2_ARGB_32);
assert(CheckPremultiplied(color));
uint8_t bytes[4];
ToUInt8(color, bytes);
// Unmultiply.
if (format_ == WP2_ARGB_32 && color.a != 255u && color.a != 0u) {
for (uint32_t c = 1; c <= 3; ++c) {
bytes[c] = DivRound<uint16_t>(bytes[c] * 255u, bytes[0]);
}
}
Fill(window, bytes);
}
void ArgbBuffer::Fill(const Rectangle& window, Argb38b color) {
assert(format_ == WP2_Argb_38);
assert(CheckPremultiplied(color));
const uint16_t color16[] = {color.a, color.r, color.g, color.b};
Fill(window, color16);
}
void ArgbBuffer::Fill(const Rectangle& window, const uint8_t color[]) {
assert(WP2Formatbpc(format_) <= 8);
FillImpl(window, color);
}
void ArgbBuffer::Fill(const Rectangle& window, const uint16_t color[]) {
assert(WP2Formatbpc(format_) > 8);
FillImpl(window, color);
}
void ArgbBuffer::Fill(const Argb32b color) { Fill(AsRect(), color); }
void ArgbBuffer::Fill(const Argb38b color) { Fill(AsRect(), color); }
void ArgbBuffer::FillImpl(const Rectangle& window, const void* color) {
const Rectangle r = window.ClipWith(AsRect());
const uint32_t pixel_depth = WP2FormatBpp(format_);
if (r.width == 0 || r.height == 0) return;
// Even if channels are uint16_t, uint8_t is used as the stride and
// pixel_depth take care of memory increments.
uint8_t* dst_row = GetPosition(r.x, r.y);
for (uint32_t y = 0; y < r.height; ++y) {
uint8_t* dst_pixel = dst_row;
for (uint32_t x = 0; x < r.width; ++x) {
std::memcpy(dst_pixel, color, pixel_depth);
dst_pixel += pixel_depth;
}
dst_row += stride_;
}
}
void ArgbBuffer::DrawRect(const Rectangle& window, Argb32b color,
uint32_t edges_to_draw) {
assert(CheckPremultiplied(color));
// top
if (edges_to_draw & 1) Fill({window.x, window.y, window.width, 1}, color);
// left
if (edges_to_draw & 2) Fill({window.x, window.y, 1, window.height}, color);
// bottom
if (edges_to_draw & 4)
Fill({window.x, window.y + window.height - 1, window.width, 1}, color);
// right
if (edges_to_draw & 8)
Fill({window.x + window.width - 1, window.y, 1, window.height}, color);
// mid horizontal
if (edges_to_draw & 16)
Fill({window.x, window.y + window.height / 2, window.width, 1}, color);
// mid vertical
if (edges_to_draw & 32)
Fill({window.x + window.width / 2, window.y, 1, window.height}, color);
}
WP2Status ArgbBuffer::CompositeOver(Argb32b background) {
WP2_CHECK_OK(format_ == WP2_Argb_32, WP2_STATUS_INVALID_COLORSPACE);
WP2_CHECK_OK(CheckPremultiplied(background), WP2_STATUS_INVALID_PARAMETER);
for (uint32_t y = 0; y < height_; ++y) {
uint8_t* const row = GetRow8(y);
for (uint32_t x = 0; x < width_; ++x) {
const uint8_t b = kAlphaMax - row[4 * x + 0]; // ~ 255 * (1-alpha)
row[4 * x + 0] += DivBy255(b * background.a);
row[4 * x + 1] += DivBy255(b * background.r);
row[4 * x + 2] += DivBy255(b * background.g);
row[4 * x + 3] += DivBy255(b * background.b);
}
}
return WP2_STATUS_OK;
}
static WP2Status CompositeBuffers(const ArgbBuffer& background,
const ArgbBuffer& foreground,
const Rectangle& window,
ArgbBuffer* const dest) {
WP2_CHECK_OK(
background.format() == WP2_Argb_32 || background.format() == WP2_ARGB_32,
WP2_STATUS_UNSUPPORTED_FEATURE);
WP2_CHECK_OK(foreground.format() == background.format(),
WP2_STATUS_INVALID_COLORSPACE);
WP2_CHECK_OK(foreground.width() == background.width(),
WP2_STATUS_INVALID_PARAMETER);
WP2_CHECK_OK(foreground.height() == background.height(),
WP2_STATUS_INVALID_PARAMETER);
const Rectangle w = (window.GetArea() > 0) ? window : background.AsRect();
WP2_CHECK_OK(window.x + window.width <= background.width(),
WP2_STATUS_INVALID_PARAMETER);
WP2_CHECK_OK(window.y + window.height <= background.height(),
WP2_STATUS_INVALID_PARAMETER);
for (uint32_t y = w.y; y < w.y + w.height; ++y) {
const uint8_t* const fg_row = foreground.GetPosition(w.x, y);
const uint8_t* const bg_row = background.GetPosition(w.x, y);
uint8_t* const dest_row = dest->GetPosition(w.x, y);
// See https://en.wikipedia.org/wiki/Alpha_compositing.
if (WP2IsPremultiplied(foreground.format())) {
for (uint32_t x = 0; x < w.width; ++x) {
const uint8_t b = kAlphaMax - fg_row[4 * x + 0]; // ~ 255 * (1-alpha)
for (uint32_t c = 0; c < 4; ++c) {
dest_row[4 * x + c] =
fg_row[4 * x + c] + DivBy255(b * bg_row[4 * x + c]);
}
}
} else {
for (uint32_t x = 0; x < w.width; ++x) {
const uint8_t b = kAlphaMax - fg_row[4 * x + 0]; // ~ 255 * (1-alpha)
const uint8_t fg_alpha = fg_row[4 * x + 0];
if (fg_alpha == 0) {
for (uint32_t c = 0; c < 4; ++c) {
dest_row[4 * x + c] = bg_row[4 * x + c];
}
} else {
const uint8_t bg_alpha = bg_row[4 * x + 0];
const uint8_t dest_alpha = fg_alpha + DivBy255(b * bg_row[4 * x + 0]);
dest_row[4 * x + 0] = dest_alpha;
for (uint32_t c = 1; c < 4; ++c) {
dest_row[4 * x + c] = DivRound<int32_t>(
fg_row[4 * x + c] * fg_alpha +
DivBy255(b * bg_row[4 * x + c] * bg_alpha),
dest_alpha);
}
}
}
}
}
return WP2_STATUS_OK;
}
WP2Status ArgbBuffer::CompositeOver(const ArgbBuffer& background) {
return CompositeBuffers(background, /*foreground=*/*this, /*window=*/{},
/*dest=*/this);
}
WP2Status ArgbBuffer::CompositeUnder(const ArgbBuffer& foreground,
const Rectangle& window) {
return CompositeBuffers(/*background=*/*this, foreground, window,
/*dest=*/this);
}
WP2Status ArgbBuffer::SetView(const ArgbBuffer& src) {
return SetView(src, src.AsRect());
}
WP2Status ArgbBuffer::SetView(const ArgbBuffer& src, const Rectangle& window) {
WP2_CHECK_OK(src.format() == format_, WP2_STATUS_INVALID_COLORSPACE);
WP2_CHECK_STATUS(
CheckDimensions(window.width, window.height, src.stride_, src.format()));
WP2_CHECK_OK(window.x + window.width <= src.width_,
WP2_STATUS_INVALID_PARAMETER);
WP2_CHECK_OK(window.y + window.height <= src.height_,
WP2_STATUS_INVALID_PARAMETER);
if (!IsEmpty() && !IsView() && !src.IsEmpty() && src.IsView()) {
// 'this' cannot be a view of 'src' if 'src' is already a view of 'this'.
const void* end_pixels =
(const uint8_t*)pixels_ +
(SafeSub(height_, 1u) * stride_ + width_ * WP2FormatBpp(format_));
WP2_CHECK_OK(src.pixels_ < pixels_ || src.pixels_ >= end_pixels,
WP2_STATUS_INVALID_PARAMETER);
}
if (&src != this) Deallocate();
pixels_ = (void*)src.GetPosition(window.x, window.y);
width_ = window.width;
height_ = window.height;
stride_ = src.stride_;
is_external_memory_ = (&src != this) || IsView();
is_slow_memory_ = src.is_slow_memory_;
return WP2_STATUS_OK;
}
WP2Status ArgbBuffer::SetExternalImpl(uint32_t new_width, uint32_t new_height,
void* samples, uint32_t new_stride,
bool is_slow) {
WP2_CHECK_OK(samples != nullptr, WP2_STATUS_NULL_PARAMETER);
WP2_CHECK_STATUS(CheckDimensions(new_width, new_height, new_stride, format_));
Deallocate();
width_ = new_width;
height_ = new_height;
pixels_ = samples;
stride_ = new_stride;
is_external_memory_ = true;
is_slow_memory_ = is_slow;
return WP2_STATUS_OK;
}
WP2Status ArgbBuffer::SetExternal(uint32_t new_width, uint32_t new_height,
uint8_t* samples, uint32_t new_stride,
bool is_slow) {
WP2_CHECK_OK(WP2Formatbpc(format_) <= 8, WP2_STATUS_INVALID_COLORSPACE);
WP2_CHECK_STATUS(
SetExternalImpl(new_width, new_height, samples, new_stride, is_slow));
return WP2_STATUS_OK;
}
WP2Status ArgbBuffer::SetExternal(uint32_t new_width, uint32_t new_height,
uint16_t* samples, uint32_t new_stride,
bool is_slow) {
WP2_CHECK_OK(WP2Formatbpc(format_) > 8, WP2_STATUS_INVALID_COLORSPACE);
WP2_CHECK_STATUS(
SetExternalImpl(new_width, new_height, samples, new_stride, is_slow));
return WP2_STATUS_OK;
}
//------------------------------------------------------------------------------
bool ArgbBuffer::HasTransparency() const {
if (IsEmpty()) return false;
uint32_t alpha_channel_index;
if (!WP2FormatHasAlpha(format_, &alpha_channel_index)) return false;
if (WP2Formatbpc(format_) <= 8) {
WP2AlphaInit();
for (uint32_t y = 0; y < height_; ++y) {
const uint8_t* const row = GetRow8(y) + alpha_channel_index;
if (WP2HasOtherValue8b32b(row, width_, 0xff)) return true;
}
} else {
const uint32_t alpha_max = FormatMax(format_, 0);
for (uint32_t y = 0; y < height_; ++y) {
const uint16_t* const row = GetRow16(y) + alpha_channel_index;
for (uint32_t x = 0; x < width_; ++x) {
if (row[4 * x + 0] != alpha_max) return true;
}
}
}
return false;
}
//------------------------------------------------------------------------------
void swap(ArgbBuffer& a, ArgbBuffer& b) {
using std::swap;
swap(a.format_, b.format_);
swap(a.width_, b.width_);
swap(a.height_, b.height_);
swap(a.stride_, b.stride_);
swap(a.metadata_, b.metadata_);
swap(a.pixels_, b.pixels_);
swap(a.is_external_memory_, b.is_external_memory_);
swap(a.private_memory_, b.private_memory_);
swap(a.is_slow_memory_, b.is_slow_memory_);
}
//------------------------------------------------------------------------------
void ArgbBuffer::SimpleHalfDownsample() {
assert(WP2Formatbpc(format_) <= 8); // not supported yet
const uint32_t half_w = (width_ + 1) >> 1;
const uint32_t half_w_low = width_ >> 1;
const uint32_t half_h = (height_ + 1) >> 1;
for (uint32_t y = 0; y < half_h; ++y) {
const uint8_t* row1 = GetRow8(2 * y + 0);
const uint32_t next_row = (2 * y + 1 == height_) ? 0 : 1;
const uint8_t* row2 = GetRow8(2 * y + next_row);
uint8_t* dst = GetRow8(y);
for (uint32_t x = 0; x < half_w_low; ++x) {
// TODO(skal): implement in dsp/
for (uint32_t c = 0; c < 4; ++c) {
*dst++ =
(row1[c + 0] + row1[c + 4] + row2[c + 0] + row2[c + 4] + 2) >> 2;
}
row1 += 2 * 4;
row2 += 2 * 4;
}
if (half_w != half_w_low) {
for (uint32_t c = 0; c < 4; ++c) dst[c] = row1[c];
}
}
const WP2Status status = SetView(*this, {0, 0, half_w, half_h});
(void)status;
assert(status == WP2_STATUS_OK); // should never fail
}
//------------------------------------------------------------------------------
} // namespace WP2