Files
panopainter/src/assets/ppi_header.cpp

889 lines
37 KiB
C++

#include "assets/ppi_header.h"
#include "assets/image_metadata.h"
#include "foundation/binary_stream.h"
#include <array>
#include <bit>
#include <cmath>
#include <limits>
#include <string>
#include <string_view>
#include <utility>
namespace pp::assets {
namespace {
[[nodiscard]] pp::foundation::Result<std::uint32_t> read_u32(pp::foundation::ByteReader& reader) noexcept
{
return reader.read_u32_le();
}
[[nodiscard]] pp::foundation::Result<std::uint32_t> read_positive_i32(
pp::foundation::ByteReader& reader,
const char* message) noexcept
{
const auto value = reader.read_u32_le();
if (!value) {
return value;
}
if (value.value() > static_cast<std::uint32_t>(std::numeric_limits<std::int32_t>::max())) {
return pp::foundation::Result<std::uint32_t>::failure(
pp::foundation::Status::out_of_range(message));
}
return value;
}
[[nodiscard]] pp::foundation::Result<float> read_f32(pp::foundation::ByteReader& reader) noexcept
{
const auto bits = reader.read_u32_le();
if (!bits) {
return pp::foundation::Result<float>::failure(bits.status());
}
return pp::foundation::Result<float>::success(std::bit_cast<float>(bits.value()));
}
void append_u32(std::vector<std::byte>& bytes, std::uint32_t value)
{
bytes.push_back(static_cast<std::byte>(value & 0xffU));
bytes.push_back(static_cast<std::byte>((value >> 8U) & 0xffU));
bytes.push_back(static_cast<std::byte>((value >> 16U) & 0xffU));
bytes.push_back(static_cast<std::byte>((value >> 24U) & 0xffU));
}
void append_f32(std::vector<std::byte>& bytes, float value)
{
append_u32(bytes, std::bit_cast<std::uint32_t>(value));
}
void append_ascii(std::vector<std::byte>& bytes, std::string_view value)
{
for (const auto ch : value) {
bytes.push_back(static_cast<std::byte>(ch));
}
}
[[nodiscard]] pp::foundation::Status skip_bytes(
pp::foundation::ByteReader& reader,
std::size_t bytes) noexcept
{
const auto skipped = reader.read_bytes(bytes);
if (!skipped) {
return skipped.status();
}
return pp::foundation::Status::success();
}
[[nodiscard]] pp::foundation::Status validate_canvas_size(std::uint32_t width, std::uint32_t height) noexcept
{
if (width == 0 || height == 0) {
return pp::foundation::Status::invalid_argument("PPI canvas dimensions must be greater than zero");
}
if (width > max_ppi_canvas_dimension || height > max_ppi_canvas_dimension) {
return pp::foundation::Status::out_of_range("PPI canvas dimensions exceed the configured limit");
}
return pp::foundation::Status::success();
}
[[nodiscard]] std::string generated_layer_name(std::string_view base_name, std::uint32_t layer_index, std::uint32_t layer_count)
{
if (layer_count == 1U) {
return std::string(base_name);
}
std::string name(base_name);
name.push_back(' ');
name += std::to_string(layer_index + 1U);
return name;
}
[[nodiscard]] pp::foundation::Status add_payload_bytes(PpiBodySummary& summary, std::uint32_t bytes) noexcept
{
const auto next = summary.compressed_face_bytes + static_cast<std::uint64_t>(bytes);
if (next > max_ppi_face_payload_bytes) {
return pp::foundation::Status::out_of_range("PPI compressed face payload exceeds the configured limit");
}
summary.compressed_face_bytes = next;
return pp::foundation::Status::success();
}
[[nodiscard]] pp::foundation::Result<ImageMetadata> validate_face_png_payload(
std::span<const std::byte> payload,
std::uint32_t width,
std::uint32_t height) noexcept
{
const auto metadata = parse_png_metadata(payload);
if (!metadata) {
return pp::foundation::Result<ImageMetadata>::failure(metadata.status());
}
if (metadata.value().width != width || metadata.value().height != height) {
return pp::foundation::Result<ImageMetadata>::failure(
pp::foundation::Status::invalid_argument("PPI face PNG dimensions do not match the dirty box"));
}
if (metadata.value().bit_depth != 8U || metadata.value().components != 4U
|| metadata.value().color_type != ImageColorType::rgba) {
return pp::foundation::Result<ImageMetadata>::failure(
pp::foundation::Status::invalid_argument("PPI face PNG payload must be 8-bit RGBA"));
}
return pp::foundation::Result<ImageMetadata>::success(metadata.value());
}
}
pp::foundation::Result<PpiHeaderInfo> parse_ppi_header(std::span<const std::byte> bytes) noexcept
{
if (bytes.size() < ppi_header_size) {
return pp::foundation::Result<PpiHeaderInfo>::failure(
pp::foundation::Status::out_of_range("PPI header is truncated"));
}
pp::foundation::ByteReader reader(bytes.subspan(0, ppi_header_size));
const auto magic = reader.read_bytes(4);
if (!magic || magic.value()[0] != std::byte { 'P' } || magic.value()[1] != std::byte { 'P' }
|| magic.value()[2] != std::byte { 'I' } || magic.value()[3] != std::byte { 0 }) {
return pp::foundation::Result<PpiHeaderInfo>::failure(
pp::foundation::Status::invalid_argument("PPI header magic is invalid"));
}
PpiHeaderInfo info;
const auto doc_major = read_u32(reader);
const auto doc_minor = read_u32(reader);
const auto soft_major = read_u32(reader);
const auto soft_minor = read_u32(reader);
const auto soft_fix = read_u32(reader);
const auto soft_build = read_u32(reader);
const auto thumb_width = read_u32(reader);
const auto thumb_height = read_u32(reader);
const auto thumb_components = read_u32(reader);
if (!doc_major || !doc_minor || !soft_major || !soft_minor || !soft_fix || !soft_build
|| !thumb_width || !thumb_height || !thumb_components) {
return pp::foundation::Result<PpiHeaderInfo>::failure(
pp::foundation::Status::out_of_range("PPI header is truncated"));
}
info.document_version = { doc_major.value(), doc_minor.value() };
info.software_version = {
soft_major.value(),
soft_minor.value(),
soft_fix.value(),
soft_build.value(),
};
info.thumbnail = {
thumb_width.value(),
thumb_height.value(),
thumb_components.value(),
};
if (info.document_version.major != 0 || info.document_version.minor < 1) {
return pp::foundation::Result<PpiHeaderInfo>::failure(
pp::foundation::Status::invalid_argument("PPI document version is unsupported"));
}
if (info.thumbnail.width != 128 || info.thumbnail.height != 128 || info.thumbnail.components != 4) {
return pp::foundation::Result<PpiHeaderInfo>::failure(
pp::foundation::Status::invalid_argument("PPI thumbnail descriptor is invalid"));
}
return pp::foundation::Result<PpiHeaderInfo>::success(info);
}
pp::foundation::Result<std::size_t> ppi_thumbnail_byte_size(PpiThumbnailInfo thumbnail) noexcept
{
if (thumbnail.width == 0 || thumbnail.height == 0 || thumbnail.components == 0) {
return pp::foundation::Result<std::size_t>::failure(
pp::foundation::Status::invalid_argument("PPI thumbnail descriptor is invalid"));
}
const auto width = static_cast<std::uint64_t>(thumbnail.width);
const auto height = static_cast<std::uint64_t>(thumbnail.height);
const auto components = static_cast<std::uint64_t>(thumbnail.components);
if (width > std::numeric_limits<std::uint64_t>::max() / height) {
return pp::foundation::Result<std::size_t>::failure(
pp::foundation::Status::out_of_range("PPI thumbnail byte size overflows"));
}
const auto pixels = width * height;
if (pixels > std::numeric_limits<std::uint64_t>::max() / components) {
return pp::foundation::Result<std::size_t>::failure(
pp::foundation::Status::out_of_range("PPI thumbnail byte size overflows"));
}
const auto bytes = pixels * components;
if (bytes > static_cast<std::uint64_t>(std::numeric_limits<std::size_t>::max())) {
return pp::foundation::Result<std::size_t>::failure(
pp::foundation::Status::out_of_range("PPI thumbnail byte size exceeds addressable memory"));
}
return pp::foundation::Result<std::size_t>::success(static_cast<std::size_t>(bytes));
}
pp::foundation::Result<PpiProjectLayout> parse_ppi_project_layout(std::span<const std::byte> bytes) noexcept
{
const auto header = parse_ppi_header(bytes);
if (!header) {
return pp::foundation::Result<PpiProjectLayout>::failure(header.status());
}
const auto thumbnail_bytes = ppi_thumbnail_byte_size(header.value().thumbnail);
if (!thumbnail_bytes) {
return pp::foundation::Result<PpiProjectLayout>::failure(thumbnail_bytes.status());
}
if (thumbnail_bytes.value() > std::numeric_limits<std::size_t>::max() - ppi_header_size) {
return pp::foundation::Result<PpiProjectLayout>::failure(
pp::foundation::Status::out_of_range("PPI thumbnail byte size overflows"));
}
const auto body_offset = ppi_header_size + thumbnail_bytes.value();
if (bytes.size() < body_offset) {
return pp::foundation::Result<PpiProjectLayout>::failure(
pp::foundation::Status::out_of_range("PPI thumbnail payload is truncated"));
}
return pp::foundation::Result<PpiProjectLayout>::success(PpiProjectLayout {
.header = header.value(),
.thumbnail_offset = ppi_header_size,
.thumbnail_bytes = thumbnail_bytes.value(),
.body_offset = body_offset,
.body_bytes = bytes.size() - body_offset,
});
}
namespace {
pp::foundation::Result<PpiBodySummary> parse_ppi_body_impl(
PpiHeaderInfo header,
std::span<const std::byte> body,
PpiBodyIndex* index) noexcept
{
if (index != nullptr) {
index->summary = {};
index->layers.clear();
}
pp::foundation::ByteReader reader(body);
const auto width = read_positive_i32(reader, "PPI canvas width is outside the supported range");
const auto height = read_positive_i32(reader, "PPI canvas height is outside the supported range");
const auto layer_count = read_positive_i32(reader, "PPI layer count is outside the supported range");
if (!width || !height || !layer_count) {
return pp::foundation::Result<PpiBodySummary>::failure(
!width ? width.status() : (!height ? height.status() : layer_count.status()));
}
const auto canvas_status = validate_canvas_size(width.value(), height.value());
if (!canvas_status.ok()) {
return pp::foundation::Result<PpiBodySummary>::failure(canvas_status);
}
if (layer_count.value() == 0 || layer_count.value() > max_ppi_layer_count) {
return pp::foundation::Result<PpiBodySummary>::failure(
pp::foundation::Status::out_of_range("PPI layer count is outside the configured range"));
}
PpiBodySummary summary {
.width = width.value(),
.height = height.value(),
.layer_count = layer_count.value(),
.declared_frame_count = 1,
};
std::vector<bool> seen_orders;
if (index != nullptr) {
index->layers.resize(summary.layer_count);
seen_orders.assign(summary.layer_count, false);
}
if (header.document_version.minor >= 3U) {
const auto declared_frames = read_positive_i32(reader, "PPI declared frame count is outside the supported range");
if (!declared_frames) {
return pp::foundation::Result<PpiBodySummary>::failure(declared_frames.status());
}
if (declared_frames.value() == 0 || declared_frames.value() > max_ppi_frame_count) {
return pp::foundation::Result<PpiBodySummary>::failure(
pp::foundation::Status::out_of_range("PPI declared frame count is outside the configured range"));
}
summary.declared_frame_count = declared_frames.value();
}
for (std::uint32_t layer_index = 0; layer_index < summary.layer_count; ++layer_index) {
const auto order = read_positive_i32(reader, "PPI layer order is outside the supported range");
const auto opacity = read_f32(reader);
const auto name_length = read_positive_i32(reader, "PPI layer name length is outside the supported range");
if (!order || !opacity || !name_length) {
return pp::foundation::Result<PpiBodySummary>::failure(
!order ? order.status() : (!opacity ? opacity.status() : name_length.status()));
}
if (order.value() >= summary.layer_count) {
return pp::foundation::Result<PpiBodySummary>::failure(
pp::foundation::Status::out_of_range("PPI layer order is outside the layer list"));
}
if (index != nullptr) {
if (seen_orders[order.value()]) {
return pp::foundation::Result<PpiBodySummary>::failure(
pp::foundation::Status::invalid_argument("PPI layer order is duplicated"));
}
seen_orders[order.value()] = true;
}
if (!std::isfinite(opacity.value())) {
return pp::foundation::Result<PpiBodySummary>::failure(
pp::foundation::Status::invalid_argument("PPI layer opacity must be finite"));
}
if (opacity.value() < 0.0F || opacity.value() > 1.0F) {
return pp::foundation::Result<PpiBodySummary>::failure(
pp::foundation::Status::out_of_range("PPI layer opacity is outside the supported range"));
}
if (name_length.value() > max_ppi_layer_name_length) {
return pp::foundation::Result<PpiBodySummary>::failure(
pp::foundation::Status::out_of_range("PPI layer name exceeds the configured limit"));
}
const auto name_bytes = reader.read_bytes(name_length.value());
if (!name_bytes) {
return pp::foundation::Result<PpiBodySummary>::failure(name_bytes.status());
}
PpiLayerSummary layer_summary;
if (index != nullptr) {
layer_summary.stored_order = order.value();
layer_summary.opacity = opacity.value();
layer_summary.name.reserve(name_bytes.value().size());
for (const auto byte : name_bytes.value()) {
layer_summary.name.push_back(static_cast<char>(std::to_integer<unsigned char>(byte)));
}
}
if (header.document_version.minor >= 2U) {
const auto blend_mode = read_positive_i32(reader, "PPI layer blend mode is outside the supported range");
const auto alpha_locked = reader.read_u8();
const auto visible = reader.read_u8();
if (!blend_mode || !alpha_locked || !visible) {
return pp::foundation::Result<PpiBodySummary>::failure(
!blend_mode ? blend_mode.status() : (!alpha_locked ? alpha_locked.status() : visible.status()));
}
if (alpha_locked.value() > 1U || visible.value() > 1U) {
return pp::foundation::Result<PpiBodySummary>::failure(
pp::foundation::Status::invalid_argument("PPI layer boolean field is invalid"));
}
if (blend_mode.value() > 4U) {
return pp::foundation::Result<PpiBodySummary>::failure(
pp::foundation::Status::out_of_range("PPI layer blend mode is outside the supported range"));
}
if (index != nullptr) {
layer_summary.blend_mode = blend_mode.value();
layer_summary.alpha_locked = alpha_locked.value() != 0U;
layer_summary.visible = visible.value() != 0U;
}
}
std::uint32_t layer_frames = 1;
if (header.document_version.minor >= 3U) {
const auto frame_count = read_positive_i32(reader, "PPI layer frame count is outside the supported range");
if (!frame_count) {
return pp::foundation::Result<PpiBodySummary>::failure(frame_count.status());
}
if (frame_count.value() == 0 || frame_count.value() > max_ppi_frame_count) {
return pp::foundation::Result<PpiBodySummary>::failure(
pp::foundation::Status::out_of_range("PPI layer frame count is outside the configured range"));
}
layer_frames = frame_count.value();
}
if (summary.total_layer_frames > max_ppi_frame_count - layer_frames) {
return pp::foundation::Result<PpiBodySummary>::failure(
pp::foundation::Status::out_of_range("PPI total frame count exceeds the configured limit"));
}
summary.total_layer_frames += layer_frames;
if (index != nullptr) {
layer_summary.frames.resize(layer_frames);
}
for (std::uint32_t frame_index = 0; frame_index < layer_frames; ++frame_index) {
if (header.document_version.minor >= 3U) {
const auto duration = read_positive_i32(reader, "PPI frame duration is outside the supported range");
if (!duration) {
return pp::foundation::Result<PpiBodySummary>::failure(duration.status());
}
if (duration.value() == 0) {
return pp::foundation::Result<PpiBodySummary>::failure(
pp::foundation::Status::invalid_argument("PPI frame duration must be greater than zero"));
}
if (index != nullptr) {
layer_summary.frames[frame_index].duration_ms = duration.value();
}
}
for (std::uint32_t face = 0; face < 6U; ++face) {
const auto has_data = read_positive_i32(reader, "PPI face data flag is outside the supported range");
if (!has_data) {
return pp::foundation::Result<PpiBodySummary>::failure(has_data.status());
}
if (has_data.value() > 1U) {
return pp::foundation::Result<PpiBodySummary>::failure(
pp::foundation::Status::invalid_argument("PPI face data flag is invalid"));
}
if (has_data.value() == 0U) {
continue;
}
++summary.dirty_face_count;
const auto x0 = read_positive_i32(reader, "PPI dirty box coordinate is outside the supported range");
const auto y0 = read_positive_i32(reader, "PPI dirty box coordinate is outside the supported range");
const auto x1 = read_positive_i32(reader, "PPI dirty box coordinate is outside the supported range");
const auto y1 = read_positive_i32(reader, "PPI dirty box coordinate is outside the supported range");
const auto data_size = read_positive_i32(reader, "PPI compressed face data size is outside the supported range");
if (!x0 || !y0 || !x1 || !y1 || !data_size) {
return pp::foundation::Result<PpiBodySummary>::failure(
!x0 ? x0.status()
: (!y0 ? y0.status() : (!x1 ? x1.status() : (!y1 ? y1.status() : data_size.status()))));
}
if (x0.value() >= x1.value() || y0.value() >= y1.value() || x1.value() > summary.width
|| y1.value() > summary.height) {
return pp::foundation::Result<PpiBodySummary>::failure(
pp::foundation::Status::out_of_range("PPI dirty box is outside the canvas"));
}
if (data_size.value() == 0U) {
return pp::foundation::Result<PpiBodySummary>::failure(
pp::foundation::Status::invalid_argument("PPI compressed face payload must not be empty"));
}
const auto byte_status = add_payload_bytes(summary, data_size.value());
if (!byte_status.ok()) {
return pp::foundation::Result<PpiBodySummary>::failure(byte_status);
}
const auto payload_offset = reader.position();
const auto payload = reader.read_bytes(data_size.value());
if (!payload) {
return pp::foundation::Result<PpiBodySummary>::failure(payload.status());
}
const auto png_metadata = validate_face_png_payload(
payload.value(),
x1.value() - x0.value(),
y1.value() - y0.value());
if (!png_metadata) {
return pp::foundation::Result<PpiBodySummary>::failure(png_metadata.status());
}
++summary.rgba_face_payload_count;
if (index != nullptr) {
layer_summary.frames[frame_index].faces[face] = PpiFacePayloadSummary {
.has_data = true,
.x0 = x0.value(),
.y0 = y0.value(),
.x1 = x1.value(),
.y1 = y1.value(),
.body_payload_offset = static_cast<std::uint32_t>(payload_offset),
.payload_bytes = data_size.value(),
.png_width = png_metadata.value().width,
.png_height = png_metadata.value().height,
};
}
}
}
if (index != nullptr) {
index->layers[order.value()] = std::move(layer_summary);
}
}
if (header.document_version.minor >= 3U && summary.total_layer_frames != summary.declared_frame_count) {
return pp::foundation::Result<PpiBodySummary>::failure(
pp::foundation::Status::invalid_argument("PPI declared frame count does not match layer frames"));
}
if (header.document_version.minor >= 4U) {
const auto info_bytes = read_positive_i32(reader, "PPI info block size is outside the supported range");
if (!info_bytes) {
return pp::foundation::Result<PpiBodySummary>::failure(info_bytes.status());
}
summary.info_bytes = info_bytes.value();
const auto info_status = skip_bytes(reader, summary.info_bytes);
if (!info_status.ok()) {
return pp::foundation::Result<PpiBodySummary>::failure(info_status);
}
}
if (!reader.empty()) {
return pp::foundation::Result<PpiBodySummary>::failure(
pp::foundation::Status::invalid_argument("PPI body has trailing bytes"));
}
if (index != nullptr) {
index->summary = summary;
}
return pp::foundation::Result<PpiBodySummary>::success(summary);
}
}
pp::foundation::Result<PpiBodySummary> parse_ppi_body_summary(
PpiHeaderInfo header,
std::span<const std::byte> body) noexcept
{
return parse_ppi_body_impl(header, body, nullptr);
}
pp::foundation::Result<PpiBodyIndex> parse_ppi_body_index(
PpiHeaderInfo header,
std::span<const std::byte> body)
{
PpiBodyIndex index;
const auto summary = parse_ppi_body_impl(header, body, &index);
if (!summary) {
return pp::foundation::Result<PpiBodyIndex>::failure(summary.status());
}
return pp::foundation::Result<PpiBodyIndex>::success(std::move(index));
}
pp::foundation::Result<PpiProjectSummary> parse_ppi_project_summary(std::span<const std::byte> bytes) noexcept
{
const auto layout = parse_ppi_project_layout(bytes);
if (!layout) {
return pp::foundation::Result<PpiProjectSummary>::failure(layout.status());
}
const auto body = parse_ppi_body_summary(
layout.value().header,
bytes.subspan(layout.value().body_offset, layout.value().body_bytes));
if (!body) {
return pp::foundation::Result<PpiProjectSummary>::failure(body.status());
}
return pp::foundation::Result<PpiProjectSummary>::success(PpiProjectSummary {
.layout = layout.value(),
.body = body.value(),
});
}
pp::foundation::Result<PpiProjectIndex> parse_ppi_project_index(std::span<const std::byte> bytes)
{
const auto layout = parse_ppi_project_layout(bytes);
if (!layout) {
return pp::foundation::Result<PpiProjectIndex>::failure(layout.status());
}
const auto body = parse_ppi_body_index(
layout.value().header,
bytes.subspan(layout.value().body_offset, layout.value().body_bytes));
if (!body) {
return pp::foundation::Result<PpiProjectIndex>::failure(body.status());
}
return pp::foundation::Result<PpiProjectIndex>::success(PpiProjectIndex {
.layout = layout.value(),
.body = body.value(),
});
}
pp::foundation::Result<PpiDecodedProjectImages> decode_ppi_project_images(std::span<const std::byte> bytes)
{
auto project = parse_ppi_project_index(bytes);
if (!project) {
return pp::foundation::Result<PpiDecodedProjectImages>::failure(project.status());
}
PpiDecodedProjectImages decoded {
.project = project.value(),
.faces = {},
};
decoded.faces.reserve(decoded.project.body.summary.rgba_face_payload_count);
const auto body = bytes.subspan(decoded.project.layout.body_offset, decoded.project.layout.body_bytes);
for (std::size_t layer_index = 0; layer_index < decoded.project.body.layers.size(); ++layer_index) {
const auto& layer = decoded.project.body.layers[layer_index];
for (std::size_t frame_index = 0; frame_index < layer.frames.size(); ++frame_index) {
const auto& frame = layer.frames[frame_index];
for (std::size_t face_index = 0; face_index < frame.faces.size(); ++face_index) {
const auto& face = frame.faces[face_index];
if (!face.has_data) {
continue;
}
if (face.body_payload_offset > body.size()
|| face.payload_bytes > body.size() - face.body_payload_offset) {
return pp::foundation::Result<PpiDecodedProjectImages>::failure(
pp::foundation::Status::out_of_range("PPI face payload range is outside the body"));
}
const auto image = decode_png_rgba8(
body.subspan(face.body_payload_offset, face.payload_bytes));
if (!image) {
return pp::foundation::Result<PpiDecodedProjectImages>::failure(image.status());
}
if (image.value().width != face.png_width || image.value().height != face.png_height) {
return pp::foundation::Result<PpiDecodedProjectImages>::failure(
pp::foundation::Status::invalid_argument("decoded PPI face payload dimensions changed"));
}
decoded.faces.push_back(PpiDecodedFacePayload {
.layer_index = static_cast<std::uint32_t>(layer_index),
.frame_index = static_cast<std::uint32_t>(frame_index),
.face_index = static_cast<std::uint32_t>(face_index),
.descriptor = face,
.image = image.value(),
});
}
}
}
return pp::foundation::Result<PpiDecodedProjectImages>::success(std::move(decoded));
}
pp::foundation::Result<std::vector<std::byte>> create_ppi_project(PpiProjectConfig config)
{
const auto canvas_status = validate_canvas_size(config.width, config.height);
if (!canvas_status.ok()) {
return pp::foundation::Result<std::vector<std::byte>>::failure(canvas_status);
}
if (config.layers.empty() || config.layers.size() > max_ppi_layer_count) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::out_of_range("PPI layer count is outside the configured range"));
}
std::uint32_t total_frame_count = 0;
std::vector<std::size_t> layer_frame_offsets;
layer_frame_offsets.reserve(config.layers.size());
for (const auto& layer : config.layers) {
if (layer.name.empty()) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::invalid_argument("PPI layer name must not be empty"));
}
if (layer.name.size() > max_ppi_layer_name_length) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::out_of_range("PPI layer name exceeds the configured limit"));
}
if (!std::isfinite(layer.metadata.opacity)) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::invalid_argument("PPI layer opacity must be finite"));
}
if (layer.metadata.opacity < 0.0F || layer.metadata.opacity > 1.0F) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::out_of_range("PPI layer opacity is outside the supported range"));
}
if (layer.metadata.blend_mode > 4U) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::out_of_range("PPI layer blend mode is outside the supported range"));
}
if (layer.frames.empty() || layer.frames.size() > max_ppi_frame_count) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::out_of_range("PPI layer frame count is outside the configured range"));
}
if (layer.frames.size() > max_ppi_frame_count - total_frame_count) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::out_of_range("PPI total layer frame count exceeds the configured range"));
}
layer_frame_offsets.push_back(total_frame_count);
total_frame_count += static_cast<std::uint32_t>(layer.frames.size());
for (const auto& frame : layer.frames) {
if (frame.duration_ms == 0) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::invalid_argument("PPI frame duration must be greater than zero"));
}
}
}
std::vector<std::array<bool, 6>> seen_faces(total_frame_count);
std::uint64_t total_payload_bytes = 0;
for (const auto& face : config.dirty_faces) {
if (face.layer_index >= config.layers.size()) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::out_of_range("PPI dirty face layer index is outside the layer list"));
}
if (face.frame_index >= config.layers[face.layer_index].frames.size()) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::out_of_range("PPI dirty face frame index is outside the frame list"));
}
if (face.face_index >= 6U) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::out_of_range("PPI dirty face index is outside the cube face list"));
}
const auto slot_index = layer_frame_offsets[face.layer_index] + static_cast<std::size_t>(face.frame_index);
if (seen_faces[slot_index][face.face_index]) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::invalid_argument("PPI dirty face slot is duplicated"));
}
seen_faces[slot_index][face.face_index] = true;
if (face.width == 0 || face.height == 0) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::invalid_argument("PPI dirty face dimensions must be greater than zero"));
}
if (face.x > config.width || face.width > config.width - face.x
|| face.y > config.height || face.height > config.height - face.y) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::out_of_range("PPI dirty face box is outside the canvas"));
}
if (face.png_rgba8.empty()) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::invalid_argument("PPI dirty face PNG payload must not be empty"));
}
if (face.png_rgba8.size() > static_cast<std::size_t>(std::numeric_limits<std::uint32_t>::max())) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::out_of_range("PPI dirty face PNG payload is too large"));
}
const auto next_payload_bytes = total_payload_bytes + face.png_rgba8.size();
if (next_payload_bytes > max_ppi_face_payload_bytes) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::out_of_range("PPI dirty face PNG payloads exceed the configured limit"));
}
total_payload_bytes = next_payload_bytes;
const auto metadata = validate_face_png_payload(face.png_rgba8, face.width, face.height);
if (!metadata) {
return pp::foundation::Result<std::vector<std::byte>>::failure(metadata.status());
}
}
std::vector<std::byte> bytes {
std::byte { 'P' },
std::byte { 'P' },
std::byte { 'I' },
std::byte { 0 },
};
append_u32(bytes, 0);
append_u32(bytes, 4);
append_u32(bytes, 0);
append_u32(bytes, 0);
append_u32(bytes, 0);
append_u32(bytes, 0);
append_u32(bytes, 128);
append_u32(bytes, 128);
append_u32(bytes, 4);
constexpr std::size_t thumbnail_bytes = 128U * 128U * 4U;
bytes.resize(ppi_header_size + thumbnail_bytes, std::byte { 0 });
append_u32(bytes, config.width);
append_u32(bytes, config.height);
append_u32(bytes, static_cast<std::uint32_t>(config.layers.size()));
append_u32(bytes, total_frame_count);
for (std::uint32_t layer = 0; layer < config.layers.size(); ++layer) {
const auto& layer_config = config.layers[layer];
append_u32(bytes, layer);
append_f32(bytes, layer_config.metadata.opacity);
append_u32(bytes, static_cast<std::uint32_t>(layer_config.name.size()));
append_ascii(bytes, layer_config.name);
append_u32(bytes, layer_config.metadata.blend_mode);
bytes.push_back(layer_config.metadata.alpha_locked ? std::byte { 1 } : std::byte { 0 });
bytes.push_back(layer_config.metadata.visible ? std::byte { 1 } : std::byte { 0 });
append_u32(bytes, static_cast<std::uint32_t>(layer_config.frames.size()));
for (std::uint32_t frame = 0; frame < layer_config.frames.size(); ++frame) {
append_u32(bytes, layer_config.frames[frame].duration_ms);
for (std::uint32_t face = 0; face < 6U; ++face) {
const PpiDirtyFacePayloadConfig* dirty_face = nullptr;
for (const auto& candidate : config.dirty_faces) {
if (candidate.layer_index == layer && candidate.frame_index == frame
&& candidate.face_index == face) {
dirty_face = &candidate;
break;
}
}
if (dirty_face == nullptr) {
append_u32(bytes, 0);
continue;
}
append_u32(bytes, 1);
append_u32(bytes, dirty_face->x);
append_u32(bytes, dirty_face->y);
append_u32(bytes, dirty_face->x + dirty_face->width);
append_u32(bytes, dirty_face->y + dirty_face->height);
append_u32(bytes, static_cast<std::uint32_t>(dirty_face->png_rgba8.size()));
bytes.insert(bytes.end(), dirty_face->png_rgba8.begin(), dirty_face->png_rgba8.end());
}
}
}
append_u32(bytes, 0);
return pp::foundation::Result<std::vector<std::byte>>::success(std::move(bytes));
}
pp::foundation::Result<std::vector<std::byte>> create_minimal_ppi_project(PpiMinimalProjectConfig config)
{
if (config.layer_count == 0 || config.layer_count > max_ppi_layer_count) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::out_of_range("PPI layer count is outside the configured range"));
}
if (config.frame_count == 0 || config.frame_count > max_ppi_frame_count) {
return pp::foundation::Result<std::vector<std::byte>>::failure(
pp::foundation::Status::out_of_range("PPI frame count is outside the configured range"));
}
std::vector<std::string> names;
names.reserve(config.layer_count);
std::vector<std::vector<PpiFrameConfig>> frame_lists;
frame_lists.reserve(config.layer_count);
std::vector<PpiLayerConfig> layers;
layers.reserve(config.layer_count);
for (std::uint32_t layer = 0; layer < config.layer_count; ++layer) {
names.push_back(generated_layer_name(config.layer_name, layer, config.layer_count));
auto& frames = frame_lists.emplace_back();
frames.assign(config.frame_count, PpiFrameConfig { .duration_ms = config.frame_duration_ms });
layers.push_back(PpiLayerConfig {
.name = names.back(),
.metadata = config.layer_metadata,
.frames = std::span<const PpiFrameConfig>(frames.data(), frames.size()),
});
}
return create_ppi_project(PpiProjectConfig {
.width = config.width,
.height = config.height,
.layers = std::span<const PpiLayerConfig>(layers.data(), layers.size()),
.dirty_faces = config.dirty_faces,
});
}
}