Attach PPI pixels to documents

This commit is contained in:
2026-06-01 13:43:27 +02:00
parent 88507df90e
commit ad255a6ddf
14 changed files with 569 additions and 104 deletions

View File

@@ -2,6 +2,8 @@
#include <algorithm>
#include <cmath>
#include <limits>
#include <utility>
namespace pp::document {
@@ -105,6 +107,67 @@ namespace {
return pp::foundation::Status::success();
}
[[nodiscard]] pp::foundation::Result<std::size_t> rgba8_byte_size(
std::uint32_t width,
std::uint32_t height) noexcept
{
const auto width64 = static_cast<std::uint64_t>(width);
const auto height64 = static_cast<std::uint64_t>(height);
if (width64 > std::numeric_limits<std::uint64_t>::max() / height64) {
return pp::foundation::Result<std::size_t>::failure(
pp::foundation::Status::out_of_range("face pixel dimensions overflow"));
}
const auto pixels = width64 * height64;
if (pixels > std::numeric_limits<std::uint64_t>::max() / rgba8_components) {
return pp::foundation::Result<std::size_t>::failure(
pp::foundation::Status::out_of_range("face pixel byte size overflows"));
}
const auto bytes = pixels * rgba8_components;
if (bytes > max_face_pixel_payload_bytes) {
return pp::foundation::Result<std::size_t>::failure(
pp::foundation::Status::out_of_range("face pixel payload exceeds the configured limit"));
}
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("face pixel payload exceeds addressable memory"));
}
return pp::foundation::Result<std::size_t>::success(static_cast<std::size_t>(bytes));
}
[[nodiscard]] pp::foundation::Status validate_face_pixels(
LayerFacePixels pixels,
std::uint32_t document_width,
std::uint32_t document_height) noexcept
{
if (pixels.face_index >= cube_face_count) {
return pp::foundation::Status::out_of_range("cube face index is outside the document");
}
if (pixels.width == 0 || pixels.height == 0) {
return pp::foundation::Status::invalid_argument("face pixel dimensions must be greater than zero");
}
if (pixels.x > document_width || pixels.width > document_width - pixels.x
|| pixels.y > document_height || pixels.height > document_height - pixels.y) {
return pp::foundation::Status::out_of_range("face pixel rectangle is outside the document");
}
const auto expected_bytes = rgba8_byte_size(pixels.width, pixels.height);
if (!expected_bytes) {
return expected_bytes.status();
}
if (pixels.rgba8.size() != expected_bytes.value()) {
return pp::foundation::Status::invalid_argument("face pixel payload byte size does not match dimensions");
}
return pp::foundation::Status::success();
}
}
pp::foundation::Result<CanvasDocument> CanvasDocument::create(DocumentConfig config)
@@ -251,6 +314,17 @@ pp::foundation::Result<std::uint64_t> CanvasDocument::layer_animation_duration_m
return pp::foundation::Result<std::uint64_t>::success(frame_duration_sum(layers_[index].frames));
}
std::size_t CanvasDocument::face_pixel_payload_count() const noexcept
{
std::size_t count = 0;
for (const auto& layer : layers_) {
for (const auto& frame : layer.frames) {
count += frame.face_pixels.size();
}
}
return count;
}
std::span<const Layer> CanvasDocument::layers() const noexcept
{
return layers_;
@@ -423,9 +497,9 @@ pp::foundation::Result<std::size_t> CanvasDocument::add_frame(std::uint32_t dura
duration_status);
}
frames_.push_back(AnimationFrame { .duration_ms = duration_ms });
frames_.push_back(AnimationFrame { .duration_ms = duration_ms, .face_pixels = {} });
for (auto& layer : layers_) {
layer.frames.push_back(AnimationFrame { .duration_ms = duration_ms });
layer.frames.push_back(AnimationFrame { .duration_ms = duration_ms, .face_pixels = {} });
}
active_frame_index_ = frames_.size() - 1U;
return pp::foundation::Result<std::size_t>::success(active_frame_index_);
@@ -547,6 +621,41 @@ pp::foundation::Status CanvasDocument::set_active_frame(std::size_t index) noexc
return pp::foundation::Status::success();
}
pp::foundation::Status CanvasDocument::set_layer_frame_face_pixels(
std::size_t layer_index,
std::size_t frame_index,
LayerFacePixels pixels)
{
const auto layer_status = validate_layer_index(layer_index, layers_.size());
if (!layer_status.ok()) {
return layer_status;
}
const auto frame_status = validate_frame_index(frame_index, layers_[layer_index].frames.size());
if (!frame_status.ok()) {
return frame_status;
}
const auto pixels_status = validate_face_pixels(pixels, width_, height_);
if (!pixels_status.ok()) {
return pixels_status;
}
auto& faces = layers_[layer_index].frames[frame_index].face_pixels;
const auto existing = std::find_if(
faces.begin(),
faces.end(),
[face_index = pixels.face_index](const LayerFacePixels& face) {
return face.face_index == face_index;
});
if (existing == faces.end()) {
faces.push_back(std::move(pixels));
} else {
*existing = std::move(pixels);
}
return pp::foundation::Status::success();
}
pp::foundation::Result<DocumentHistory> DocumentHistory::create(
CanvasDocument initial_document,
std::size_t max_entries)

