Add document export CLI automation

This commit is contained in:
2026-06-02 11:18:19 +02:00
parent 7b14c356db
commit 99b2eeb99d
5 changed files with 224 additions and 2 deletions

View File

@@ -3,6 +3,7 @@
#include "assets/image_pixels.h"
#include "assets/ppi_header.h"
#include "document/document.h"
#include "document/ppi_export.h"
#include "document/ppi_import.h"
#include "foundation/parse.h"
#include "foundation/result.h"
@@ -99,6 +100,11 @@ struct SimulateImageImportArgs {
std::uint32_t height = 32;
};
struct SimulateDocumentExportArgs {
std::uint32_t width = 64;
std::uint32_t height = 32;
};
struct SimulateDocumentHistoryArgs {
std::uint32_t width = 64;
std::uint32_t height = 32;
@@ -210,6 +216,7 @@ void print_help()
<< " record-render [--width N] [--height N]\n"
<< " save-project --path FILE --width N --height N [--layer-name NAME] [--layer-opacity N] [--blend-mode N] [--alpha-locked] [--hidden] [--layers N] [--frames N] [--frame-duration-ms N] [--include-test-face-payload] [--payload-layer N] [--payload-frame N]\n"
<< " simulate-document-edits [--width N] [--height N]\n"
<< " simulate-document-export [--width N] [--height N]\n"
<< " simulate-document-history [--width N] [--height N] [--history N]\n"
<< " simulate-image-import [--width N] [--height N]\n"
<< " simulate-stroke --x1 N --y1 N --x2 N --y2 N [--spacing N]\n"
@@ -1464,6 +1471,196 @@ int simulate_image_import(int argc, char** argv)
return 0;
}
pp::foundation::Status parse_simulate_document_export_args(
int argc,
char** argv,
SimulateDocumentExportArgs& args)
{
for (int i = 2; i < argc; ++i) {
const std::string_view key(argv[i]);
if (key == "--width" || key == "--height") {
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 {
args.height = 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.width < 8 || args.height < 8) {
return pp::foundation::Status::out_of_range("width and height must be at least 8 for export simulation");
}
return pp::foundation::Status::success();
}
int simulate_document_export(int argc, char** argv)
{
SimulateDocumentExportArgs args;
const auto status = parse_simulate_document_export_args(argc, argv, args);
if (!status.ok()) {
print_error("simulate-document-export", status.message);
return 2;
}
const std::vector<std::uint8_t> red_pixel { 255, 0, 0, 255 };
const std::vector<std::uint8_t> blue_pixel { 0, 0, 255, 128 };
const pp::document::AnimationFrame root_frames[] {
{ .duration_ms = 100, .face_pixels = {} },
{ .duration_ms = 250, .face_pixels = {} },
};
const pp::document::AnimationFrame base_frames[] {
{
.duration_ms = 100,
.face_pixels = {
pp::document::LayerFacePixels {
.face_index = 0,
.x = 2,
.y = 3,
.width = 1,
.height = 1,
.rgba8 = red_pixel,
},
},
},
{ .duration_ms = 250, .face_pixels = {} },
};
const pp::document::AnimationFrame paint_frames[] {
{
.duration_ms = 333,
.face_pixels = {
pp::document::LayerFacePixels {
.face_index = 5,
.x = 4,
.y = 5,
.width = 1,
.height = 1,
.rgba8 = blue_pixel,
},
},
},
};
const pp::document::DocumentLayerConfig layers[] {
{
.name = "Base",
.visible = true,
.alpha_locked = false,
.opacity = 1.0F,
.blend_mode = pp::paint::BlendMode::normal,
.frames = std::span<const pp::document::AnimationFrame>(base_frames, 2),
},
{
.name = "Paint",
.visible = false,
.alpha_locked = true,
.opacity = 0.5F,
.blend_mode = pp::paint::BlendMode::overlay,
.frames = std::span<const pp::document::AnimationFrame>(paint_frames, 1),
},
};
const auto document_result = pp::document::CanvasDocument::create_from_snapshot(
pp::document::DocumentSnapshotConfig {
.width = args.width,
.height = args.height,
.layers = std::span<const pp::document::DocumentLayerConfig>(layers, 2),
.frames = std::span<const pp::document::AnimationFrame>(root_frames, 2),
.selection_masks = {},
});
if (!document_result) {
print_error("simulate-document-export", document_result.status().message);
return 2;
}
const auto exported = pp::document::export_ppi_project_document(document_result.value());
if (!exported) {
print_error("simulate-document-export", exported.status().message);
return 2;
}
const auto decoded = pp::assets::decode_ppi_project_images(exported.value());
if (!decoded) {
print_error("simulate-document-export", decoded.status().message);
return 2;
}
const auto imported = pp::document::import_ppi_project_document(decoded.value());
if (!imported) {
print_error("simulate-document-export", imported.status().message);
return 2;
}
const auto& source = document_result.value();
const auto& roundtrip = imported.value();
std::cout << "{\"ok\":true,\"command\":\"simulate-document-export\""
<< ",\"source\":{\"width\":" << source.width()
<< ",\"height\":" << source.height()
<< ",\"layers\":" << source.layers().size()
<< ",\"frames\":" << source.frames().size()
<< ",\"facePayloads\":" << source.face_pixel_payload_count()
<< "},\"export\":{\"bytes\":" << exported.value().size()
<< ",\"dirtyFaces\":" << decoded.value().project.body.summary.dirty_face_count
<< ",\"rgbaFacePayloads\":" << decoded.value().project.body.summary.rgba_face_payload_count
<< ",\"compressedBytes\":" << decoded.value().project.body.summary.compressed_face_bytes
<< "},\"roundtrip\":{\"layers\":" << roundtrip.layers().size()
<< ",\"frames\":" << roundtrip.frames().size()
<< ",\"facePayloads\":" << roundtrip.face_pixel_payload_count()
<< ",\"layerNames\":[";
for (std::size_t layer_index = 0; layer_index < roundtrip.layers().size(); ++layer_index) {
if (layer_index != 0U) {
std::cout << ",";
}
std::cout << "\"" << json_escape(roundtrip.layers()[layer_index].name) << "\"";
}
std::cout << "],\"layerFrameCounts\":[";
for (std::size_t layer_index = 0; layer_index < roundtrip.layers().size(); ++layer_index) {
if (layer_index != 0U) {
std::cout << ",";
}
std::cout << roundtrip.layers()[layer_index].frames.size();
}
std::cout << "],\"layerDurationsMs\":[";
for (std::size_t layer_index = 0; layer_index < roundtrip.layers().size(); ++layer_index) {
if (layer_index != 0U) {
std::cout << ",";
}
const auto duration = roundtrip.layer_animation_duration_ms(layer_index);
std::cout << (duration ? duration.value() : 0U);
}
std::cout << "]},\"payloads\":[";
for (std::size_t payload_index = 0; payload_index < decoded.value().faces.size(); ++payload_index) {
const auto& payload = decoded.value().faces[payload_index];
if (payload_index != 0U) {
std::cout << ",";
}
std::cout << "{\"layer\":" << payload.layer_index
<< ",\"frame\":" << payload.frame_index
<< ",\"face\":" << payload.face_index
<< ",\"x\":" << payload.descriptor.x0
<< ",\"y\":" << payload.descriptor.y0
<< ",\"bytes\":" << payload.image.pixels.size()
<< ",\"alpha\":" << static_cast<int>(payload.image.pixels[3])
<< "}";
}
std::cout << "]}\n";
return 0;
}
pp::foundation::Status parse_simulate_document_history_args(
int argc,
char** argv,
@@ -1813,6 +2010,10 @@ int main(int argc, char** argv)
return simulate_document_edits(argc, argv);
}
if (command == "simulate-document-export") {
return simulate_document_export(argc, argv);
}
if (command == "simulate-document-history") {
return simulate_document_history(argc, argv);
}