diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index c4f5bb8..b743e7f 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -115,9 +115,10 @@ Known local toolchain state: 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 save-project` writes metadata-only one-layer/one-frame PPI files - through `pp_assets` and is covered by `pano_cli_save_project_roundtrip_smoke`, - which reloads the generated project through `pano_cli load-project`. +- `pano_cli save-project` writes one-layer/one-frame PPI files through + `pp_assets` and is covered by `pano_cli_save_project_roundtrip_smoke` and + `pano_cli_save_project_payload_roundtrip_smoke`, which reload generated + metadata-only and dirty-face-payload projects through `pano_cli load-project`. - `pano_cli create-document` supports `--frames` and `--frame-duration-ms` and is covered by `pano_cli_create_animation_document_smoke`. - `pano_cli simulate-document-edits` exercises pure document layer/frame edit @@ -274,9 +275,9 @@ Known local toolchain state: face-payload attachment through JSON automation and is covered by `pano_cli_import_image_rejects_truncated_png`; a checked-in decodable image fixture and export/round-trip automation remain future CLI tasks. -- `pano_cli save-project` exposes metadata-only PPI writing through JSON - automation and is covered by `pano_cli_save_project_roundtrip_smoke`; full - dirty-face payload save parity remains tracked by DEBT-0013. +- `pano_cli save-project` exposes one-layer/one-frame PPI writing through JSON + automation and is covered by metadata-only and dirty-face-payload round-trip + smoke tests; full legacy canvas save parity remains tracked by DEBT-0013. - `pp_ui_core` consumes vcpkg tinyxml2 only when `PP_USE_VCPKG_TINYXML2=ON` through the vcpkg preset; default and Android validation still use the retained vendored fallback tracked by DEBT-0012. diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index a75d039..b631476 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -31,7 +31,7 @@ agent or engineer to remove them without reconstructing context from chat. | 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`, `pano_cli load-project`, and `pano_cli save-project` validate the fixed PPI header, thumbnail/body byte layout, metadata-only PPI writing, metadata-only save/load round-trip, 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 dirty-face payload saving and full legacy PPI round-trip parity are not yet extracted | Full PPI save parity 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`; `pano_cli_save_project_roundtrip_smoke` | Full PPI load/save fixtures cover thumbnail, decoded layer face payloads attached to documents, frames, metadata, corrupt payloads, dirty-face payload saving, and round-trip compatibility | +| DEBT-0013 | Open | Modernization | `pp_assets`, `pano_cli inspect-project`, `pano_cli load-project`, and `pano_cli save-project` validate the fixed PPI header, thumbnail/body byte layout, one-layer/one-frame PPI writing, metadata-only and dirty-face-payload save/load round-trips, 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 legacy PPI round-trip parity is not yet extracted | Full PPI save parity 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`; `pano_cli_save_project_roundtrip_smoke`; `pano_cli_save_project_payload_roundtrip_smoke` | Full PPI load/save fixtures cover thumbnails, decoded layer face payloads attached to documents, frames, metadata, corrupt payloads, dirty-face payload saving, multiple layers/frames, and legacy app 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 8f68716..970fe88 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -340,8 +340,9 @@ asset-level decode coverage, and `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 save-project` writes a metadata-only one-layer/one-frame PPI through -the extracted `pp_assets` writer and round-trips it through `load-project`. +`pano_cli save-project` writes one-layer/one-frame PPI files through the +extracted `pp_assets` writer and round-trips metadata-only and test +dirty-face-payload variants through `load-project`. `pano_cli create-document` can create simple animation documents with explicit frame count/duration. `pano_cli simulate-document-edits` exercises pure layer metadata, frame reordering, active-index preservation, and tiny @@ -718,6 +719,9 @@ Results: - `pano_cli_save_project_roundtrip_smoke` passed and proves the metadata-only `pp_assets` PPI writer can save a generated PPI and reload it through `pano_cli load-project`. +- `pano_cli_save_project_payload_roundtrip_smoke` passed and proves the + `pp_assets` PPI writer can save a compressed RGBA PNG dirty-face payload and + reload it as decoded `pp_document` face-pixel data. - `pano_cli_parse_layout_smoke` passed. - `pano_cli_simulate_stroke_smoke` passed and reports deterministic stroke sample counts/distances. @@ -782,9 +786,10 @@ Results: `pp_assets`, attaches them to a pure `pp_document` face payload, and has a truncated-PNG rejection smoke test. A checked-in decodable image fixture and full export/round-trip automation remain future `pano_cli` tasks. -- `pano_cli save-project` exposes a metadata-only PPI writer through JSON - automation and is covered by a save/load round-trip smoke test. Full - dirty-face payload save parity remains tracked by DEBT-0013. +- `pano_cli save-project` exposes one-layer/one-frame PPI writing through JSON + automation and is covered by metadata-only and dirty-face-payload save/load + round-trip smoke tests. Full legacy canvas save parity remains tracked by + DEBT-0013. - PowerShell package-smoke wrapper validates the Windows CMake app executable and runtime `data/` copy. - Android arm64 configured with NDK 29.0.14206865 through the platform-build diff --git a/src/assets/ppi_header.cpp b/src/assets/ppi_header.cpp index 16cb8cd..9edd3bc 100644 --- a/src/assets/ppi_header.cpp +++ b/src/assets/ppi_header.cpp @@ -659,6 +659,54 @@ pp::foundation::Result> create_minimal_ppi_project(PpiMin pp::foundation::Status::invalid_argument("PPI frame duration must be greater than zero")); } + bool seen_faces[6] {}; + std::uint64_t total_payload_bytes = 0; + for (const auto& face : config.dirty_faces) { + if (face.face_index >= 6U) { + return pp::foundation::Result>::failure( + pp::foundation::Status::out_of_range("PPI dirty face index is outside the cube face list")); + } + + if (seen_faces[face.face_index]) { + return pp::foundation::Result>::failure( + pp::foundation::Status::invalid_argument("PPI dirty face index is duplicated")); + } + seen_faces[face.face_index] = true; + + if (face.width == 0 || face.height == 0) { + return pp::foundation::Result>::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>::failure( + pp::foundation::Status::out_of_range("PPI dirty face box is outside the canvas")); + } + + if (face.png_rgba8.empty()) { + return pp::foundation::Result>::failure( + pp::foundation::Status::invalid_argument("PPI dirty face PNG payload must not be empty")); + } + + if (face.png_rgba8.size() > static_cast(std::numeric_limits::max())) { + return pp::foundation::Result>::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>::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>::failure(metadata.status()); + } + } + std::vector bytes { std::byte { 'P' }, std::byte { 'P' }, @@ -692,7 +740,26 @@ pp::foundation::Result> create_minimal_ppi_project(PpiMin append_u32(bytes, 1); append_u32(bytes, config.frame_duration_ms); for (std::uint32_t face = 0; face < 6U; ++face) { - append_u32(bytes, 0); + const PpiDirtyFacePayloadConfig* dirty_face = nullptr; + for (const auto& candidate : config.dirty_faces) { + if (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(dirty_face->png_rgba8.size())); + bytes.insert(bytes.end(), dirty_face->png_rgba8.begin(), dirty_face->png_rgba8.end()); } append_u32(bytes, 0); diff --git a/src/assets/ppi_header.h b/src/assets/ppi_header.h index 721143d..6c38326 100644 --- a/src/assets/ppi_header.h +++ b/src/assets/ppi_header.h @@ -118,11 +118,21 @@ struct PpiDecodedProjectImages { std::vector faces; }; +struct PpiDirtyFacePayloadConfig { + 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::span png_rgba8; +}; + struct PpiMinimalProjectConfig { std::uint32_t width = 0; std::uint32_t height = 0; std::string layer_name; std::uint32_t frame_duration_ms = 100; + std::span dirty_faces; }; [[nodiscard]] pp::foundation::Result parse_ppi_header( diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 5a3b697..35301ab 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -291,6 +291,14 @@ if(TARGET pano_cli) set_tests_properties(pano_cli_save_project_roundtrip_smoke PROPERTIES LABELS "assets;document;integration;desktop-fast") + add_test(NAME pano_cli_save_project_payload_roundtrip_smoke + COMMAND "${CMAKE_COMMAND}" + -DPANO_CLI=$ + -DOUTPUT_PATH=${CMAKE_CURRENT_BINARY_DIR}/data/generated/payload-roundtrip.ppi + -P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/pano_cli_save_project_payload_roundtrip.cmake") + set_tests_properties(pano_cli_save_project_payload_roundtrip_smoke PROPERTIES + LABELS "assets;document;integration;desktop-fast") + add_test(NAME pano_cli_parse_layout_smoke COMMAND pano_cli parse-layout --path "${CMAKE_CURRENT_SOURCE_DIR}/data/layouts/simple-layout.xml") set_tests_properties(pano_cli_parse_layout_smoke PROPERTIES diff --git a/tests/assets/ppi_header_tests.cpp b/tests/assets/ppi_header_tests.cpp index 8eccb4d..863368f 100644 --- a/tests/assets/ppi_header_tests.cpp +++ b/tests/assets/ppi_header_tests.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -392,6 +393,7 @@ void creates_minimal_project_for_roundtrip_load(pp::tests::Harness& h) .height = 128, .layer_name = "Roundtrip", .frame_duration_ms = 333, + .dirty_faces = {}, }); PP_EXPECT(h, project.ok()); @@ -409,25 +411,90 @@ void creates_minimal_project_for_roundtrip_load(pp::tests::Harness& h) PP_EXPECT(h, index.value().body.layers[0].frames[0].duration_ms == 333U); } +void creates_minimal_project_with_dirty_face_payload(pp::tests::Harness& h) +{ + const auto png_payload = transparent_png_1x1(); + const pp::assets::PpiDirtyFacePayloadConfig dirty_faces[] { + { + .face_index = 2, + .x = 4, + .y = 5, + .width = 1, + .height = 1, + .png_rgba8 = std::span(png_payload.data(), png_payload.size()), + }, + }; + const auto project = create_minimal_ppi_project(pp::assets::PpiMinimalProjectConfig { + .width = 256, + .height = 128, + .layer_name = "Payload", + .frame_duration_ms = 333, + .dirty_faces = std::span(dirty_faces, 1), + }); + + PP_EXPECT(h, project.ok()); + const auto decoded = decode_ppi_project_images(project.value()); + PP_EXPECT(h, decoded.ok()); + PP_EXPECT(h, decoded.value().project.body.summary.dirty_face_count == 1U); + PP_EXPECT(h, decoded.value().project.body.summary.rgba_face_payload_count == 1U); + PP_EXPECT(h, decoded.value().project.body.summary.compressed_face_bytes == png_payload.size()); + PP_EXPECT(h, decoded.value().faces.size() == 1U); + PP_EXPECT(h, decoded.value().faces[0].face_index == 2U); + PP_EXPECT(h, decoded.value().faces[0].descriptor.x0 == 4U); + PP_EXPECT(h, decoded.value().faces[0].descriptor.y0 == 5U); + PP_EXPECT(h, decoded.value().faces[0].image.width == 1U); + PP_EXPECT(h, decoded.value().faces[0].image.height == 1U); + PP_EXPECT(h, decoded.value().faces[0].image.pixels.size() == 4U); +} + void rejects_invalid_minimal_project_writer_inputs(pp::tests::Harness& h) { + const auto png_payload = transparent_png_1x1(); + const pp::assets::PpiDirtyFacePayloadConfig duplicate_faces[] { + { + .face_index = 0, + .x = 0, + .y = 0, + .width = 1, + .height = 1, + .png_rgba8 = std::span(png_payload.data(), png_payload.size()), + }, + { + .face_index = 0, + .x = 1, + .y = 1, + .width = 1, + .height = 1, + .png_rgba8 = std::span(png_payload.data(), png_payload.size()), + }, + }; const auto no_size = create_minimal_ppi_project(pp::assets::PpiMinimalProjectConfig { .width = 0, .height = 128, .layer_name = "Ink", .frame_duration_ms = 100, + .dirty_faces = {}, }); const auto no_name = create_minimal_ppi_project(pp::assets::PpiMinimalProjectConfig { .width = 128, .height = 128, .layer_name = "", .frame_duration_ms = 100, + .dirty_faces = {}, }); const auto no_duration = create_minimal_ppi_project(pp::assets::PpiMinimalProjectConfig { .width = 128, .height = 128, .layer_name = "Ink", .frame_duration_ms = 0, + .dirty_faces = {}, + }); + const auto duplicate_dirty_face = create_minimal_ppi_project(pp::assets::PpiMinimalProjectConfig { + .width = 128, + .height = 128, + .layer_name = "Ink", + .frame_duration_ms = 100, + .dirty_faces = std::span(duplicate_faces, 2), }); PP_EXPECT(h, !no_size.ok()); @@ -436,6 +503,8 @@ void rejects_invalid_minimal_project_writer_inputs(pp::tests::Harness& h) PP_EXPECT(h, no_name.status().code == StatusCode::invalid_argument); PP_EXPECT(h, !no_duration.ok()); PP_EXPECT(h, no_duration.status().code == StatusCode::invalid_argument); + PP_EXPECT(h, !duplicate_dirty_face.ok()); + PP_EXPECT(h, duplicate_dirty_face.status().code == StatusCode::invalid_argument); } } @@ -456,6 +525,7 @@ int main() harness.run("rejects_invalid_dirty_face_png_payloads", rejects_invalid_dirty_face_png_payloads); harness.run("rejects_invalid_project_body_summaries", rejects_invalid_project_body_summaries); harness.run("creates_minimal_project_for_roundtrip_load", creates_minimal_project_for_roundtrip_load); + harness.run("creates_minimal_project_with_dirty_face_payload", creates_minimal_project_with_dirty_face_payload); harness.run("rejects_invalid_minimal_project_writer_inputs", rejects_invalid_minimal_project_writer_inputs); return harness.finish(); } diff --git a/tests/cmake/pano_cli_save_project_payload_roundtrip.cmake b/tests/cmake/pano_cli_save_project_payload_roundtrip.cmake new file mode 100644 index 0000000..a93261f --- /dev/null +++ b/tests/cmake/pano_cli_save_project_payload_roundtrip.cmake @@ -0,0 +1,62 @@ +if(NOT DEFINED PANO_CLI) + message(FATAL_ERROR "PANO_CLI must be set") +endif() + +if(NOT DEFINED OUTPUT_PATH) + message(FATAL_ERROR "OUTPUT_PATH must be set") +endif() + +get_filename_component(output_dir "${OUTPUT_PATH}" DIRECTORY) +file(MAKE_DIRECTORY "${output_dir}") +file(REMOVE "${OUTPUT_PATH}") + +execute_process( + COMMAND "${PANO_CLI}" save-project + --path "${OUTPUT_PATH}" + --width 96 + --height 48 + --layer-name Payload + --frame-duration-ms 321 + --include-test-face-payload + RESULT_VARIABLE save_result + OUTPUT_VARIABLE save_output + ERROR_VARIABLE save_error) + +if(NOT save_result EQUAL 0) + message(FATAL_ERROR "save-project with payload failed: ${save_output}${save_error}") +endif() + +string(FIND "${save_output}" "\"command\":\"save-project\"" save_command_index) +string(FIND "${save_output}" "\"facePayloads\":1" save_payload_index) +if(save_command_index LESS 0 OR save_payload_index LESS 0) + message(FATAL_ERROR "save-project payload output did not contain expected summary: ${save_output}") +endif() + +if(NOT EXISTS "${OUTPUT_PATH}") + message(FATAL_ERROR "save-project did not create ${OUTPUT_PATH}") +endif() + +execute_process( + COMMAND "${PANO_CLI}" load-project --path "${OUTPUT_PATH}" + RESULT_VARIABLE load_result + OUTPUT_VARIABLE load_output + ERROR_VARIABLE load_error) + +if(NOT load_result EQUAL 0) + message(FATAL_ERROR "load-project failed after payload save-project: ${load_output}${load_error}") +endif() + +string(FIND "${load_output}" "\"command\":\"load-project\"" load_command_index) +string(FIND "${load_output}" "\"pixelDataLoaded\":true" load_pixels_index) +string(FIND "${load_output}" "\"facePayloads\":1" load_payload_index) +string(FIND "${load_output}" "\"width\":96" load_width_index) +string(FIND "${load_output}" "\"height\":48" load_height_index) +string(FIND "${load_output}" "\"layerNames\":[\"Payload\"]" load_layer_index) +if(load_command_index LESS 0 + OR load_pixels_index LESS 0 + OR load_payload_index LESS 0 + OR load_width_index LESS 0 + OR load_height_index LESS 0 + OR load_layer_index LESS 0) + message(FATAL_ERROR "load-project output did not contain expected payload summary: ${load_output}") +endif() diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index 356877f..75e7c73 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -37,6 +37,7 @@ struct SaveProjectArgs { std::uint32_t height = 0; std::string layer_name = "Ink"; std::uint32_t frame_duration_ms = 100; + bool include_test_face_payload = false; }; struct InspectImageArgs { @@ -99,6 +100,30 @@ void print_error(std::string_view command, std::string_view message) << "\",\"error\":\"" << message << "\"}\n"; } +std::span transparent_png_1x1_bytes() noexcept +{ + static constexpr std::array transparent_png_1x1 { + 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 }, + }; + return transparent_png_1x1; +} + std::string json_escape(std::string_view value) { constexpr char hex[] = "0123456789abcdef"; @@ -152,7 +177,7 @@ void print_help() << " load-project --path FILE\n" << " parse-layout --path FILE\n" << " record-render [--width N] [--height N]\n" - << " save-project --path FILE --width N --height N [--layer-name NAME] [--frame-duration-ms N]\n" + << " save-project --path FILE --width N --height N [--layer-name NAME] [--frame-duration-ms N] [--include-test-face-payload]\n" << " simulate-document-edits [--width N] [--height N]\n" << " simulate-document-history [--width N] [--height N] [--history N]\n" << " simulate-image-import [--width N] [--height N]\n" @@ -289,6 +314,8 @@ pp::foundation::Status parse_save_project_args(int argc, char** argv, SaveProjec } else { args.frame_duration_ms = value.value(); } + } else if (key == "--include-test-face-payload") { + args.include_test_face_payload = true; } else { return pp::foundation::Status::invalid_argument("unknown option"); } @@ -322,11 +349,25 @@ int save_project(int argc, char** argv) return 2; } + const auto test_payload = transparent_png_1x1_bytes(); + const pp::assets::PpiDirtyFacePayloadConfig dirty_faces[] { + { + .face_index = 0, + .x = 0, + .y = 0, + .width = 1, + .height = 1, + .png_rgba8 = test_payload, + }, + }; const auto project = pp::assets::create_minimal_ppi_project(pp::assets::PpiMinimalProjectConfig { .width = args.width, .height = args.height, .layer_name = args.layer_name, .frame_duration_ms = args.frame_duration_ms, + .dirty_faces = args.include_test_face_payload + ? std::span(dirty_faces, 1) + : std::span(), }); if (!project) { print_error("save-project", project.status().message); @@ -356,6 +397,7 @@ int save_project(int argc, char** argv) << ",\"frames\":1" << ",\"layerName\":\"" << json_escape(args.layer_name) << "\"" << ",\"frameDurationMs\":" << args.frame_duration_ms + << ",\"facePayloads\":" << (args.include_test_face_payload ? 1 : 0) << "}}\n"; return 0; } @@ -1111,26 +1153,6 @@ pp::foundation::Status parse_simulate_image_import_args( int simulate_image_import(int argc, char** argv) { - static constexpr std::array transparent_png_1x1 { - 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 }, - }; - SimulateImageImportArgs args; const auto status = parse_simulate_image_import_args(argc, argv, args); if (!status.ok()) { @@ -1138,7 +1160,7 @@ int simulate_image_import(int argc, char** argv) return 2; } - const auto image_result = pp::assets::decode_png_rgba8(transparent_png_1x1); + const auto image_result = pp::assets::decode_png_rgba8(transparent_png_1x1_bytes()); if (!image_result) { print_error("simulate-image-import", image_result.status().message); return 2;