#include "assets/image_format.h" #include "assets/image_metadata.h" #include "assets/ppi_header.h" #include "document/document.h" #include "foundation/parse.h" #include "foundation/result.h" #include "paint/stroke.h" #include "paint/stroke_script.h" #include "ui_core/layout_xml.h" #include #include #include #include #include #include #include #include namespace { struct DocumentArgs { std::uint32_t width = 0; std::uint32_t height = 0; std::uint32_t layers = 1; std::uint32_t frames = 1; std::uint32_t frame_duration_ms = 100; }; struct InspectImageArgs { std::string path; }; struct ParseLayoutArgs { std::string path; }; struct InspectProjectArgs { std::string path; }; struct SimulateStrokeArgs { std::uint32_t x1 = 0; std::uint32_t y1 = 0; std::uint32_t x2 = 0; std::uint32_t y2 = 0; std::uint32_t spacing = 1; }; struct SimulateStrokeScriptArgs { std::string path; }; void print_error(std::string_view command, std::string_view message) { std::cout << "{\"ok\":false,\"command\":\"" << command << "\",\"error\":\"" << message << "\"}\n"; } std::string json_escape(std::string_view value) { constexpr char hex[] = "0123456789abcdef"; std::string escaped; escaped.reserve(value.size()); for (const unsigned char ch : value) { switch (ch) { case '"': escaped += "\\\""; break; case '\\': escaped += "\\\\"; break; case '\b': escaped += "\\b"; break; case '\f': escaped += "\\f"; break; case '\n': escaped += "\\n"; break; case '\r': escaped += "\\r"; break; case '\t': escaped += "\\t"; break; default: if (ch < 0x20U) { escaped += "\\u00"; escaped.push_back(hex[(ch >> 4U) & 0x0fU]); escaped.push_back(hex[ch & 0x0fU]); } else { escaped.push_back(static_cast(ch)); } break; } } return escaped; } void print_help() { std::cout << "pano_cli commands:\n" << " create-document --width N --height N [--layers N] [--frames N] [--frame-duration-ms N]\n" << " inspect-image --path FILE\n" << " inspect-project --path FILE\n" << " parse-layout --path FILE\n" << " simulate-stroke --x1 N --y1 N --x2 N --y2 N [--spacing N]\n" << " simulate-stroke-script --path FILE\n" << " --help\n"; } pp::foundation::Status parse_document_args(int argc, char** argv, DocumentArgs& args) { for (int i = 2; i < argc; ++i) { const std::string_view key(argv[i]); 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"); } const auto value = pp::foundation::parse_u32(argv[++i]); if (!value) { return value.status(); } if (key == "--width") { 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 { args.frame_duration_ms = value.value(); } } else { return pp::foundation::Status::invalid_argument("unknown option"); } } if (args.width == 0 || args.height == 0) { return pp::foundation::Status::invalid_argument("width and height must be greater than zero"); } 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"); } if (args.frame_duration_ms == 0) { return pp::foundation::Status::invalid_argument("frame duration must be greater than zero"); } return pp::foundation::Status::success(); } int create_document(int argc, char** argv) { DocumentArgs args; const auto status = parse_document_args(argc, argv, args); if (!status.ok()) { print_error("create-document", status.message); return 2; } const auto document_result = pp::document::CanvasDocument::create( pp::document::DocumentConfig { .width = args.width, .height = args.height, .layer_count = args.layers, }); if (!document_result) { print_error("create-document", document_result.status().message); return 2; } auto document = document_result.value(); const auto duration_status = document.set_frame_duration(0, args.frame_duration_ms); if (!duration_status.ok()) { print_error("create-document", duration_status.message); return 2; } for (std::uint32_t i = 1; i < args.frames; ++i) { const auto added_frame = document.add_frame(args.frame_duration_ms); if (!added_frame) { print_error("create-document", added_frame.status().message); return 2; } } std::cout << "{\"ok\":true,\"command\":\"create-document\",\"document\":{" << "\"width\":" << document.width() << ",\"height\":" << document.height() << ",\"layers\":" << document.layers().size() << ",\"activeLayer\":" << document.active_layer_index() << ",\"frames\":" << document.frames().size() << ",\"activeFrame\":" << document.active_frame_index() << ",\"animationDurationMs\":" << document.animation_duration_ms() << "}}\n"; return 0; } pp::foundation::Status parse_inspect_image_args(int argc, char** argv, InspectImageArgs& 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 { return pp::foundation::Status::invalid_argument("unknown option"); } } if (args.path.empty()) { return pp::foundation::Status::invalid_argument("path must not be empty"); } return pp::foundation::Status::success(); } int inspect_image(int argc, char** argv) { InspectImageArgs args; const auto status = parse_inspect_image_args(argc, argv, args); if (!status.ok()) { print_error("inspect-image", status.message); return 2; } std::ifstream stream(args.path, std::ios::binary); if (!stream) { print_error("inspect-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 format = pp::assets::detect_image_format(std::span(data, chars.size())); if (!format) { print_error("inspect-image", format.status().message); return 2; } pp::foundation::Result metadata = pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("image metadata is unavailable")); if (format.value() == pp::assets::ImageFormat::png) { metadata = pp::assets::parse_png_metadata(std::span(data, chars.size())); if (!metadata) { print_error("inspect-image", metadata.status().message); return 2; } } std::cout << "{\"ok\":true,\"command\":\"inspect-image\",\"format\":\"" << pp::assets::image_format_name(format.value()) << "\",\"bytes\":" << chars.size(); if (metadata) { std::cout << ",\"metadata\":{\"width\":" << metadata.value().width << ",\"height\":" << metadata.value().height << ",\"bitDepth\":" << static_cast(metadata.value().bit_depth) << ",\"components\":" << static_cast(metadata.value().components) << ",\"colorType\":\"" << pp::assets::image_color_type_name(metadata.value().color_type) << "\"}"; } else { std::cout << ",\"metadata\":null"; } std::cout << "}\n"; return 0; } pp::foundation::Status parse_inspect_project_args(int argc, char** argv, InspectProjectArgs& 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 { return pp::foundation::Status::invalid_argument("unknown option"); } } if (args.path.empty()) { return pp::foundation::Status::invalid_argument("path must not be empty"); } return pp::foundation::Status::success(); } int inspect_project(int argc, char** argv) { InspectProjectArgs args; const auto status = parse_inspect_project_args(argc, argv, args); if (!status.ok()) { print_error("inspect-project", status.message); return 2; } std::ifstream stream(args.path, std::ios::binary); if (!stream) { print_error("inspect-project", "project 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 project = pp::assets::parse_ppi_project_index(std::span(data, chars.size())); if (!project) { print_error("inspect-project", project.status().message); return 2; } std::cout << "{\"ok\":true,\"command\":\"inspect-project\"" << ",\"documentVersion\":\"" << project.value().layout.header.document_version.major << "." << project.value().layout.header.document_version.minor << "\"" << ",\"softwareVersion\":\"" << project.value().layout.header.software_version.major << "." << project.value().layout.header.software_version.minor << "." << project.value().layout.header.software_version.fix << "." << project.value().layout.header.software_version.build << "\"" << ",\"thumbnail\":{\"width\":" << project.value().layout.header.thumbnail.width << ",\"height\":" << project.value().layout.header.thumbnail.height << ",\"components\":" << project.value().layout.header.thumbnail.components << ",\"bytes\":" << project.value().layout.thumbnail_bytes << "},\"body\":{\"offset\":" << project.value().layout.body_offset << ",\"bytes\":" << project.value().layout.body_bytes << ",\"width\":" << project.value().body.summary.width << ",\"height\":" << project.value().body.summary.height << ",\"layers\":" << project.value().body.summary.layer_count << ",\"frames\":" << project.value().body.summary.declared_frame_count << ",\"dirtyFaces\":" << project.value().body.summary.dirty_face_count << ",\"rgbaFacePayloads\":" << project.value().body.summary.rgba_face_payload_count << ",\"compressedBytes\":" << project.value().body.summary.compressed_face_bytes << ",\"infoBytes\":" << project.value().body.summary.info_bytes << "},\"layers\":["; for (std::size_t layer_index = 0; layer_index < project.value().body.layers.size(); ++layer_index) { const auto& layer = project.value().body.layers[layer_index]; if (layer_index != 0U) { std::cout << ","; } std::cout << "{\"index\":" << layer_index << ",\"storedOrder\":" << layer.stored_order << ",\"name\":\"" << json_escape(layer.name) << "\"" << ",\"opacity\":" << layer.opacity << ",\"blendMode\":" << layer.blend_mode << ",\"alphaLocked\":" << (layer.alpha_locked ? "true" : "false") << ",\"visible\":" << (layer.visible ? "true" : "false") << ",\"frames\":["; for (std::size_t frame_index = 0; frame_index < layer.frames.size(); ++frame_index) { const auto& frame = layer.frames[frame_index]; if (frame_index != 0U) { std::cout << ","; } std::cout << "{\"index\":" << frame_index << ",\"durationMs\":" << frame.duration_ms << ",\"dirtyFaces\":["; bool first_face = true; for (std::size_t face_index = 0; face_index < frame.faces.size(); ++face_index) { const auto& face = frame.faces[face_index]; if (!face.has_data) { continue; } if (!first_face) { std::cout << ","; } first_face = false; std::cout << "{\"face\":" << face_index << ",\"box\":[" << face.x0 << "," << face.y0 << "," << face.x1 << "," << face.y1 << "]" << ",\"bodyPayloadOffset\":" << face.body_payload_offset << ",\"bytes\":" << face.payload_bytes << ",\"png\":{\"width\":" << face.png_width << ",\"height\":" << face.png_height << "}}"; } std::cout << "]}"; } std::cout << "]}"; } std::cout << "]}\n"; return 0; } pp::foundation::Status parse_simulate_stroke_args(int argc, char** argv, SimulateStrokeArgs& args) { for (int i = 2; i < argc; ++i) { const std::string_view key(argv[i]); if (key == "--x1" || key == "--y1" || key == "--x2" || key == "--y2" || key == "--spacing") { 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 == "--x1") { args.x1 = value.value(); } else if (key == "--y1") { args.y1 = value.value(); } else if (key == "--x2") { args.x2 = value.value(); } else if (key == "--y2") { args.y2 = value.value(); } else { args.spacing = value.value(); } } else { return pp::foundation::Status::invalid_argument("unknown option"); } } if (args.spacing == 0) { return pp::foundation::Status::invalid_argument("stroke spacing must be greater than zero"); } return pp::foundation::Status::success(); } int simulate_stroke(int argc, char** argv) { SimulateStrokeArgs args; const auto status = parse_simulate_stroke_args(argc, argv, args); if (!status.ok()) { print_error("simulate-stroke", status.message); return 2; } const pp::paint::StrokePoint points[] { pp::paint::StrokePoint { .x = static_cast(args.x1), .y = static_cast(args.y1), .pressure = 1.0F, }, pp::paint::StrokePoint { .x = static_cast(args.x2), .y = static_cast(args.y2), .pressure = 1.0F, }, }; const auto samples = pp::paint::sample_stroke( points, pp::paint::StrokeSamplingConfig { .spacing = static_cast(args.spacing), }); if (!samples) { print_error("simulate-stroke", samples.status().message); return 2; } const auto& first = samples.value().front(); const auto& last = samples.value().back(); std::cout << "{\"ok\":true,\"command\":\"simulate-stroke\"" << ",\"samples\":" << samples.value().size() << ",\"first\":{\"x\":" << first.x << ",\"y\":" << first.y << "}" << ",\"last\":{\"x\":" << last.x << ",\"y\":" << last.y << ",\"distance\":" << last.distance << "}}\n"; return 0; } pp::foundation::Status parse_simulate_stroke_script_args(int argc, char** argv, SimulateStrokeScriptArgs& 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 { return pp::foundation::Status::invalid_argument("unknown option"); } } if (args.path.empty()) { return pp::foundation::Status::invalid_argument("path must not be empty"); } return pp::foundation::Status::success(); } int simulate_stroke_script(int argc, char** argv) { SimulateStrokeScriptArgs args; const auto status = parse_simulate_stroke_script_args(argc, argv, args); if (!status.ok()) { print_error("simulate-stroke-script", status.message); return 2; } std::ifstream stream(args.path, std::ios::binary); if (!stream) { print_error("simulate-stroke-script", "stroke script file could not be opened"); return 2; } const std::string text { std::istreambuf_iterator(stream), std::istreambuf_iterator() }; const auto script = pp::paint::parse_stroke_script(text); if (!script) { print_error("simulate-stroke-script", script.status().message); return 2; } std::size_t total_samples = 0; float total_distance = 0.0F; for (const auto& stroke : script.value().strokes) { const pp::paint::StrokePoint points[] { stroke.start, stroke.end }; const auto samples = pp::paint::sample_stroke( points, pp::paint::StrokeSamplingConfig { .spacing = stroke.spacing, }); if (!samples) { print_error("simulate-stroke-script", samples.status().message); return 2; } total_samples += samples.value().size(); total_distance += samples.value().back().distance; } std::cout << "{\"ok\":true,\"command\":\"simulate-stroke-script\"" << ",\"strokes\":" << script.value().strokes.size() << ",\"samples\":" << total_samples << ",\"distance\":" << total_distance << "}\n"; return 0; } pp::foundation::Status parse_layout_args(int argc, char** argv, ParseLayoutArgs& 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 { return pp::foundation::Status::invalid_argument("unknown option"); } } if (args.path.empty()) { return pp::foundation::Status::invalid_argument("path must not be empty"); } return pp::foundation::Status::success(); } int parse_layout(int argc, char** argv) { ParseLayoutArgs args; const auto status = parse_layout_args(argc, argv, args); if (!status.ok()) { print_error("parse-layout", status.message); return 2; } std::ifstream stream(args.path, std::ios::binary); if (!stream) { print_error("parse-layout", "layout file could not be opened"); return 2; } const std::string xml { std::istreambuf_iterator(stream), std::istreambuf_iterator() }; const auto summary = pp::ui::parse_layout_xml(xml); if (!summary) { print_error("parse-layout", summary.status().message); return 2; } std::cout << "{\"ok\":true,\"command\":\"parse-layout\"" << ",\"nodes\":" << summary.value().node_count << ",\"lengthAttributes\":" << summary.value().length_attribute_count << "}\n"; return 0; } } int main(int argc, char** argv) { if (argc < 2) { print_help(); return 1; } const std::string_view command(argv[1]); if (command == "--help" || command == "-h") { print_help(); return 0; } if (command == "create-document") { return create_document(argc, argv); } if (command == "inspect-image") { return inspect_image(argc, argv); } if (command == "inspect-project") { return inspect_project(argc, argv); } if (command == "simulate-stroke") { return simulate_stroke(argc, argv); } if (command == "simulate-stroke-script") { return simulate_stroke_script(argc, argv); } if (command == "parse-layout") { return parse_layout(argc, argv); } print_error(command, "unknown command"); return 2; }