From b0445382ddeba350ea7169f7d4ca4dad465ab654 Mon Sep 17 00:00:00 2001 From: omigamedev Date: Tue, 2 Jun 2026 10:04:48 +0200 Subject: [PATCH] Add file-driven image import automation --- docs/modernization/build-inventory.md | 11 +- docs/modernization/roadmap.md | 11 +- tests/CMakeLists.txt | 9 ++ ...expect_pano_cli_import_image_failure.cmake | 28 ++++ tools/pano_cli/main.cpp | 140 ++++++++++++++++++ 5 files changed, 196 insertions(+), 3 deletions(-) create mode 100644 tests/cmake/expect_pano_cli_import_image_failure.cmake diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index 7cf7468..ac446c2 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -98,6 +98,10 @@ Known local toolchain state: capability coverage, UI color parsing, and layout XML parse coverage. - `pano_cli inspect-image` reports PNG IHDR metadata as JSON and is covered by `pano_cli_inspect_png_metadata_smoke` with a tiny IHDR fixture. +- `pano_cli import-image` accepts a PNG path, decodes RGBA8 pixels through + `pp_assets`, attaches them to a pure `pp_document` face payload, and is + covered for metadata-valid truncated PNG rejection by + `pano_cli_import_image_rejects_truncated_png`. - `pano_cli inspect-project` reports validated PPI thumbnail/body byte layout, body summary fields, layer/frame descriptors, and dirty-face PNG payload metadata, and is covered by `pano_cli_inspect_project_layout_smoke` with a @@ -262,8 +266,11 @@ Known local toolchain state: automation and is covered by `pano_cli_simulate_document_edits_smoke`. - `pano_cli simulate-image-import` exposes embedded PNG decode and document face-payload attachment through JSON automation and is covered by - `pano_cli_simulate_image_import_smoke`; full file import/export remains a - future CLI automation task. + `pano_cli_simulate_image_import_smoke`. +- `pano_cli import-image` exposes file-driven PNG decode and document + 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. - `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/roadmap.md b/docs/modernization/roadmap.md index 57b964d..7ec7996 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -332,6 +332,8 @@ the paint blend reference. `pp_ui_core` has started with XML-layout-facing length parsing, color parsing, tinyxml-backed layout XML parsing, and invalid input tests. `pano_cli inspect-image` exposes PNG IHDR metadata as JSON, +`pano_cli import-image` accepts a PNG path and imports decoded RGBA8 pixels +into a new pure `pp_document` face payload, `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 @@ -702,6 +704,9 @@ Results: test. - `pano_cli_inspect_png_metadata_smoke` passed and reports PNG metadata JSON for the tiny IHDR fixture. +- `pano_cli_import_image_rejects_truncated_png` passed as an expected failure + test, proving the file-driven image import command rejects a metadata-valid + but undecodable PNG payload. - `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. @@ -767,7 +772,11 @@ Results: agent automation. - `pano_cli simulate-image-import` exercises embedded PNG decode through `pp_assets` and `pp_document` face-payload attachment through JSON - automation. Full file import/export remains a future `pano_cli` task. + automation. +- `pano_cli import-image` accepts a PNG file path, decodes RGBA8 pixels through + `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. - 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/tests/CMakeLists.txt b/tests/CMakeLists.txt index d74b281..8cd59bb 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -262,6 +262,15 @@ if(TARGET pano_cli) LABELS "assets;integration;desktop-fast" PASS_REGULAR_EXPRESSION "\"format\":\"png\".*\"width\":320.*\"height\":240.*\"components\":4.*\"colorType\":\"rgba\"") + add_test(NAME pano_cli_import_image_rejects_truncated_png + COMMAND "${CMAKE_COMMAND}" + -DPANO_CLI=$ + -DIMAGE_PATH=${CMAKE_CURRENT_SOURCE_DIR}/data/images/tiny-rgba-header.png + "-DEXPECTED_OUTPUT=PNG payload could not be decoded" + -P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/expect_pano_cli_import_image_failure.cmake") + set_tests_properties(pano_cli_import_image_rejects_truncated_png PROPERTIES + LABELS "assets;document;integration;desktop-fast") + add_test(NAME pano_cli_inspect_project_layout_smoke COMMAND pano_cli inspect-project --path "${CMAKE_CURRENT_SOURCE_DIR}/data/projects/minimal-project.ppi") set_tests_properties(pano_cli_inspect_project_layout_smoke PROPERTIES diff --git a/tests/cmake/expect_pano_cli_import_image_failure.cmake b/tests/cmake/expect_pano_cli_import_image_failure.cmake new file mode 100644 index 0000000..82d99d6 --- /dev/null +++ b/tests/cmake/expect_pano_cli_import_image_failure.cmake @@ -0,0 +1,28 @@ +if(NOT DEFINED PANO_CLI) + message(FATAL_ERROR "PANO_CLI must be set") +endif() + +if(NOT DEFINED IMAGE_PATH) + message(FATAL_ERROR "IMAGE_PATH must be set") +endif() + +if(NOT DEFINED EXPECTED_OUTPUT) + message(FATAL_ERROR "EXPECTED_OUTPUT must be set") +endif() + +execute_process( + COMMAND "${PANO_CLI}" import-image --path "${IMAGE_PATH}" + RESULT_VARIABLE result + OUTPUT_VARIABLE output + ERROR_VARIABLE error) + +if(result EQUAL 0) + message(FATAL_ERROR "Expected pano_cli import-image to fail, but it exited 0") +endif() + +set(combined_output "${output}${error}") +string(FIND "${combined_output}" "${EXPECTED_OUTPUT}" expected_index) +if(expected_index LESS 0) + message(FATAL_ERROR + "Expected output to contain '${EXPECTED_OUTPUT}', got: ${combined_output}") +endif() diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index c98d073..090cd31 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -35,6 +35,15 @@ struct InspectImageArgs { std::string path; }; +struct ImportImageArgs { + std::string path; + std::uint32_t document_width = 0; + std::uint32_t document_height = 0; + std::uint32_t face = 0; + std::uint32_t x = 0; + std::uint32_t y = 0; +}; + struct ParseLayoutArgs { std::string path; }; @@ -130,6 +139,7 @@ void print_help() << "pano_cli commands:\n" << " create-document --width N --height N [--layers N] [--frames N] [--frame-duration-ms N]\n" << " inspect-image --path FILE\n" + << " import-image --path FILE [--document-width N] [--document-height N] [--face N] [--x N] [--y N]\n" << " inspect-project --path FILE\n" << " load-project --path FILE\n" << " parse-layout --path FILE\n" @@ -317,6 +327,132 @@ int inspect_image(int argc, char** argv) return 0; } +pp::foundation::Status parse_import_image_args(int argc, char** argv, ImportImageArgs& args) +{ + for (int i = 2; i < argc; ++i) { + const std::string_view key(argv[i]); + if (key == "--path") { + if (i + 1 >= argc) { + return pp::foundation::Status::invalid_argument("missing value for option"); + } + + args.path = argv[++i]; + } else if (key == "--document-width" || key == "--document-height" + || key == "--face" || key == "--x" || key == "--y") { + if (i + 1 >= argc) { + return pp::foundation::Status::invalid_argument("missing value for option"); + } + + const auto value = pp::foundation::parse_u32(argv[++i]); + if (!value) { + return value.status(); + } + + if (key == "--document-width") { + args.document_width = value.value(); + } else if (key == "--document-height") { + args.document_height = value.value(); + } else if (key == "--face") { + args.face = value.value(); + } else if (key == "--x") { + args.x = value.value(); + } else { + args.y = value.value(); + } + } else { + return pp::foundation::Status::invalid_argument("unknown option"); + } + } + + if (args.path.empty()) { + return pp::foundation::Status::invalid_argument("path must not be empty"); + } + + if ((args.document_width == 0) != (args.document_height == 0)) { + return pp::foundation::Status::invalid_argument( + "document width and height must both be omitted or both be greater than zero"); + } + + return pp::foundation::Status::success(); +} + +int import_image(int argc, char** argv) +{ + ImportImageArgs args; + const auto status = parse_import_image_args(argc, argv, args); + if (!status.ok()) { + print_error("import-image", status.message); + return 2; + } + + std::ifstream stream(args.path, std::ios::binary); + if (!stream) { + print_error("import-image", "image file could not be opened"); + return 2; + } + + const std::vector chars { + std::istreambuf_iterator(stream), + std::istreambuf_iterator() + }; + const auto* data = reinterpret_cast(chars.data()); + const auto image_result = pp::assets::decode_png_rgba8(std::span(data, chars.size())); + if (!image_result) { + print_error("import-image", image_result.status().message); + return 2; + } + + const auto& image = image_result.value(); + const std::uint32_t document_width = args.document_width == 0 ? image.width : args.document_width; + const std::uint32_t document_height = args.document_height == 0 ? image.height : args.document_height; + const auto document_result = pp::document::CanvasDocument::create( + pp::document::DocumentConfig { + .width = document_width, + .height = document_height, + .layer_count = 1, + }); + if (!document_result) { + print_error("import-image", document_result.status().message); + return 2; + } + + auto document = document_result.value(); + const auto import_status = document.set_layer_frame_face_pixels( + 0, + 0, + pp::document::LayerFacePixels { + .face_index = args.face, + .x = args.x, + .y = args.y, + .width = image.width, + .height = image.height, + .rgba8 = image.pixels, + }); + if (!import_status.ok()) { + print_error("import-image", import_status.message); + return 2; + } + + std::cout << "{\"ok\":true,\"command\":\"import-image\"" + << ",\"source\":\"" << json_escape(args.path) << "\"" + << ",\"image\":{\"format\":\"png\",\"width\":" << image.width + << ",\"height\":" << image.height + << ",\"bytes\":" << image.pixels.size() + << "},\"document\":{\"width\":" << document.width() + << ",\"height\":" << document.height() + << ",\"layers\":" << document.layers().size() + << ",\"frames\":" << document.frames().size() + << ",\"facePayloads\":" << document.face_pixel_payload_count() + << "},\"payload\":{\"face\":" << args.face + << ",\"x\":" << args.x + << ",\"y\":" << args.y + << ",\"width\":" << image.width + << ",\"height\":" << image.height + << ",\"bytes\":" << image.pixels.size() + << "}}\n"; + return 0; +} + pp::foundation::Status parse_inspect_project_args(int argc, char** argv, InspectProjectArgs& args) { for (int i = 2; i < argc; ++i) { @@ -1268,6 +1404,10 @@ int main(int argc, char** argv) return inspect_image(argc, argv); } + if (command == "import-image") { + return import_image(argc, argv); + } + if (command == "inspect-project") { return inspect_project(argc, argv); }