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

@@ -11,6 +11,7 @@ using pp::document::DocumentConfig;
using pp::document::DocumentLayerConfig;
using pp::document::DocumentSnapshotConfig;
using pp::document::AnimationFrame;
using pp::document::LayerFacePixels;
using pp::document::max_document_history_entries;
using pp::document::max_canvas_dimension;
using pp::document::max_frame_count;
@@ -186,8 +187,8 @@ void creates_document_from_snapshot_metadata(pp::tests::Harness& h)
},
};
const AnimationFrame frames[] {
{ .duration_ms = 100 },
{ .duration_ms = 250 },
{ .duration_ms = 100, .face_pixels = {} },
{ .duration_ms = 250, .face_pixels = {} },
};
const auto document_result = CanvasDocument::create_from_snapshot(DocumentSnapshotConfig {
@@ -212,14 +213,14 @@ void creates_document_from_snapshot_metadata(pp::tests::Harness& h)
void preserves_per_layer_snapshot_timelines(pp::tests::Harness& h)
{
const AnimationFrame project_frames[] {
{ .duration_ms = 100 },
{ .duration_ms = 100, .face_pixels = {} },
};
const AnimationFrame short_layer_frames[] {
{ .duration_ms = 100 },
{ .duration_ms = 150 },
{ .duration_ms = 100, .face_pixels = {} },
{ .duration_ms = 150, .face_pixels = {} },
};
const AnimationFrame long_layer_frames[] {
{ .duration_ms = 500 },
{ .duration_ms = 500, .face_pixels = {} },
};
const DocumentLayerConfig layers[] {
{
@@ -254,8 +255,8 @@ void preserves_per_layer_snapshot_timelines(pp::tests::Harness& h)
void rejects_invalid_snapshot_metadata(pp::tests::Harness& h)
{
const DocumentLayerConfig layers[] { { .name = "Ink", .frames = {} } };
const AnimationFrame frames[] { { .duration_ms = 100 } };
const AnimationFrame bad_frames[] { { .duration_ms = 0 } };
const AnimationFrame frames[] { { .duration_ms = 100, .face_pixels = {} } };
const AnimationFrame bad_frames[] { { .duration_ms = 0, .face_pixels = {} } };
const DocumentLayerConfig bad_layers[] { { .name = "", .frames = {} } };
const DocumentLayerConfig bad_layer_frames[] { { .name = "Ink", .frames = bad_frames } };
@@ -393,6 +394,115 @@ void rejects_invalid_animation_frame_operations(pp::tests::Harness& h)
PP_EXPECT(h, max_frame_count > document.frames().size());
}
void attaches_layer_frame_face_pixels(pp::tests::Harness& h)
{
auto document_result = CanvasDocument::create(
DocumentConfig { .width = 64, .height = 32, .layer_count = 1 });
PP_EXPECT(h, document_result.ok());
auto document = document_result.value();
const auto status = document.set_layer_frame_face_pixels(
0,
0,
LayerFacePixels {
.face_index = 2,
.x = 3,
.y = 4,
.width = 1,
.height = 1,
.rgba8 = { 10, 20, 30, 40 },
});
PP_EXPECT(h, status.ok());
PP_EXPECT(h, document.face_pixel_payload_count() == 1U);
PP_EXPECT(h, document.layers()[0].frames[0].face_pixels.size() == 1U);
PP_EXPECT(h, document.layers()[0].frames[0].face_pixels[0].face_index == 2U);
PP_EXPECT(h, document.layers()[0].frames[0].face_pixels[0].x == 3U);
PP_EXPECT(h, document.layers()[0].frames[0].face_pixels[0].rgba8[3] == 40U);
}
void replaces_existing_face_pixel_payload(pp::tests::Harness& h)
{
auto document_result = CanvasDocument::create(
DocumentConfig { .width = 64, .height = 32, .layer_count = 1 });
PP_EXPECT(h, document_result.ok());
auto document = document_result.value();
PP_EXPECT(h, document.set_layer_frame_face_pixels(
0,
0,
LayerFacePixels {
.face_index = 1,
.x = 0,
.y = 0,
.width = 1,
.height = 1,
.rgba8 = { 1, 2, 3, 4 },
}).ok());
PP_EXPECT(h, document.set_layer_frame_face_pixels(
0,
0,
LayerFacePixels {
.face_index = 1,
.x = 2,
.y = 3,
.width = 1,
.height = 1,
.rgba8 = { 5, 6, 7, 8 },
}).ok());
PP_EXPECT(h, document.face_pixel_payload_count() == 1U);
PP_EXPECT(h, document.layers()[0].frames[0].face_pixels[0].x == 2U);
PP_EXPECT(h, document.layers()[0].frames[0].face_pixels[0].rgba8[0] == 5U);
}
void rejects_invalid_face_pixel_payloads(pp::tests::Harness& h)
{
auto document_result = CanvasDocument::create(
DocumentConfig { .width = 64, .height = 32, .layer_count = 1 });
PP_EXPECT(h, document_result.ok());
auto document = document_result.value();
const auto missing_layer = document.set_layer_frame_face_pixels(
9,
0,
LayerFacePixels { .face_index = 0, .x = 0, .y = 0, .width = 1, .height = 1, .rgba8 = { 1, 2, 3, 4 } });
const auto missing_frame = document.set_layer_frame_face_pixels(
0,
9,
LayerFacePixels { .face_index = 0, .x = 0, .y = 0, .width = 1, .height = 1, .rgba8 = { 1, 2, 3, 4 } });
const auto bad_face = document.set_layer_frame_face_pixels(
0,
0,
LayerFacePixels { .face_index = 6, .x = 0, .y = 0, .width = 1, .height = 1, .rgba8 = { 1, 2, 3, 4 } });
const auto zero_width = document.set_layer_frame_face_pixels(
0,
0,
LayerFacePixels { .face_index = 0, .x = 0, .y = 0, .width = 0, .height = 1, .rgba8 = {} });
const auto outside_bounds = document.set_layer_frame_face_pixels(
0,
0,
LayerFacePixels { .face_index = 0, .x = 63, .y = 0, .width = 2, .height = 1, .rgba8 = { 1, 2, 3, 4, 5, 6, 7, 8 } });
const auto bad_byte_count = document.set_layer_frame_face_pixels(
0,
0,
LayerFacePixels { .face_index = 0, .x = 0, .y = 0, .width = 1, .height = 1, .rgba8 = { 1, 2, 3 } });
PP_EXPECT(h, !missing_layer.ok());
PP_EXPECT(h, missing_layer.code == StatusCode::out_of_range);
PP_EXPECT(h, !missing_frame.ok());
PP_EXPECT(h, missing_frame.code == StatusCode::out_of_range);
PP_EXPECT(h, !bad_face.ok());
PP_EXPECT(h, bad_face.code == StatusCode::out_of_range);
PP_EXPECT(h, !zero_width.ok());
PP_EXPECT(h, zero_width.code == StatusCode::invalid_argument);
PP_EXPECT(h, !outside_bounds.ok());
PP_EXPECT(h, outside_bounds.code == StatusCode::out_of_range);
PP_EXPECT(h, !bad_byte_count.ok());
PP_EXPECT(h, bad_byte_count.code == StatusCode::invalid_argument);
PP_EXPECT(h, document.face_pixel_payload_count() == 0U);
}
void records_document_history_and_restores_snapshots(pp::tests::Harness& h)
{
auto document_result = CanvasDocument::create(
@@ -521,6 +631,9 @@ int main()
harness.run("manages_animation_frames_and_duration", manages_animation_frames_and_duration);
harness.run("moves_frames_and_preserves_active_frame_identity", moves_frames_and_preserves_active_frame_identity);
harness.run("rejects_invalid_animation_frame_operations", rejects_invalid_animation_frame_operations);
harness.run("attaches_layer_frame_face_pixels", attaches_layer_frame_face_pixels);
harness.run("replaces_existing_face_pixel_payload", replaces_existing_face_pixel_payload);
harness.run("rejects_invalid_face_pixel_payloads", rejects_invalid_face_pixel_payloads);
harness.run("records_document_history_and_restores_snapshots", records_document_history_and_restores_snapshots);
harness.run("applying_after_undo_discards_redo_branch", applying_after_undo_discards_redo_branch);
harness.run("bounds_document_history_capacity", bounds_document_history_capacity);

View File

@@ -0,0 +1,148 @@
#include "assets/ppi_header.h"
#include "document/ppi_import.h"
#include "test_harness.h"
#include <bit>
#include <cstddef>
#include <cstdint>
#include <string_view>
#include <vector>
using pp::assets::decode_ppi_project_images;
using pp::foundation::StatusCode;
using pp::document::import_ppi_project_document;
namespace {
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));
}
}
std::vector<std::byte> transparent_png_1x1()
{
return {
std::byte { 0x89 }, std::byte { 0x50 }, std::byte { 0x4e }, std::byte { 0x47 },
std::byte { 0x0d }, std::byte { 0x0a }, std::byte { 0x1a }, std::byte { 0x0a },
std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x0d },
std::byte { 0x49 }, std::byte { 0x48 }, std::byte { 0x44 }, std::byte { 0x52 },
std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x01 },
std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x01 },
std::byte { 0x08 }, std::byte { 0x06 }, std::byte { 0x00 }, std::byte { 0x00 },
std::byte { 0x00 }, std::byte { 0x1f }, std::byte { 0x15 }, std::byte { 0xc4 },
std::byte { 0x89 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 },
std::byte { 0x0b }, std::byte { 0x49 }, std::byte { 0x44 }, std::byte { 0x41 },
std::byte { 0x54 }, std::byte { 0x78 }, std::byte { 0x9c }, std::byte { 0x63 },
std::byte { 0x60 }, std::byte { 0x00 }, std::byte { 0x02 }, std::byte { 0x00 },
std::byte { 0x00 }, std::byte { 0x05 }, std::byte { 0x00 }, std::byte { 0x01 },
std::byte { 0x7a }, std::byte { 0x5e }, std::byte { 0xab }, std::byte { 0x3f },
std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 }, std::byte { 0x00 },
std::byte { 0x49 }, std::byte { 0x45 }, std::byte { 0x4e }, std::byte { 0x44 },
std::byte { 0xae }, std::byte { 0x42 }, std::byte { 0x60 }, std::byte { 0x82 },
};
}
std::vector<std::byte> ppi_project_with_face_payload(std::vector<std::byte> payload)
{
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, 2);
append_u32(bytes, 3);
append_u32(bytes, 1024);
append_u32(bytes, 128);
append_u32(bytes, 128);
append_u32(bytes, 4);
bytes.resize(pp::assets::ppi_header_size + (128U * 128U * 4U), std::byte { 0 });
append_u32(bytes, 64);
append_u32(bytes, 32);
append_u32(bytes, 1);
append_u32(bytes, 1);
append_u32(bytes, 0);
append_f32(bytes, 1.0F);
append_u32(bytes, 3);
append_ascii(bytes, "Ink");
append_u32(bytes, 0);
bytes.push_back(std::byte { 0 });
bytes.push_back(std::byte { 1 });
append_u32(bytes, 1);
append_u32(bytes, 100);
append_u32(bytes, 1);
append_u32(bytes, 2);
append_u32(bytes, 3);
append_u32(bytes, 3);
append_u32(bytes, 4);
append_u32(bytes, static_cast<std::uint32_t>(payload.size()));
bytes.insert(bytes.end(), payload.begin(), payload.end());
for (std::uint32_t i = 1; i < 6U; ++i) {
append_u32(bytes, 0);
}
append_u32(bytes, 0);
return bytes;
}
void imports_decoded_ppi_pixels_into_document(pp::tests::Harness& h)
{
const auto project_bytes = ppi_project_with_face_payload(transparent_png_1x1());
const auto decoded = decode_ppi_project_images(project_bytes);
PP_EXPECT(h, decoded.ok());
const auto document = import_ppi_project_document(decoded.value());
PP_EXPECT(h, document.ok());
PP_EXPECT(h, document.value().width() == 64U);
PP_EXPECT(h, document.value().height() == 32U);
PP_EXPECT(h, document.value().face_pixel_payload_count() == 1U);
PP_EXPECT(h, document.value().layers()[0].frames[0].face_pixels.size() == 1U);
PP_EXPECT(h, document.value().layers()[0].frames[0].face_pixels[0].face_index == 0U);
PP_EXPECT(h, document.value().layers()[0].frames[0].face_pixels[0].x == 2U);
PP_EXPECT(h, document.value().layers()[0].frames[0].face_pixels[0].y == 3U);
PP_EXPECT(h, document.value().layers()[0].frames[0].face_pixels[0].width == 1U);
PP_EXPECT(h, document.value().layers()[0].frames[0].face_pixels[0].height == 1U);
PP_EXPECT(h, document.value().layers()[0].frames[0].face_pixels[0].rgba8.size() == 4U);
PP_EXPECT(h, document.value().layers()[0].frames[0].face_pixels[0].rgba8[3] == 0U);
}
void rejects_decoded_payloads_outside_document_layers(pp::tests::Harness& h)
{
const auto project_bytes = ppi_project_with_face_payload(transparent_png_1x1());
const auto decoded = decode_ppi_project_images(project_bytes);
PP_EXPECT(h, decoded.ok());
auto decoded_value = decoded.value();
decoded_value.faces[0].layer_index = 99;
const auto document = import_ppi_project_document(decoded_value);
PP_EXPECT(h, !document.ok());
PP_EXPECT(h, document.status().code == StatusCode::out_of_range);
}
}
int main()
{
pp::tests::Harness harness;
harness.run("imports_decoded_ppi_pixels_into_document", imports_decoded_ppi_pixels_into_document);
harness.run("rejects_decoded_payloads_outside_document_layers", rejects_decoded_payloads_outside_document_layers);
return harness.finish();
}