Add file-driven image import automation
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
28
tests/cmake/expect_pano_cli_import_image_failure.cmake
Normal file
28
tests/cmake/expect_pano_cli_import_image_failure.cmake
Normal 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()
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user