diff --git a/CMakeLists.txt b/CMakeLists.txt index 42ead08..01a9f98 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -135,13 +135,15 @@ target_link_libraries(pp_paint pp_project_warnings) add_library(pp_document STATIC - src/document/document.cpp) + src/document/document.cpp + src/document/ppi_import.cpp) target_include_directories(pp_document PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/src") target_link_libraries(pp_document PUBLIC pp_foundation + pp_assets pp_paint pp_project_options PRIVATE diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index d9d06ea..b39d9cf 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -92,7 +92,7 @@ Known local toolchain state: `pp_ui_core`, `pano_cli`, and their current headless test binaries, including foundation event/logging/task queue coverage, PNG metadata and decode, PPI header/layout, settings document, document - snapshot/per-layer-frame/move/duration coverage, paint + snapshot/per-layer-frame/move/duration/face-pixel coverage, paint brush/stroke/stroke-script coverage, renderer shader descriptor coverage, UI color parsing, and layout XML parse coverage. - `pano_cli inspect-image` reports PNG IHDR metadata as JSON and is covered by @@ -103,8 +103,12 @@ Known local toolchain state: minimal PPI fixture. - `pp_assets_image_pixels_tests` decodes PNG payloads to RGBA8 and rejects corrupt image payloads. -- `pano_cli load-project` creates a metadata-only `pp_document` projection with - per-layer frame counts and durations, and is covered by +- `pp_document_ppi_import_tests` attaches decoded PPI dirty-face payloads to + `pp_document` layer/frame storage and rejects payloads outside document + layers. +- `pano_cli load-project` creates a `pp_document` projection with per-layer + frame counts, durations, and decoded face-pixel payloads when present; the + metadata-only minimal fixture remains covered by `pano_cli_load_project_metadata_smoke`. - `pano_cli create-document` supports `--frames` and `--frame-duration-ms` and is covered by `pano_cli_create_animation_document_smoke`. diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 1c13005..71c2c09 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -28,10 +28,10 @@ agent or engineer to remove them without reconstructing context from chat. | DEBT-0007 | Open | Modernization | `vcpkg.json` and `windows-msvc-vcpkg-headless` are validated for the headless Windows component matrix, but app targets still use vendored libraries and Android/Apple triplets are not proven | Dependency migration must stay incremental while SDK/patched/vendor dependencies remain in use | `$env:VCPKG_ROOT="C:\Program Files\Microsoft Visual Studio\2022\Community\VC\vcpkg"; cmake --preset windows-msvc-vcpkg-headless`; `ctest --preset desktop-fast-vcpkg --build-config Debug` | Component targets consume vcpkg packages where reliable and desktop app, Android, and Apple triplets are validated or explicitly documented as permanent vendor exceptions | | DEBT-0008 | Open | Modernization | `windows-msvc-default` preset is used for local validation because the VS 2026 generator is not installed here | The target VS 2026 preset must remain, but this machine configures with Visual Studio 17 2022 | `cmake --preset windows-msvc-default`; `ctest --preset desktop-fast --build-config Debug` | Validate `windows-vs2026-x64` on a machine with Visual Studio 2026 installed and make it the default Windows validation preset | | DEBT-0009 | Open | Modernization | Android root CMake validation currently builds headless targets only, not APK/package variants | Platform app entrypoints still live in legacy Gradle/CMake projects and need Phase 6 alignment | `powershell -ExecutionPolicy Bypass -File scripts\automation\platform-build.ps1 -Presets android-arm64` | Android standard, Quest, and Focus/Wave package targets consume shared component targets and have package smoke commands | -| DEBT-0010 | Open | Modernization | `pp_document` is a pure layer/frame/document/undo-history model with alpha-lock metadata, snapshot construction, and per-layer frame metadata, but it is not yet wired to legacy `Canvas`, PPI pixel load/save, selection masks, or legacy action commands | Keep extraction incremental while preserving app behavior | `ctest --preset desktop-fast --build-config Debug`; `pano_cli create-document --width 64 --height 32 --layers 2`; `pano_cli load-project --path tests\data\projects\minimal-project.ppi` | Legacy document behavior is represented by `pp_document` tests and the app consumes it through a boundary/facade | +| DEBT-0010 | Open | Modernization | `pp_document` is a pure layer/frame/document/undo-history model with alpha-lock metadata, snapshot construction, per-layer frame metadata, and renderer-free RGBA8 face payload storage, but it is not yet wired to legacy `Canvas`, selection masks, save, or legacy action commands | Keep extraction incremental while preserving app behavior | `ctest --preset desktop-fast --build-config Debug`; `pano_cli create-document --width 64 --height 32 --layers 2`; `pano_cli load-project --path tests\data\projects\minimal-project.ppi`; `pp_document_ppi_import_tests` | Legacy document behavior is represented by `pp_document` tests and the app consumes it through a boundary/facade | | DEBT-0011 | Open | Modernization | `package-smoke` validates the Windows CMake app artifact only, not AppX/APK/Apple/WebGL package outputs | Platform package targets are not migrated to root CMake yet | `powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -Preset windows-msvc-default -Configuration Debug` | Package-smoke covers Windows AppX, Android APK variants, Apple bundles, and WebGL output where local toolchains are present | | DEBT-0012 | Open | Modernization | `pp_ui_core` uses vcpkg tinyxml2 on `windows-msvc-vcpkg-headless`, but retains `pp_vendor_tinyxml2` for default and unproven platform presets | Mobile/AppX/Apple triplets and app packaging still need validation before removing the vendored fallback | `ctest --preset desktop-fast-vcpkg --build-config Debug`; `ctest --preset desktop-fast --build-config Debug`; `powershell -ExecutionPolicy Bypass -File scripts\automation\platform-build.ps1 -Presets android-arm64` | All supported presets consume vcpkg tinyxml2 or document a permanent vendored exception | -| DEBT-0013 | Open | Modernization | `pp_assets`, `pano_cli inspect-project`, and metadata-only `pano_cli load-project` validate the fixed PPI header, thumbnail/body byte layout, layer/frame index, dirty-face descriptors, dirty-face PNG payload metadata, and asset-level RGBA PNG payload decoding, but do not yet attach decoded pixels to `pp_document` layer/frame/face storage | Full PPI parsing requires staged extraction of legacy `Canvas` serialization and image/layer payload handling | `ctest --preset desktop-fast --build-config Debug`; `pp_assets_image_pixels_tests`; `pp_assets_ppi_header_tests`; `pano_cli_inspect_project_layout_smoke`; `pano_cli_load_project_metadata_smoke` | Full PPI load/save fixtures cover thumbnail, decoded layer face payloads attached to documents, frames, metadata, corrupt payloads, and round-trip compatibility | +| DEBT-0013 | Open | Modernization | `pp_assets`, `pano_cli inspect-project`, and `pano_cli load-project` validate the fixed PPI header, thumbnail/body byte layout, layer/frame index, dirty-face descriptors, dirty-face PNG payload metadata, asset-level RGBA PNG payload decoding, and decoded pixel attachment to `pp_document`, but full PPI save/round-trip fixtures are not yet extracted | Full PPI parsing requires staged extraction of legacy `Canvas` serialization and image/layer payload handling | `ctest --preset desktop-fast --build-config Debug`; `pp_assets_image_pixels_tests`; `pp_assets_ppi_header_tests`; `pp_document_ppi_import_tests`; `pano_cli_inspect_project_layout_smoke`; `pano_cli_load_project_metadata_smoke` | Full PPI load/save fixtures cover thumbnail, decoded layer face payloads attached to documents, frames, metadata, corrupt payloads, and round-trip compatibility | | DEBT-0014 | Open | Modernization | `windows-clangcl-asan` now configures as a headless Ninja/clang-cl preset and uses the release MSVC runtime required by ASan, but local builds still fail because installed clang-cl 18.1.8 is paired with VS 2026-preview STL headers that require Clang 20 or newer | Sanitizer validation should be local and repeatable, but this machine's compiler/header pairing is incompatible | `cmake --fresh --preset windows-clangcl-asan`; `cmake --build --preset windows-clangcl-asan --target pp_foundation` | Install/use Clang 20+ with the VS 2026 STL, or point the preset at a compatible VS 2022 toolchain, then make `platform-build.ps1 -Presets windows-clangcl-asan` pass for the headless matrix | ## Closed Debt diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 6bca35b..99e9343 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -323,7 +323,8 @@ stroke spacing/interpolation plus a pure text stroke-script parser. `pp_document` has started with a pure canvas/layer/frame model, alpha-lock metadata, snapshot construction, per-layer frame metadata, layer metadata operations, frame -move/duration queries, and layer/frame/undo-redo history invariant tests. +move/duration queries, renderer-free RGBA8 cube-face payload storage, PPI image +import, 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 @@ -334,8 +335,9 @@ input tests. `pano_cli inspect-project` reports validated PPI thumbnail/body byte layout, body summary, layer/frame descriptors, dirty-face PNG payload metadata, and asset-level decode coverage, and -`pano_cli load-project` creates a metadata-only `pp_document` projection with -per-layer frame counts and durations. +`pano_cli load-project` creates a `pp_document` projection with per-layer frame +counts, durations, and decoded face-pixel payload attachment when PPI image +payloads are present. `pano_cli create-document` can create simple animation documents with explicit frame count/duration, and `pano_cli simulate-stroke` exercises the pure stroke sampler for scripted-stroke automation. `pano_cli simulate-stroke-script` @@ -548,7 +550,7 @@ Last verified on 2026-06-01: ```powershell cmake --preset windows-msvc-default -cmake --build --preset windows-msvc-default --config Debug --target pp_foundation_binary_stream_tests pp_foundation_event_tests pp_foundation_log_tests pp_foundation_parse_tests pp_foundation_task_queue_tests pp_foundation_trace_tests pp_assets_image_format_tests pp_assets_image_metadata_tests pp_assets_image_pixels_tests pp_assets_ppi_header_tests pp_assets_settings_document_tests pp_paint_brush_tests pp_paint_blend_tests pp_paint_stroke_tests pp_document_tests pp_renderer_api_tests pp_paint_renderer_compositor_tests pp_ui_core_color_tests pp_ui_core_layout_value_tests pp_ui_core_layout_xml_tests pano_cli PanoPainter +cmake --build --preset windows-msvc-default --config Debug --target pp_foundation_binary_stream_tests pp_foundation_event_tests pp_foundation_log_tests pp_foundation_parse_tests pp_foundation_task_queue_tests pp_foundation_trace_tests pp_assets_image_format_tests pp_assets_image_metadata_tests pp_assets_image_pixels_tests pp_assets_ppi_header_tests pp_assets_settings_document_tests pp_paint_brush_tests pp_paint_blend_tests pp_paint_stroke_tests pp_document_tests pp_document_ppi_import_tests pp_renderer_api_tests pp_paint_renderer_compositor_tests pp_ui_core_color_tests pp_ui_core_layout_value_tests pp_ui_core_layout_xml_tests pano_cli PanoPainter ctest --preset desktop-fast --build-config Debug powershell -ExecutionPolicy Bypass -File scripts\automation\test.ps1 -Preset desktop-fast -Configuration Debug powershell -ExecutionPolicy Bypass -File scripts\automation\build.ps1 -Preset windows-msvc-default -Configuration Debug -Target pano_cli @@ -585,8 +587,11 @@ Results: - `pp_paint_stroke_tests` passed. - `pp_paint_stroke_script_tests` passed. - `pp_document_tests` passed, including snapshot construction, alpha-lock - metadata, per-layer frame metadata, frame move, duration, and history - invariants. + metadata, per-layer frame metadata, frame move, duration, face-pixel payload + storage/replacement/rejection, and history invariants. +- `pp_document_ppi_import_tests` passed, including decoded PPI dirty-face + payload attachment to `pp_document` layer/frame storage and out-of-range + payload rejection. - `pp_renderer_api_tests` passed, including shader descriptor validation. - `pp_paint_renderer_compositor_tests` passed. - `pp_ui_core_color_tests` passed. @@ -602,9 +607,9 @@ Results: - `pano_cli_inspect_project_layout_smoke` passed and reports PPI thumbnail/body byte layout, body summary, layer/frame descriptors, and dirty-face PNG payload metadata JSON. -- `pano_cli_load_project_metadata_smoke` passed and reports a metadata-only - `pp_document` projection with per-layer frame counts and durations for the - minimal PPI fixture. +- `pano_cli_load_project_metadata_smoke` passed and reports a `pp_document` + projection with per-layer frame counts, durations, and zero loaded face + payloads for the minimal PPI fixture. - `pano_cli_parse_layout_smoke` passed. - `pano_cli_simulate_stroke_smoke` passed and reports deterministic stroke sample counts/distances. diff --git a/scripts/automation/platform-build.ps1 b/scripts/automation/platform-build.ps1 index 401caf0..eb343c4 100644 --- a/scripts/automation/platform-build.ps1 +++ b/scripts/automation/platform-build.ps1 @@ -1,7 +1,7 @@ [CmdletBinding()] param( [string[]]$Presets = @("android-arm64"), - [string[]]$Targets = @("pp_foundation", "pp_assets", "pp_paint", "pp_document", "pp_renderer_api", "pp_paint_renderer", "pp_ui_core", "pano_cli", "pp_foundation_binary_stream_tests", "pp_foundation_event_tests", "pp_foundation_log_tests", "pp_foundation_parse_tests", "pp_foundation_task_queue_tests", "pp_foundation_trace_tests", "pp_assets_image_format_tests", "pp_assets_image_metadata_tests", "pp_assets_image_pixels_tests", "pp_assets_ppi_header_tests", "pp_assets_settings_document_tests", "pp_paint_brush_tests", "pp_paint_blend_tests", "pp_paint_stroke_tests", "pp_paint_stroke_script_tests", "pp_document_tests", "pp_renderer_api_tests", "pp_paint_renderer_compositor_tests", "pp_ui_core_color_tests", "pp_ui_core_layout_value_tests", "pp_ui_core_layout_xml_tests") + [string[]]$Targets = @("pp_foundation", "pp_assets", "pp_paint", "pp_document", "pp_renderer_api", "pp_paint_renderer", "pp_ui_core", "pano_cli", "pp_foundation_binary_stream_tests", "pp_foundation_event_tests", "pp_foundation_log_tests", "pp_foundation_parse_tests", "pp_foundation_task_queue_tests", "pp_foundation_trace_tests", "pp_assets_image_format_tests", "pp_assets_image_metadata_tests", "pp_assets_image_pixels_tests", "pp_assets_ppi_header_tests", "pp_assets_settings_document_tests", "pp_paint_brush_tests", "pp_paint_blend_tests", "pp_paint_stroke_tests", "pp_paint_stroke_script_tests", "pp_document_tests", "pp_document_ppi_import_tests", "pp_renderer_api_tests", "pp_paint_renderer_compositor_tests", "pp_ui_core_color_tests", "pp_ui_core_layout_value_tests", "pp_ui_core_layout_xml_tests") ) $ErrorActionPreference = "Stop" diff --git a/scripts/automation/platform-build.sh b/scripts/automation/platform-build.sh index be4d948..b367c14 100644 --- a/scripts/automation/platform-build.sh +++ b/scripts/automation/platform-build.sh @@ -3,7 +3,7 @@ set -u preset="${1:-android-arm64}" shift || true -targets="${*:-pp_foundation pp_assets pp_paint pp_document pp_renderer_api pp_paint_renderer pp_ui_core pano_cli pp_foundation_binary_stream_tests pp_foundation_event_tests pp_foundation_log_tests pp_foundation_parse_tests pp_foundation_task_queue_tests pp_foundation_trace_tests pp_assets_image_format_tests pp_assets_image_metadata_tests pp_assets_image_pixels_tests pp_assets_ppi_header_tests pp_assets_settings_document_tests pp_paint_brush_tests pp_paint_blend_tests pp_paint_stroke_tests pp_paint_stroke_script_tests pp_document_tests pp_renderer_api_tests pp_paint_renderer_compositor_tests pp_ui_core_color_tests pp_ui_core_layout_value_tests pp_ui_core_layout_xml_tests}" +targets="${*:-pp_foundation pp_assets pp_paint pp_document pp_renderer_api pp_paint_renderer pp_ui_core pano_cli pp_foundation_binary_stream_tests pp_foundation_event_tests pp_foundation_log_tests pp_foundation_parse_tests pp_foundation_task_queue_tests pp_foundation_trace_tests pp_assets_image_format_tests pp_assets_image_metadata_tests pp_assets_image_pixels_tests pp_assets_ppi_header_tests pp_assets_settings_document_tests pp_paint_brush_tests pp_paint_blend_tests pp_paint_stroke_tests pp_paint_stroke_script_tests pp_document_tests pp_document_ppi_import_tests pp_renderer_api_tests pp_paint_renderer_compositor_tests pp_ui_core_color_tests pp_ui_core_layout_value_tests pp_ui_core_layout_xml_tests}" start="$(date +%s)" cmake --preset "$preset" diff --git a/src/document/document.cpp b/src/document/document.cpp index 143bd35..ff70823 100644 --- a/src/document/document.cpp +++ b/src/document/document.cpp @@ -2,6 +2,8 @@ #include #include +#include +#include namespace pp::document { @@ -105,6 +107,67 @@ namespace { return pp::foundation::Status::success(); } +[[nodiscard]] pp::foundation::Result rgba8_byte_size( + std::uint32_t width, + std::uint32_t height) noexcept +{ + const auto width64 = static_cast(width); + const auto height64 = static_cast(height); + if (width64 > std::numeric_limits::max() / height64) { + return pp::foundation::Result::failure( + pp::foundation::Status::out_of_range("face pixel dimensions overflow")); + } + + const auto pixels = width64 * height64; + if (pixels > std::numeric_limits::max() / rgba8_components) { + return pp::foundation::Result::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::failure( + pp::foundation::Status::out_of_range("face pixel payload exceeds the configured limit")); + } + + if (bytes > static_cast(std::numeric_limits::max())) { + return pp::foundation::Result::failure( + pp::foundation::Status::out_of_range("face pixel payload exceeds addressable memory")); + } + + return pp::foundation::Result::success(static_cast(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::create(DocumentConfig config) @@ -251,6 +314,17 @@ pp::foundation::Result CanvasDocument::layer_animation_duration_m return pp::foundation::Result::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 CanvasDocument::layers() const noexcept { return layers_; @@ -423,9 +497,9 @@ pp::foundation::Result 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::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::create( CanvasDocument initial_document, std::size_t max_entries) diff --git a/src/document/document.h b/src/document/document.h index 62c9680..6d65844 100644 --- a/src/document/document.h +++ b/src/document/document.h @@ -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 rgba8; +}; + struct AnimationFrame { std::uint32_t duration_ms = 100; + std::vector 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 layer_animation_duration_ms(std::size_t index) const noexcept; + [[nodiscard]] std::size_t face_pixel_payload_count() const noexcept; [[nodiscard]] std::span layers() const noexcept; [[nodiscard]] std::span 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; diff --git a/src/document/ppi_import.cpp b/src/document/ppi_import.cpp new file mode 100644 index 0000000..175d896 --- /dev/null +++ b/src/document/ppi_import.cpp @@ -0,0 +1,116 @@ +#include "document/ppi_import.h" + +#include +#include +#include + +namespace pp::document { + +namespace { + +[[nodiscard]] pp::foundation::Result ppi_layer_blend_mode( + std::uint32_t blend_mode) noexcept +{ + switch (blend_mode) { + case 0: + return pp::foundation::Result::success(pp::paint::BlendMode::normal); + case 1: + return pp::foundation::Result::success(pp::paint::BlendMode::multiply); + case 2: + return pp::foundation::Result::success(pp::paint::BlendMode::screen); + case 3: + return pp::foundation::Result::success(pp::paint::BlendMode::color_dodge); + case 4: + return pp::foundation::Result::success(pp::paint::BlendMode::overlay); + default: + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("PPI layer blend mode is not supported by pp_document")); + } +} + +[[nodiscard]] pp::foundation::Result document_from_ppi_index( + const pp::assets::PpiProjectIndex& project) +{ + if (project.body.layers.empty()) { + return pp::foundation::Result::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::failure( + pp::foundation::Status::invalid_argument("PPI project has no frames")); + } + + std::vector 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> layer_frames; + layer_frames.reserve(project.body.layers.size()); + std::vector 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::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(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 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::failure(status); + } + } + + return pp::foundation::Result::success(std::move(value)); +} + +} diff --git a/src/document/ppi_import.h b/src/document/ppi_import.h new file mode 100644 index 0000000..0930ee7 --- /dev/null +++ b/src/document/ppi_import.h @@ -0,0 +1,11 @@ +#pragma once + +#include "assets/ppi_header.h" +#include "document/document.h" + +namespace pp::document { + +[[nodiscard]] pp::foundation::Result import_ppi_project_document( + const pp::assets::PpiDecodedProjectImages& project); + +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7ac518a..f83dbd3 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -166,6 +166,16 @@ add_test(NAME pp_document_tests COMMAND pp_document_tests) set_tests_properties(pp_document_tests PROPERTIES LABELS "document;desktop-fast") +add_executable(pp_document_ppi_import_tests + document/ppi_import_tests.cpp) +target_link_libraries(pp_document_ppi_import_tests PRIVATE + pp_document + pp_test_harness) + +add_test(NAME pp_document_ppi_import_tests COMMAND pp_document_ppi_import_tests) +set_tests_properties(pp_document_ppi_import_tests PROPERTIES + LABELS "assets;document;integration;desktop-fast") + add_executable(pp_renderer_api_tests renderer_api/renderer_api_tests.cpp) target_link_libraries(pp_renderer_api_tests PRIVATE @@ -250,7 +260,7 @@ if(TARGET pano_cli) COMMAND pano_cli load-project --path "${CMAKE_CURRENT_SOURCE_DIR}/data/projects/minimal-project.ppi") set_tests_properties(pano_cli_load_project_metadata_smoke PROPERTIES LABELS "assets;document;integration;desktop-fast" - PASS_REGULAR_EXPRESSION "\"command\":\"load-project\".*\"pixelDataLoaded\":false.*\"document\":\\{\"width\":64,\"height\":32,\"layers\":1,\"frames\":1,\"animationDurationMs\":100,\"layerNames\":\\[\"Ink\"\\],\"layerFrameCounts\":\\[1\\],\"layerDurationsMs\":\\[100\\]") + PASS_REGULAR_EXPRESSION "\"command\":\"load-project\".*\"pixelDataLoaded\":false.*\"facePayloads\":0.*\"document\":\\{\"width\":64,\"height\":32,\"layers\":1,\"frames\":1,\"animationDurationMs\":100,\"layerNames\":\\[\"Ink\"\\],\"layerFrameCounts\":\\[1\\],\"layerDurationsMs\":\\[100\\]") add_test(NAME pano_cli_parse_layout_smoke COMMAND pano_cli parse-layout --path "${CMAKE_CURRENT_SOURCE_DIR}/data/layouts/simple-layout.xml") diff --git a/tests/document/document_tests.cpp b/tests/document/document_tests.cpp index 4939a47..da04ddc 100644 --- a/tests/document/document_tests.cpp +++ b/tests/document/document_tests.cpp @@ -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); diff --git a/tests/document/ppi_import_tests.cpp b/tests/document/ppi_import_tests.cpp new file mode 100644 index 0000000..90a95a8 --- /dev/null +++ b/tests/document/ppi_import_tests.cpp @@ -0,0 +1,148 @@ +#include "assets/ppi_header.h" +#include "document/ppi_import.h" +#include "test_harness.h" + +#include +#include +#include +#include +#include + +using pp::assets::decode_ppi_project_images; +using pp::foundation::StatusCode; +using pp::document::import_ppi_project_document; + +namespace { + +void append_u32(std::vector& bytes, std::uint32_t value) +{ + bytes.push_back(static_cast(value & 0xffU)); + bytes.push_back(static_cast((value >> 8U) & 0xffU)); + bytes.push_back(static_cast((value >> 16U) & 0xffU)); + bytes.push_back(static_cast((value >> 24U) & 0xffU)); +} + +void append_f32(std::vector& bytes, float value) +{ + append_u32(bytes, std::bit_cast(value)); +} + +void append_ascii(std::vector& bytes, std::string_view value) +{ + for (const auto ch : value) { + bytes.push_back(static_cast(ch)); + } +} + +std::vector 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 ppi_project_with_face_payload(std::vector payload) +{ + std::vector 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(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(); +} diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index ed45e37..1b20375 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -2,6 +2,7 @@ #include "assets/image_metadata.h" #include "assets/ppi_header.h" #include "document/document.h" +#include "document/ppi_import.h" #include "foundation/parse.h" #include "foundation/result.h" #include "paint/stroke.h" @@ -405,79 +406,6 @@ int inspect_project(int argc, char** argv) return 0; } -pp::foundation::Result ppi_layer_blend_mode(std::uint32_t blend_mode) noexcept -{ - switch (blend_mode) { - case 0: - return pp::foundation::Result::success(pp::paint::BlendMode::normal); - case 1: - return pp::foundation::Result::success(pp::paint::BlendMode::multiply); - case 2: - return pp::foundation::Result::success(pp::paint::BlendMode::screen); - case 3: - return pp::foundation::Result::success(pp::paint::BlendMode::color_dodge); - case 4: - return pp::foundation::Result::success(pp::paint::BlendMode::overlay); - default: - return pp::foundation::Result::failure( - pp::foundation::Status::invalid_argument("PPI layer blend mode is not supported by pp_document")); - } -} - -pp::foundation::Result document_from_ppi_index( - const pp::assets::PpiProjectIndex& project) -{ - if (project.body.layers.empty()) { - return pp::foundation::Result::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::failure( - pp::foundation::Status::invalid_argument("PPI project has no frames")); - } - - std::vector frames; - frames.reserve(reference_frames.size()); - for (const auto& frame : reference_frames) { - frames.push_back(pp::document::AnimationFrame { .duration_ms = frame.duration_ms }); - } - - std::vector> layer_frames; - layer_frames.reserve(project.body.layers.size()); - std::vector 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::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(pp::document::AnimationFrame { .duration_ms = frame.duration_ms }); - } - - layers.push_back(pp::document::DocumentLayerConfig { - .name = layer.name, - .visible = layer.visible, - .alpha_locked = layer.alpha_locked, - .opacity = layer.opacity, - .blend_mode = blend_mode.value(), - .frames = std::span(frame_list.data(), frame_list.size()), - }); - } - - return pp::document::CanvasDocument::create_from_snapshot(pp::document::DocumentSnapshotConfig { - .width = project.body.summary.width, - .height = project.body.summary.height, - .layers = layers, - .frames = frames, - }); -} - int load_project(int argc, char** argv) { InspectProjectArgs args; @@ -498,13 +426,13 @@ int load_project(int argc, char** argv) std::istreambuf_iterator() }; const auto* data = reinterpret_cast(chars.data()); - const auto project = pp::assets::parse_ppi_project_index(std::span(data, chars.size())); + const auto project = pp::assets::decode_ppi_project_images(std::span(data, chars.size())); if (!project) { print_error("load-project", project.status().message); return 2; } - const auto document_result = document_from_ppi_index(project.value()); + const auto document_result = pp::document::import_ppi_project_document(project.value()); if (!document_result) { print_error("load-project", document_result.status().message); return 2; @@ -513,7 +441,8 @@ int load_project(int argc, char** argv) const auto& document = document_result.value(); std::cout << "{\"ok\":true,\"command\":\"load-project\"" << ",\"source\":\"ppi\"" - << ",\"pixelDataLoaded\":false" + << ",\"pixelDataLoaded\":" << (document.face_pixel_payload_count() > 0U ? "true" : "false") + << ",\"facePayloads\":" << document.face_pixel_payload_count() << ",\"document\":{\"width\":" << document.width() << ",\"height\":" << document.height() << ",\"layers\":" << document.layers().size()