Add file-driven image import automation

This commit is contained in:
2026-06-02 10:04:48 +02:00
parent 3701fd2a71
commit b0445382dd
5 changed files with 196 additions and 3 deletions

View File

@@ -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.

View File

@@ -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

View File

@@ -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=$<TARGET_FILE:pano_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

View File

@@ -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()

View File

@@ -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<char> chars {
std::istreambuf_iterator<char>(stream),
std::istreambuf_iterator<char>()
};
const auto* data = reinterpret_cast<const std::byte*>(chars.data());
const auto image_result = pp::assets::decode_png_rgba8(std::span<const std::byte>(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);
}