Add document layer metadata tests

This commit is contained in:
2026-06-01 08:34:26 +02:00
parent cc377b5eb5
commit 551013c771
4 changed files with 167 additions and 5 deletions

View File

@@ -312,8 +312,8 @@ PPI header recognition, and a pure typed settings document model, with
corrupt/truncated/unsupported and key/value limit tests.
`pp_paint` has started with CPU reference math for the five current shader
blend modes plus deterministic stroke spacing/interpolation. `pp_document` has
started with a pure canvas/layer/frame model and layer/frame/undo-redo history
invariant tests. `pp_renderer_api` has started with renderer-neutral
started with a pure canvas/layer/frame model, layer metadata operations, and
layer/frame/undo-redo history invariant tests. `pp_renderer_api` has started with renderer-neutral
texture/readback descriptors and validation tests. `pp_paint_renderer` has
started with deterministic CPU layer compositing over renderer extents using
the paint blend reference. `pp_ui_core` has started with XML-layout-facing

View File

@@ -1,6 +1,7 @@
#include "document/document.h"
#include <algorithm>
#include <cmath>
namespace pp::document {
@@ -32,6 +33,28 @@ namespace {
return "Layer " + std::to_string(index + 1U);
}
[[nodiscard]] pp::foundation::Status validate_layer_name(std::string_view name) noexcept
{
if (name.empty()) {
return pp::foundation::Status::invalid_argument("layer name must not be empty");
}
if (name.size() > max_layer_name_length) {
return pp::foundation::Status::out_of_range("layer name length exceeds the configured limit");
}
return pp::foundation::Status::success();
}
[[nodiscard]] pp::foundation::Status validate_layer_index(std::size_t index, std::size_t layer_count) noexcept
{
if (index >= layer_count) {
return pp::foundation::Status::out_of_range("layer index is outside the document");
}
return pp::foundation::Status::success();
}
}
pp::foundation::Result<CanvasDocument> CanvasDocument::create(DocumentConfig config)
@@ -91,7 +114,15 @@ pp::foundation::Result<std::size_t> CanvasDocument::add_layer(std::string_view n
}
Layer layer;
layer.name = name.empty() ? default_layer_name(layers_.size()) : std::string(name);
if (name.empty()) {
layer.name = default_layer_name(layers_.size());
} else {
const auto name_status = validate_layer_name(name);
if (!name_status.ok()) {
return pp::foundation::Result<std::size_t>::failure(name_status);
}
layer.name = std::string(name);
}
layers_.push_back(layer);
active_layer_index_ = layers_.size() - 1U;
return pp::foundation::Result<std::size_t>::success(active_layer_index_);
@@ -144,14 +175,77 @@ pp::foundation::Status CanvasDocument::move_layer(std::size_t from, std::size_t
pp::foundation::Status CanvasDocument::set_active_layer(std::size_t index) noexcept
{
if (index >= layers_.size()) {
return pp::foundation::Status::out_of_range("layer index is outside the document");
const auto index_status = validate_layer_index(index, layers_.size());
if (!index_status.ok()) {
return index_status;
}
active_layer_index_ = index;
return pp::foundation::Status::success();
}
pp::foundation::Status CanvasDocument::rename_layer(std::size_t index, std::string_view name)
{
const auto index_status = validate_layer_index(index, layers_.size());
if (!index_status.ok()) {
return index_status;
}
const auto name_status = validate_layer_name(name);
if (!name_status.ok()) {
return name_status;
}
layers_[index].name = std::string(name);
return pp::foundation::Status::success();
}
pp::foundation::Status CanvasDocument::set_layer_visible(std::size_t index, bool visible) noexcept
{
const auto index_status = validate_layer_index(index, layers_.size());
if (!index_status.ok()) {
return index_status;
}
layers_[index].visible = visible;
return pp::foundation::Status::success();
}
pp::foundation::Status CanvasDocument::set_layer_opacity(std::size_t index, float opacity) noexcept
{
const auto index_status = validate_layer_index(index, layers_.size());
if (!index_status.ok()) {
return index_status;
}
if (!std::isfinite(opacity) || opacity < 0.0F || opacity > 1.0F) {
return pp::foundation::Status::out_of_range("layer opacity must be finite and within 0..1");
}
layers_[index].opacity = opacity;
return pp::foundation::Status::success();
}
pp::foundation::Status CanvasDocument::set_layer_blend_mode(std::size_t index, pp::paint::BlendMode blend_mode) noexcept
{
const auto index_status = validate_layer_index(index, layers_.size());
if (!index_status.ok()) {
return index_status;
}
switch (blend_mode) {
case pp::paint::BlendMode::normal:
case pp::paint::BlendMode::multiply:
case pp::paint::BlendMode::screen:
case pp::paint::BlendMode::color_dodge:
case pp::paint::BlendMode::overlay:
layers_[index].blend_mode = blend_mode;
return pp::foundation::Status::success();
}
return pp::foundation::Status::invalid_argument("layer blend mode is not supported");
}
pp::foundation::Result<std::size_t> CanvasDocument::add_frame(std::uint32_t duration_ms)
{
if (frames_.size() >= max_frame_count) {

View File

@@ -17,6 +17,7 @@ constexpr std::uint32_t max_frame_count = 100000;
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;
struct DocumentConfig {
std::uint32_t width = 0;
@@ -50,6 +51,10 @@ public:
[[nodiscard]] pp::foundation::Status remove_layer(std::size_t index);
[[nodiscard]] pp::foundation::Status move_layer(std::size_t from, std::size_t to);
[[nodiscard]] pp::foundation::Status set_active_layer(std::size_t index) noexcept;
[[nodiscard]] pp::foundation::Status rename_layer(std::size_t index, std::string_view name);
[[nodiscard]] pp::foundation::Status set_layer_visible(std::size_t index, bool visible) noexcept;
[[nodiscard]] pp::foundation::Status set_layer_opacity(std::size_t index, float opacity) noexcept;
[[nodiscard]] pp::foundation::Status set_layer_blend_mode(std::size_t index, pp::paint::BlendMode blend_mode) noexcept;
[[nodiscard]] pp::foundation::Result<std::size_t> add_frame(std::uint32_t duration_ms);
[[nodiscard]] pp::foundation::Result<std::size_t> duplicate_frame(std::size_t index);

View File

@@ -1,8 +1,10 @@
#include "document/document.h"
#include "test_harness.h"
#include <cmath>
#include <string_view>
using pp::paint::BlendMode;
using pp::document::CanvasDocument;
using pp::document::DocumentHistory;
using pp::document::DocumentConfig;
@@ -10,6 +12,7 @@ using pp::document::max_document_history_entries;
using pp::document::max_canvas_dimension;
using pp::document::max_frame_count;
using pp::document::max_layer_count;
using pp::document::max_layer_name_length;
using pp::foundation::StatusCode;
namespace {
@@ -93,6 +96,64 @@ void moves_layers_and_preserves_active_layer_identity(pp::tests::Harness& h)
PP_EXPECT(h, bad_move.code == StatusCode::out_of_range);
}
void updates_layer_metadata(pp::tests::Harness& h)
{
auto document_result = CanvasDocument::create(
DocumentConfig { .width = 64, .height = 64, .layer_count = 2 });
PP_EXPECT(h, document_result.ok());
auto document = document_result.value();
PP_EXPECT(h, document.rename_layer(1, "Ink").ok());
PP_EXPECT(h, document.set_layer_visible(1, false).ok());
PP_EXPECT(h, document.set_layer_opacity(1, 0.25F).ok());
PP_EXPECT(h, document.set_layer_blend_mode(1, BlendMode::multiply).ok());
PP_EXPECT(h, document.layers()[1].name == std::string_view("Ink"));
PP_EXPECT(h, !document.layers()[1].visible);
PP_EXPECT(h, std::fabs(document.layers()[1].opacity - 0.25F) < 0.0001F);
PP_EXPECT(h, document.layers()[1].blend_mode == BlendMode::multiply);
}
void rejects_invalid_layer_metadata(pp::tests::Harness& h)
{
auto document_result = CanvasDocument::create(
DocumentConfig { .width = 64, .height = 64, .layer_count = 1 });
PP_EXPECT(h, document_result.ok());
auto document = document_result.value();
const auto empty_name = document.rename_layer(0, "");
const auto long_name = document.rename_layer(0, std::string(max_layer_name_length + 1U, 'x'));
const auto missing_name = document.rename_layer(4, "Missing");
const auto bad_opacity_low = document.set_layer_opacity(0, -0.1F);
const auto bad_opacity_high = document.set_layer_opacity(0, 1.1F);
const auto bad_opacity_nan = document.set_layer_opacity(0, std::nanf(""));
const auto missing_visible = document.set_layer_visible(2, true);
const auto missing_blend = document.set_layer_blend_mode(2, BlendMode::normal);
const auto bad_blend = document.set_layer_blend_mode(0, static_cast<BlendMode>(255));
const auto bad_add_layer = document.add_layer(std::string(max_layer_name_length + 1U, 'x'));
PP_EXPECT(h, !empty_name.ok());
PP_EXPECT(h, empty_name.code == StatusCode::invalid_argument);
PP_EXPECT(h, !long_name.ok());
PP_EXPECT(h, long_name.code == StatusCode::out_of_range);
PP_EXPECT(h, !missing_name.ok());
PP_EXPECT(h, missing_name.code == StatusCode::out_of_range);
PP_EXPECT(h, !bad_opacity_low.ok());
PP_EXPECT(h, bad_opacity_low.code == StatusCode::out_of_range);
PP_EXPECT(h, !bad_opacity_high.ok());
PP_EXPECT(h, bad_opacity_high.code == StatusCode::out_of_range);
PP_EXPECT(h, !bad_opacity_nan.ok());
PP_EXPECT(h, bad_opacity_nan.code == StatusCode::out_of_range);
PP_EXPECT(h, !missing_visible.ok());
PP_EXPECT(h, missing_visible.code == StatusCode::out_of_range);
PP_EXPECT(h, !missing_blend.ok());
PP_EXPECT(h, missing_blend.code == StatusCode::out_of_range);
PP_EXPECT(h, !bad_blend.ok());
PP_EXPECT(h, bad_blend.code == StatusCode::invalid_argument);
PP_EXPECT(h, !bad_add_layer.ok());
PP_EXPECT(h, bad_add_layer.status().code == StatusCode::out_of_range);
}
void manages_animation_frames_and_duration(pp::tests::Harness& h)
{
auto document_result = CanvasDocument::create(
@@ -267,6 +328,8 @@ int main()
harness.run("rejects_invalid_document_configs", rejects_invalid_document_configs);
harness.run("manages_layer_add_remove_and_active_index", manages_layer_add_remove_and_active_index);
harness.run("moves_layers_and_preserves_active_layer_identity", moves_layers_and_preserves_active_layer_identity);
harness.run("updates_layer_metadata", updates_layer_metadata);
harness.run("rejects_invalid_layer_metadata", rejects_invalid_layer_metadata);
harness.run("manages_animation_frames_and_duration", manages_animation_frames_and_duration);
harness.run("rejects_invalid_animation_frame_operations", rejects_invalid_animation_frame_operations);
harness.run("records_document_history_and_restores_snapshots", records_document_history_and_restores_snapshots);