View File

@@ -18,6 +18,9 @@ constexpr std::uint32_t min_frame_duration_ms = 1;
constexpr std::size_t min_document_history_entries = 2;
constexpr std::size_t max_document_history_entries = 10000;
constexpr std::size_t max_layer_name_length = 128;
constexpr std::uint32_t cube_face_count = 6;
constexpr std::uint32_t rgba8_components = 4;
constexpr std::uint64_t max_face_pixel_payload_bytes = 1024ULL * 1024ULL * 1024ULL;
struct DocumentConfig {
std::uint32_t width = 0;
@@ -25,8 +28,18 @@ struct DocumentConfig {
std::uint32_t layer_count = 1;
};
struct LayerFacePixels {
std::uint32_t face_index = 0;
std::uint32_t x = 0;
std::uint32_t y = 0;
std::uint32_t width = 0;
std::uint32_t height = 0;
std::vector<std::uint8_t> rgba8;
};
struct AnimationFrame {
std::uint32_t duration_ms = 100;
std::vector<LayerFacePixels> face_pixels;
};
struct Layer {
@@ -65,6 +78,7 @@ public:
[[nodiscard]] std::size_t active_frame_index() const noexcept;
[[nodiscard]] std::uint64_t animation_duration_ms() const noexcept;
[[nodiscard]] pp::foundation::Result<std::uint64_t> layer_animation_duration_ms(std::size_t index) const noexcept;
[[nodiscard]] std::size_t face_pixel_payload_count() const noexcept;
[[nodiscard]] std::span<const Layer> layers() const noexcept;
[[nodiscard]] std::span<const AnimationFrame> frames() const noexcept;
@@ -84,6 +98,10 @@ public:
[[nodiscard]] pp::foundation::Status move_frame(std::size_t from, std::size_t to);
[[nodiscard]] pp::foundation::Status set_frame_duration(std::size_t index, std::uint32_t duration_ms) noexcept;
[[nodiscard]] pp::foundation::Status set_active_frame(std::size_t index) noexcept;
[[nodiscard]] pp::foundation::Status set_layer_frame_face_pixels(
std::size_t layer_index,
std::size_t frame_index,
LayerFacePixels pixels);
private:
std::uint32_t width_ = 0;

116
src/document/ppi_import.cpp Normal file
View File

@@ -0,0 +1,116 @@
#include "document/ppi_import.h"
#include <utility>
#include <span>
#include <vector>
namespace pp::document {
namespace {
[[nodiscard]] pp::foundation::Result<pp::paint::BlendMode> ppi_layer_blend_mode(
std::uint32_t blend_mode) noexcept
{
switch (blend_mode) {
case 0:
return pp::foundation::Result<pp::paint::BlendMode>::success(pp::paint::BlendMode::normal);
case 1:
return pp::foundation::Result<pp::paint::BlendMode>::success(pp::paint::BlendMode::multiply);
case 2:
return pp::foundation::Result<pp::paint::BlendMode>::success(pp::paint::BlendMode::screen);
case 3:
return pp::foundation::Result<pp::paint::BlendMode>::success(pp::paint::BlendMode::color_dodge);
case 4:
return pp::foundation::Result<pp::paint::BlendMode>::success(pp::paint::BlendMode::overlay);
default:
return pp::foundation::Result<pp::paint::BlendMode>::failure(
pp::foundation::Status::invalid_argument("PPI layer blend mode is not supported by pp_document"));
}
}
[[nodiscard]] pp::foundation::Result<CanvasDocument> document_from_ppi_index(
const pp::assets::PpiProjectIndex& project)
{
if (project.body.layers.empty()) {
return pp::foundation::Result<CanvasDocument>::failure(
pp::foundation::Status::invalid_argument("PPI project has no layers"));
}
const auto& reference_frames = project.body.layers.front().frames;
if (reference_frames.empty()) {
return pp::foundation::Result<CanvasDocument>::failure(
pp::foundation::Status::invalid_argument("PPI project has no frames"));
}
std::vector<AnimationFrame> frames;
frames.reserve(reference_frames.size());
for (const auto& frame : reference_frames) {
frames.push_back(AnimationFrame { .duration_ms = frame.duration_ms, .face_pixels = {} });
}
std::vector<std::vector<AnimationFrame>> layer_frames;
layer_frames.reserve(project.body.layers.size());
std::vector<DocumentLayerConfig> layers;
layers.reserve(project.body.layers.size());
for (const auto& layer : project.body.layers) {
const auto blend_mode = ppi_layer_blend_mode(layer.blend_mode);
if (!blend_mode) {
return pp::foundation::Result<CanvasDocument>::failure(blend_mode.status());
}
auto& frame_list = layer_frames.emplace_back();
frame_list.reserve(layer.frames.size());
for (const auto& frame : layer.frames) {
frame_list.push_back(AnimationFrame { .duration_ms = frame.duration_ms, .face_pixels = {} });
}
layers.push_back(DocumentLayerConfig {
.name = layer.name,
.visible = layer.visible,
.alpha_locked = layer.alpha_locked,
.opacity = layer.opacity,
.blend_mode = blend_mode.value(),
.frames = std::span<const AnimationFrame>(frame_list.data(), frame_list.size()),
});
}
return CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
.width = project.body.summary.width,
.height = project.body.summary.height,
.layers = layers,
.frames = frames,
});
}
}
pp::foundation::Result<CanvasDocument> import_ppi_project_document(
const pp::assets::PpiDecodedProjectImages& project)
{
auto document = document_from_ppi_index(project.project);
if (!document) {
return document;
}
auto value = document.value();
for (const auto& face : project.faces) {
const auto status = value.set_layer_frame_face_pixels(
face.layer_index,
face.frame_index,
LayerFacePixels {
.face_index = face.face_index,
.x = face.descriptor.x0,
.y = face.descriptor.y0,
.width = face.image.width,
.height = face.image.height,
.rgba8 = face.image.pixels,
});
if (!status.ok()) {
return pp::foundation::Result<CanvasDocument>::failure(status);
}
}
return pp::foundation::Result<CanvasDocument>::success(std::move(value));
}
}

11
src/document/ppi_import.h Normal file
View File

@@ -0,0 +1,11 @@
#pragma once
#include "assets/ppi_header.h"
#include "document/document.h"
namespace pp::document {
[[nodiscard]] pp::foundation::Result<CanvasDocument> import_ppi_project_document(
const pp::assets::PpiDecodedProjectImages& project);
}