Add multi-layer PPI save automation
This commit is contained in:
@@ -115,7 +115,7 @@ 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 single-layer multi-frame PPI files through
|
||||
- `pano_cli save-project` writes generated multi-layer, multi-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`.
|
||||
@@ -275,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 single-layer multi-frame PPI writing through JSON
|
||||
- `pano_cli save-project` exposes generated multi-layer, multi-frame PPI writing through JSON
|
||||
automation and is covered by metadata-only and dirty-face-payload round-trip
|
||||
smoke tests; full multi-layer legacy canvas save parity remains tracked by
|
||||
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
|
||||
|
||||
@@ -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, single-layer multi-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, and legacy app 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, generated multi-layer/multi-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, broader legacy layer metadata, 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
|
||||
|
||||
@@ -340,7 +340,7 @@ 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 single-layer multi-frame PPI files through the
|
||||
`pano_cli save-project` writes generated multi-layer, multi-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
|
||||
@@ -786,9 +786,9 @@ 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 single-layer multi-frame PPI writing through JSON
|
||||
- `pano_cli save-project` exposes generated multi-layer, multi-frame PPI writing through JSON
|
||||
automation and is covered by metadata-only and dirty-face-payload save/load
|
||||
round-trip smoke tests. Full multi-layer legacy canvas save parity remains
|
||||
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.
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include <bit>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
@@ -89,6 +90,18 @@ void append_ascii(std::vector<std::byte>& bytes, std::string_view value)
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string generated_layer_name(std::string_view base_name, std::uint32_t layer_index, std::uint32_t layer_count)
|
||||
{
|
||||
if (layer_count == 1U) {
|
||||
return std::string(base_name);
|
||||
}
|
||||
|
||||
std::string name(base_name);
|
||||
name.push_back(' ');
|
||||
name += std::to_string(layer_index + 1U);
|
||||
return name;
|
||||
}
|
||||
|
||||
[[nodiscard]] pp::foundation::Status add_payload_bytes(PpiBodySummary& summary, std::uint32_t bytes) noexcept
|
||||
{
|
||||
const auto next = summary.compressed_face_bytes + static_cast<std::uint64_t>(bytes);
|
||||
@@ -654,6 +667,11 @@ pp::foundation::Result<std::vector<std::byte>> create_minimal_ppi_project(PpiMin
|
||||
pp::foundation::Status::out_of_range("PPI layer name exceeds the configured limit"));
|
||||
}
|
||||
|
||||
if (config.layer_count == 0 || config.layer_count > max_ppi_layer_count) {
|
||||
return pp::foundation::Result<std::vector<std::byte>>::failure(
|
||||
pp::foundation::Status::out_of_range("PPI layer count is outside the configured range"));
|
||||
}
|
||||
|
||||
if (config.frame_duration_ms == 0) {
|
||||
return pp::foundation::Result<std::vector<std::byte>>::failure(
|
||||
pp::foundation::Status::invalid_argument("PPI frame duration must be greater than zero"));
|
||||
@@ -664,6 +682,19 @@ pp::foundation::Result<std::vector<std::byte>> create_minimal_ppi_project(PpiMin
|
||||
pp::foundation::Status::out_of_range("PPI frame count is outside the configured range"));
|
||||
}
|
||||
|
||||
if (config.layer_count > max_ppi_frame_count / config.frame_count) {
|
||||
return pp::foundation::Result<std::vector<std::byte>>::failure(
|
||||
pp::foundation::Status::out_of_range("PPI total layer frame count exceeds the configured range"));
|
||||
}
|
||||
|
||||
for (std::uint32_t layer = 0; layer < config.layer_count; ++layer) {
|
||||
const auto name = generated_layer_name(config.layer_name, layer, config.layer_count);
|
||||
if (name.size() > max_ppi_layer_name_length) {
|
||||
return pp::foundation::Result<std::vector<std::byte>>::failure(
|
||||
pp::foundation::Status::out_of_range("PPI generated layer name exceeds the configured limit"));
|
||||
}
|
||||
}
|
||||
|
||||
bool seen_faces[6] {};
|
||||
std::uint64_t total_payload_bytes = 0;
|
||||
for (const auto& face : config.dirty_faces) {
|
||||
@@ -733,41 +764,44 @@ pp::foundation::Result<std::vector<std::byte>> create_minimal_ppi_project(PpiMin
|
||||
|
||||
append_u32(bytes, config.width);
|
||||
append_u32(bytes, config.height);
|
||||
append_u32(bytes, 1);
|
||||
append_u32(bytes, config.frame_count);
|
||||
append_u32(bytes, 0);
|
||||
append_f32(bytes, 1.0F);
|
||||
append_u32(bytes, static_cast<std::uint32_t>(config.layer_name.size()));
|
||||
append_ascii(bytes, config.layer_name);
|
||||
append_u32(bytes, 0);
|
||||
bytes.push_back(std::byte { 0 });
|
||||
bytes.push_back(std::byte { 1 });
|
||||
append_u32(bytes, config.frame_count);
|
||||
for (std::uint32_t frame = 0; frame < config.frame_count; ++frame) {
|
||||
append_u32(bytes, config.frame_duration_ms);
|
||||
for (std::uint32_t face = 0; face < 6U; ++face) {
|
||||
const PpiDirtyFacePayloadConfig* dirty_face = nullptr;
|
||||
if (frame == 0U) {
|
||||
for (const auto& candidate : config.dirty_faces) {
|
||||
if (candidate.face_index == face) {
|
||||
dirty_face = &candidate;
|
||||
break;
|
||||
append_u32(bytes, config.layer_count);
|
||||
append_u32(bytes, config.layer_count * config.frame_count);
|
||||
for (std::uint32_t layer = 0; layer < config.layer_count; ++layer) {
|
||||
const auto name = generated_layer_name(config.layer_name, layer, config.layer_count);
|
||||
append_u32(bytes, layer);
|
||||
append_f32(bytes, 1.0F);
|
||||
append_u32(bytes, static_cast<std::uint32_t>(name.size()));
|
||||
append_ascii(bytes, name);
|
||||
append_u32(bytes, 0);
|
||||
bytes.push_back(std::byte { 0 });
|
||||
bytes.push_back(std::byte { 1 });
|
||||
append_u32(bytes, config.frame_count);
|
||||
for (std::uint32_t frame = 0; frame < config.frame_count; ++frame) {
|
||||
append_u32(bytes, config.frame_duration_ms);
|
||||
for (std::uint32_t face = 0; face < 6U; ++face) {
|
||||
const PpiDirtyFacePayloadConfig* dirty_face = nullptr;
|
||||
if (layer == 0U && frame == 0U) {
|
||||
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;
|
||||
}
|
||||
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<std::uint32_t>(dirty_face->png_rgba8.size()));
|
||||
bytes.insert(bytes.end(), dirty_face->png_rgba8.begin(), dirty_face->png_rgba8.end());
|
||||
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<std::uint32_t>(dirty_face->png_rgba8.size()));
|
||||
bytes.insert(bytes.end(), dirty_face->png_rgba8.begin(), dirty_face->png_rgba8.end());
|
||||
}
|
||||
}
|
||||
}
|
||||
append_u32(bytes, 0);
|
||||
|
||||
@@ -131,6 +131,7 @@ struct PpiMinimalProjectConfig {
|
||||
std::uint32_t width = 0;
|
||||
std::uint32_t height = 0;
|
||||
std::string layer_name;
|
||||
std::uint32_t layer_count = 1;
|
||||
std::uint32_t frame_count = 1;
|
||||
std::uint32_t frame_duration_ms = 100;
|
||||
std::span<const PpiDirtyFacePayloadConfig> dirty_faces;
|
||||
|
||||
@@ -392,6 +392,7 @@ void creates_minimal_project_for_roundtrip_load(pp::tests::Harness& h)
|
||||
.width = 256,
|
||||
.height = 128,
|
||||
.layer_name = "Roundtrip",
|
||||
.layer_count = 1,
|
||||
.frame_count = 1,
|
||||
.frame_duration_ms = 333,
|
||||
.dirty_faces = {},
|
||||
@@ -412,12 +413,42 @@ 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_multiple_layers(pp::tests::Harness& h)
|
||||
{
|
||||
const auto project = create_minimal_ppi_project(pp::assets::PpiMinimalProjectConfig {
|
||||
.width = 256,
|
||||
.height = 128,
|
||||
.layer_name = "Layer",
|
||||
.layer_count = 2,
|
||||
.frame_count = 2,
|
||||
.frame_duration_ms = 111,
|
||||
.dirty_faces = {},
|
||||
});
|
||||
|
||||
PP_EXPECT(h, project.ok());
|
||||
const auto index = parse_ppi_project_index(project.value());
|
||||
PP_EXPECT(h, index.ok());
|
||||
PP_EXPECT(h, index.value().body.summary.layer_count == 2U);
|
||||
PP_EXPECT(h, index.value().body.summary.declared_frame_count == 4U);
|
||||
PP_EXPECT(h, index.value().body.summary.total_layer_frames == 4U);
|
||||
PP_EXPECT(h, index.value().body.layers.size() == 2U);
|
||||
PP_EXPECT(h, index.value().body.layers[0].stored_order == 0U);
|
||||
PP_EXPECT(h, index.value().body.layers[1].stored_order == 1U);
|
||||
PP_EXPECT(h, index.value().body.layers[0].name == "Layer 1");
|
||||
PP_EXPECT(h, index.value().body.layers[1].name == "Layer 2");
|
||||
PP_EXPECT(h, index.value().body.layers[0].frames.size() == 2U);
|
||||
PP_EXPECT(h, index.value().body.layers[1].frames.size() == 2U);
|
||||
PP_EXPECT(h, index.value().body.layers[0].frames[1].duration_ms == 111U);
|
||||
PP_EXPECT(h, index.value().body.layers[1].frames[1].duration_ms == 111U);
|
||||
}
|
||||
|
||||
void creates_minimal_project_with_multiple_frames(pp::tests::Harness& h)
|
||||
{
|
||||
const auto project = create_minimal_ppi_project(pp::assets::PpiMinimalProjectConfig {
|
||||
.width = 256,
|
||||
.height = 128,
|
||||
.layer_name = "Frames",
|
||||
.layer_count = 1,
|
||||
.frame_count = 3,
|
||||
.frame_duration_ms = 111,
|
||||
.dirty_faces = {},
|
||||
@@ -451,6 +482,7 @@ void creates_minimal_project_with_dirty_face_payload(pp::tests::Harness& h)
|
||||
.width = 256,
|
||||
.height = 128,
|
||||
.layer_name = "Payload",
|
||||
.layer_count = 1,
|
||||
.frame_count = 1,
|
||||
.frame_duration_ms = 333,
|
||||
.dirty_faces = std::span<const pp::assets::PpiDirtyFacePayloadConfig>(dirty_faces, 1),
|
||||
@@ -496,6 +528,7 @@ void rejects_invalid_minimal_project_writer_inputs(pp::tests::Harness& h)
|
||||
.width = 0,
|
||||
.height = 128,
|
||||
.layer_name = "Ink",
|
||||
.layer_count = 1,
|
||||
.frame_count = 1,
|
||||
.frame_duration_ms = 100,
|
||||
.dirty_faces = {},
|
||||
@@ -504,6 +537,7 @@ void rejects_invalid_minimal_project_writer_inputs(pp::tests::Harness& h)
|
||||
.width = 128,
|
||||
.height = 128,
|
||||
.layer_name = "",
|
||||
.layer_count = 1,
|
||||
.frame_count = 1,
|
||||
.frame_duration_ms = 100,
|
||||
.dirty_faces = {},
|
||||
@@ -512,6 +546,7 @@ void rejects_invalid_minimal_project_writer_inputs(pp::tests::Harness& h)
|
||||
.width = 128,
|
||||
.height = 128,
|
||||
.layer_name = "Ink",
|
||||
.layer_count = 1,
|
||||
.frame_count = 1,
|
||||
.frame_duration_ms = 0,
|
||||
.dirty_faces = {},
|
||||
@@ -520,14 +555,25 @@ void rejects_invalid_minimal_project_writer_inputs(pp::tests::Harness& h)
|
||||
.width = 128,
|
||||
.height = 128,
|
||||
.layer_name = "Ink",
|
||||
.layer_count = 1,
|
||||
.frame_count = 0,
|
||||
.frame_duration_ms = 100,
|
||||
.dirty_faces = {},
|
||||
});
|
||||
const auto no_layers = create_minimal_ppi_project(pp::assets::PpiMinimalProjectConfig {
|
||||
.width = 128,
|
||||
.height = 128,
|
||||
.layer_name = "Ink",
|
||||
.layer_count = 0,
|
||||
.frame_count = 1,
|
||||
.frame_duration_ms = 100,
|
||||
.dirty_faces = {},
|
||||
});
|
||||
const auto duplicate_dirty_face = create_minimal_ppi_project(pp::assets::PpiMinimalProjectConfig {
|
||||
.width = 128,
|
||||
.height = 128,
|
||||
.layer_name = "Ink",
|
||||
.layer_count = 1,
|
||||
.frame_count = 1,
|
||||
.frame_duration_ms = 100,
|
||||
.dirty_faces = std::span<const pp::assets::PpiDirtyFacePayloadConfig>(duplicate_faces, 2),
|
||||
@@ -541,6 +587,8 @@ void rejects_invalid_minimal_project_writer_inputs(pp::tests::Harness& h)
|
||||
PP_EXPECT(h, no_duration.status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !no_frames.ok());
|
||||
PP_EXPECT(h, no_frames.status().code == StatusCode::out_of_range);
|
||||
PP_EXPECT(h, !no_layers.ok());
|
||||
PP_EXPECT(h, no_layers.status().code == StatusCode::out_of_range);
|
||||
PP_EXPECT(h, !duplicate_dirty_face.ok());
|
||||
PP_EXPECT(h, duplicate_dirty_face.status().code == StatusCode::invalid_argument);
|
||||
}
|
||||
@@ -563,6 +611,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_multiple_layers", creates_minimal_project_with_multiple_layers);
|
||||
harness.run("creates_minimal_project_with_multiple_frames", creates_minimal_project_with_multiple_frames);
|
||||
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);
|
||||
|
||||
@@ -16,6 +16,7 @@ execute_process(
|
||||
--width 96
|
||||
--height 48
|
||||
--layer-name Payload
|
||||
--layers 2
|
||||
--frames 2
|
||||
--frame-duration-ms 321
|
||||
--include-test-face-payload
|
||||
@@ -28,8 +29,10 @@ if(NOT save_result EQUAL 0)
|
||||
endif()
|
||||
|
||||
string(FIND "${save_output}" "\"command\":\"save-project\"" save_command_index)
|
||||
string(FIND "${save_output}" "\"layers\":2" save_layers_index)
|
||||
string(FIND "${save_output}" "\"frames\":2" save_frames_index)
|
||||
string(FIND "${save_output}" "\"facePayloads\":1" save_payload_index)
|
||||
if(save_command_index LESS 0 OR save_payload_index LESS 0)
|
||||
if(save_command_index LESS 0 OR save_layers_index LESS 0 OR save_frames_index LESS 0 OR save_payload_index LESS 0)
|
||||
message(FATAL_ERROR "save-project payload output did not contain expected summary: ${save_output}")
|
||||
endif()
|
||||
|
||||
@@ -52,15 +55,17 @@ 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}" "\"layers\":2" load_layers_index)
|
||||
string(FIND "${load_output}" "\"frames\":2" load_frames_index)
|
||||
string(FIND "${load_output}" "\"animationDurationMs\":642" load_duration_index)
|
||||
string(FIND "${load_output}" "\"layerNames\":[\"Payload\"]" load_layer_index)
|
||||
string(FIND "${load_output}" "\"layerFrameCounts\":[2]" load_layer_frames_index)
|
||||
string(FIND "${load_output}" "\"layerNames\":[\"Payload 1\",\"Payload 2\"]" load_layer_index)
|
||||
string(FIND "${load_output}" "\"layerFrameCounts\":[2,2]" load_layer_frames_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_layers_index LESS 0
|
||||
OR load_frames_index LESS 0
|
||||
OR load_duration_index LESS 0
|
||||
OR load_layer_index LESS 0
|
||||
|
||||
@@ -16,6 +16,7 @@ execute_process(
|
||||
--width 96
|
||||
--height 48
|
||||
--layer-name Roundtrip
|
||||
--layers 2
|
||||
--frames 3
|
||||
--frame-duration-ms 321
|
||||
RESULT_VARIABLE save_result
|
||||
@@ -27,9 +28,10 @@ if(NOT save_result EQUAL 0)
|
||||
endif()
|
||||
|
||||
string(FIND "${save_output}" "\"command\":\"save-project\"" save_command_index)
|
||||
string(FIND "${save_output}" "\"bytes\":65711" save_bytes_index)
|
||||
string(FIND "${save_output}" "\"bytes\":65830" save_bytes_index)
|
||||
string(FIND "${save_output}" "\"layers\":2" save_layers_index)
|
||||
string(FIND "${save_output}" "\"frames\":3" save_frames_index)
|
||||
if(save_command_index LESS 0 OR save_bytes_index LESS 0 OR save_frames_index LESS 0)
|
||||
if(save_command_index LESS 0 OR save_bytes_index LESS 0 OR save_layers_index LESS 0 OR save_frames_index LESS 0)
|
||||
message(FATAL_ERROR "save-project output did not contain expected summary: ${save_output}")
|
||||
endif()
|
||||
|
||||
@@ -50,14 +52,16 @@ endif()
|
||||
string(FIND "${load_output}" "\"command\":\"load-project\"" load_command_index)
|
||||
string(FIND "${load_output}" "\"width\":96" load_width_index)
|
||||
string(FIND "${load_output}" "\"height\":48" load_height_index)
|
||||
string(FIND "${load_output}" "\"layers\":2" load_layers_index)
|
||||
string(FIND "${load_output}" "\"frames\":3" load_frames_index)
|
||||
string(FIND "${load_output}" "\"animationDurationMs\":963" load_duration_index)
|
||||
string(FIND "${load_output}" "\"layerNames\":[\"Roundtrip\"]" load_layer_index)
|
||||
string(FIND "${load_output}" "\"layerFrameCounts\":[3]" load_layer_frames_index)
|
||||
string(FIND "${load_output}" "\"layerDurationsMs\":[963]" load_layer_durations_index)
|
||||
string(FIND "${load_output}" "\"layerNames\":[\"Roundtrip 1\",\"Roundtrip 2\"]" load_layer_index)
|
||||
string(FIND "${load_output}" "\"layerFrameCounts\":[3,3]" load_layer_frames_index)
|
||||
string(FIND "${load_output}" "\"layerDurationsMs\":[963,963]" load_layer_durations_index)
|
||||
if(load_command_index LESS 0
|
||||
OR load_width_index LESS 0
|
||||
OR load_height_index LESS 0
|
||||
OR load_layers_index LESS 0
|
||||
OR load_frames_index LESS 0
|
||||
OR load_duration_index LESS 0
|
||||
OR load_layer_index LESS 0
|
||||
|
||||
@@ -36,6 +36,7 @@ struct SaveProjectArgs {
|
||||
std::uint32_t width = 0;
|
||||
std::uint32_t height = 0;
|
||||
std::string layer_name = "Ink";
|
||||
std::uint32_t layers = 1;
|
||||
std::uint32_t frames = 1;
|
||||
std::uint32_t frame_duration_ms = 100;
|
||||
bool include_test_face_payload = false;
|
||||
@@ -178,7 +179,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] [--frames N] [--frame-duration-ms N] [--include-test-face-payload]\n"
|
||||
<< " save-project --path FILE --width N --height N [--layer-name NAME] [--layers N] [--frames N] [--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"
|
||||
@@ -298,7 +299,8 @@ pp::foundation::Status parse_save_project_args(int argc, char** argv, SaveProjec
|
||||
} else {
|
||||
args.layer_name = argv[++i];
|
||||
}
|
||||
} else if (key == "--width" || key == "--height" || key == "--frames" || key == "--frame-duration-ms") {
|
||||
} else if (key == "--width" || key == "--height" || key == "--layers" || key == "--frames"
|
||||
|| key == "--frame-duration-ms") {
|
||||
if (i + 1 >= argc) {
|
||||
return pp::foundation::Status::invalid_argument("missing value for option");
|
||||
}
|
||||
@@ -312,6 +314,8 @@ pp::foundation::Status parse_save_project_args(int argc, char** argv, SaveProjec
|
||||
args.width = value.value();
|
||||
} else if (key == "--height") {
|
||||
args.height = value.value();
|
||||
} else if (key == "--layers") {
|
||||
args.layers = value.value();
|
||||
} else if (key == "--frames") {
|
||||
args.frames = value.value();
|
||||
} else {
|
||||
@@ -336,6 +340,10 @@ pp::foundation::Status parse_save_project_args(int argc, char** argv, SaveProjec
|
||||
return pp::foundation::Status::invalid_argument("layer name must not be empty");
|
||||
}
|
||||
|
||||
if (args.layers == 0) {
|
||||
return pp::foundation::Status::invalid_argument("layer count must be greater than zero");
|
||||
}
|
||||
|
||||
if (args.frames == 0) {
|
||||
return pp::foundation::Status::invalid_argument("frame count must be greater than zero");
|
||||
}
|
||||
@@ -371,6 +379,7 @@ int save_project(int argc, char** argv)
|
||||
.width = args.width,
|
||||
.height = args.height,
|
||||
.layer_name = args.layer_name,
|
||||
.layer_count = args.layers,
|
||||
.frame_count = args.frames,
|
||||
.frame_duration_ms = args.frame_duration_ms,
|
||||
.dirty_faces = args.include_test_face_payload
|
||||
@@ -401,7 +410,7 @@ int save_project(int argc, char** argv)
|
||||
<< ",\"bytes\":" << bytes.size()
|
||||
<< ",\"document\":{\"width\":" << args.width
|
||||
<< ",\"height\":" << args.height
|
||||
<< ",\"layers\":1"
|
||||
<< ",\"layers\":" << args.layers
|
||||
<< ",\"frames\":" << args.frames
|
||||
<< ",\"layerName\":\"" << json_escape(args.layer_name) << "\""
|
||||
<< ",\"frameDurationMs\":" << args.frame_duration_ms
|
||||
|
||||
Reference in New Issue
Block a user