Export layer collections through paint renderer

This commit is contained in:
2026-06-05 20:48:16 +02:00
parent 77268a28fb
commit 3c36be4b43
10 changed files with 815 additions and 21 deletions

View File

@@ -41,6 +41,11 @@ struct DocumentCubeFaceExportPayload {
std::span<const std::byte> bytes;
};
struct DocumentExportCollectionPngPayload {
std::string path_suffix;
std::span<const std::byte> bytes;
};
struct DocumentExportSuggestedName {
std::string name;
};
@@ -190,6 +195,16 @@ public:
virtual void publish_exported_image(std::string_view path) = 0;
};
class DocumentExportCollectionWriteServices {
public:
virtual ~DocumentExportCollectionWriteServices() = default;
virtual pp::foundation::Status write_binary_file(
std::string_view path,
std::span<const std::byte> bytes) = 0;
virtual void publish_exported_image(std::string_view path) = 0;
};
[[nodiscard]] constexpr DocumentExportStartDecision plan_document_export_start(
bool requires_license,
bool license_valid,
@@ -608,6 +623,41 @@ document_cube_face_export_names() noexcept
return pp::foundation::Result<DocumentCubeFaceExportTarget>::success(std::move(target));
}
[[nodiscard]] inline std::string document_export_two_digit_index(std::size_t index)
{
auto value = std::to_string(index);
if (value.size() < 2U) {
value.insert(value.begin(), '0');
}
return value;
}
[[nodiscard]] inline std::string make_document_layer_export_path_suffix(
std::size_t layer_index,
std::string_view layer_name)
{
std::string suffix;
const auto index = document_export_two_digit_index(layer_index);
suffix.reserve(10U + index.size() + layer_name.size());
suffix += "-layer";
suffix += index;
suffix += "-";
suffix += layer_name;
suffix += ".png";
return suffix;
}
[[nodiscard]] inline std::string make_document_animation_frame_export_path_suffix(std::size_t frame_index)
{
std::string suffix;
const auto index = document_export_two_digit_index(frame_index);
suffix.reserve(5U + index.size());
suffix += "-";
suffix += index;
suffix += ".png";
return suffix;
}
[[nodiscard]] inline pp::foundation::Result<DocumentExportSuggestedName> make_document_export_suggested_name(
std::string_view document_name,
std::string_view suffix)
@@ -656,6 +706,41 @@ document_cube_face_export_names() noexcept
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Status execute_document_export_collection_write(
const DocumentExportCollectionTarget& target,
std::span<const DocumentExportCollectionPngPayload> payloads,
DocumentExportCollectionWriteServices& services)
{
if (target.stem_path.empty()) {
return pp::foundation::Status::invalid_argument("export collection target requires a stem path");
}
if (payloads.empty()) {
return pp::foundation::Status::invalid_argument("export collection payloads must not be empty");
}
for (const auto& payload : payloads) {
if (payload.path_suffix.empty()) {
return pp::foundation::Status::invalid_argument("export collection payload suffix must not be empty");
}
if (payload.bytes.empty()) {
return pp::foundation::Status::invalid_argument("export collection payload must not be empty");
}
std::string path;
path.reserve(target.stem_path.size() + payload.path_suffix.size());
path += target.stem_path;
path += payload.path_suffix;
const auto write_status = services.write_binary_file(path, payload.bytes);
if (!write_status.ok()) {
return write_status;
}
services.publish_exported_image(path);
}
return pp::foundation::Status::success();
}
[[nodiscard]] inline pp::foundation::Status execute_document_export_file(
const DocumentExportFileTarget& target,
DocumentExportServices& services)

View File

@@ -65,9 +65,11 @@ pp::foundation::Status write_export_binary_file(std::string_view path, std::span
return pp::foundation::Status::success();
}
class LegacyCubeFaceExportWriteServices final : public pp::app::DocumentCubeFaceExportWriteServices {
class LegacyExportWriteServices final
: public pp::app::DocumentCubeFaceExportWriteServices
, public pp::app::DocumentExportCollectionWriteServices {
public:
explicit LegacyCubeFaceExportWriteServices(App& app) noexcept
explicit LegacyExportWriteServices(App& app) noexcept
: app_(app)
{
}
@@ -183,10 +185,90 @@ pp::foundation::Status export_cube_faces_from_document_snapshot(
payloads[face_index].bytes = std::span<const std::byte>(reports.face_pngs.face_pngs[face_index]);
}
LegacyCubeFaceExportWriteServices services(app);
LegacyExportWriteServices services(app);
return pp::app::execute_document_cube_face_export_write(target.value(), payloads, services);
}
pp::foundation::Status export_layers_from_document_snapshot(
App& app,
const pp::app::DocumentExportCollectionTarget& target,
const LegacyDocumentExportSnapshotReports& reports)
{
const auto report = pp::app::make_document_canvas_save_snapshot_report(reports.snapshot);
if (!report.payload_complete) {
return pp::foundation::Status::invalid_argument(
"document snapshot layer export still requires renderer payload readback");
}
auto exported = pp::paint_renderer::export_document_layers_equirectangular_pngs(
pp::paint_renderer::DocumentLayerEquirectangularPngExportRequest {
.document = &reports.snapshot.document,
.frame_index = reports.snapshot.document.active_frame_index(),
.clear_color = {},
});
if (!exported) {
return exported.status();
}
auto exported_value = std::move(exported.value());
LOG(
"export-layers document export PNG writer: layers=%zu bytes=%llu activeFrame=%zu",
exported_value.layer_count,
static_cast<unsigned long long>(exported_value.encoded_bytes),
reports.snapshot.document.active_frame_index());
std::vector<pp::app::DocumentExportCollectionPngPayload> payloads;
payloads.reserve(exported_value.layers.size());
for (const auto& layer : exported_value.layers) {
payloads.push_back(pp::app::DocumentExportCollectionPngPayload {
.path_suffix = pp::app::make_document_layer_export_path_suffix(layer.layer_index, layer.layer_name),
.bytes = std::span<const std::byte>(layer.png.data(), layer.png.size()),
});
}
LegacyExportWriteServices services(app);
return pp::app::execute_document_export_collection_write(target, payloads, services);
}
pp::foundation::Status export_animation_frames_from_document_snapshot(
App& app,
const pp::app::DocumentExportCollectionTarget& target,
const LegacyDocumentExportSnapshotReports& reports)
{
const auto report = pp::app::make_document_canvas_save_snapshot_report(reports.snapshot);
if (!report.payload_complete) {
return pp::foundation::Status::invalid_argument(
"document snapshot animation-frame export still requires renderer payload readback");
}
auto exported = pp::paint_renderer::export_document_animation_frames_equirectangular_pngs(
pp::paint_renderer::DocumentAnimationFrameEquirectangularPngExportRequest {
.document = &reports.snapshot.document,
.clear_color = {},
});
if (!exported) {
return exported.status();
}
auto exported_value = std::move(exported.value());
LOG(
"export-animation-frames document export PNG writer: frames=%zu bytes=%llu",
exported_value.frame_count,
static_cast<unsigned long long>(exported_value.encoded_bytes));
std::vector<pp::app::DocumentExportCollectionPngPayload> payloads;
payloads.reserve(exported_value.frames.size());
for (const auto& frame : exported_value.frames) {
payloads.push_back(pp::app::DocumentExportCollectionPngPayload {
.path_suffix = pp::app::make_document_animation_frame_export_path_suffix(frame.frame_index),
.bytes = std::span<const std::byte>(frame.png.data(), frame.png.size()),
});
}
LegacyExportWriteServices services(app);
return pp::app::execute_document_export_collection_write(target, payloads, services);
}
pp::foundation::Status export_equirectangular_png_from_document_snapshot(
App& app,
const pp::app::DocumentExportFileTarget& target,
@@ -284,7 +366,32 @@ public:
void export_layers_to_stem(const pp::app::DocumentExportStemTarget& target) override
{
auto* app = &app_;
#if !__WEB__
const auto prepared = prepare_legacy_document_export_snapshot(app_, "export-layers");
if (prepared) {
const auto collection_target = pp::app::DocumentExportCollectionTarget {
.stem_path = target.stem_path,
};
const auto exported = export_layers_from_document_snapshot(app_, collection_target, prepared.value());
if (exported.ok()) {
show_export_success_dialog(
app_,
pp::app::plan_document_export_success_dialog(
pp::app::DocumentExportSuccessKind::layers,
pp::app::DocumentExportSuccessDestination::path,
target.stem_path));
return;
}
LOG("export-layers document export writer retained legacy export after failure: %s", exported.message);
} else {
LOG(
"export-layers document export snapshot bridge retained legacy export after failure: %s",
prepared.status().message);
}
#else
prepare_legacy_document_export_snapshot_or_continue(app_, "export-layers");
#endif
app_.canvas->m_canvas->export_layers(target.stem_path, [app, target] {
show_export_success_dialog(
*app,
@@ -298,7 +405,28 @@ public:
void export_layers_to_collection(const pp::app::DocumentExportCollectionTarget& target) override
{
auto* app = &app_;
#if !__WEB__
const auto prepared = prepare_legacy_document_export_snapshot(app_, "export-layers");
if (prepared) {
const auto exported = export_layers_from_document_snapshot(app_, target, prepared.value());
if (exported.ok()) {
show_export_success_dialog(
app_,
pp::app::plan_document_export_success_dialog(
pp::app::DocumentExportSuccessKind::layers,
pp::app::DocumentExportSuccessDestination::files_panopainter));
return;
}
LOG("export-layers document export writer retained legacy export after failure: %s", exported.message);
} else {
LOG(
"export-layers document export snapshot bridge retained legacy export after failure: %s",
prepared.status().message);
}
#else
prepare_legacy_document_export_snapshot_or_continue(app_, "export-layers");
#endif
app_.canvas->m_canvas->export_layers(target.stem_path, [app] {
show_export_success_dialog(
*app,
@@ -311,7 +439,37 @@ public:
void export_animation_frames_to_stem(const pp::app::DocumentExportStemTarget& target) override
{
auto* app = &app_;
#if !__WEB__
const auto prepared = prepare_legacy_document_export_snapshot(app_, "export-animation-frames");
if (prepared) {
const auto collection_target = pp::app::DocumentExportCollectionTarget {
.stem_path = target.stem_path,
};
const auto exported = export_animation_frames_from_document_snapshot(
app_,
collection_target,
prepared.value());
if (exported.ok()) {
show_export_success_dialog(
app_,
pp::app::plan_document_export_success_dialog(
pp::app::DocumentExportSuccessKind::animation_frames,
pp::app::DocumentExportSuccessDestination::path,
target.stem_path));
return;
}
LOG(
"export-animation-frames document export writer retained legacy export after failure: %s",
exported.message);
} else {
LOG(
"export-animation-frames document export snapshot bridge retained legacy export after failure: %s",
prepared.status().message);
}
#else
prepare_legacy_document_export_snapshot_or_continue(app_, "export-animation-frames");
#endif
app_.canvas->m_canvas->export_anim_frames(target.stem_path, [app, target] {
show_export_success_dialog(
*app,
@@ -325,7 +483,30 @@ public:
void export_animation_frames_to_collection(const pp::app::DocumentExportCollectionTarget& target) override
{
auto* app = &app_;
#if !__WEB__
const auto prepared = prepare_legacy_document_export_snapshot(app_, "export-animation-frames");
if (prepared) {
const auto exported = export_animation_frames_from_document_snapshot(app_, target, prepared.value());
if (exported.ok()) {
show_export_success_dialog(
app_,
pp::app::plan_document_export_success_dialog(
pp::app::DocumentExportSuccessKind::animation_frames,
pp::app::DocumentExportSuccessDestination::files_panopainter));
return;
}
LOG(
"export-animation-frames document export writer retained legacy export after failure: %s",
exported.message);
} else {
LOG(
"export-animation-frames document export snapshot bridge retained legacy export after failure: %s",
prepared.status().message);
}
#else
prepare_legacy_document_export_snapshot_or_continue(app_, "export-animation-frames");
#endif
app_.canvas->m_canvas->export_anim_frames(target.stem_path, [app] {
show_export_success_dialog(
*app,

View File

@@ -408,6 +408,77 @@ pp::foundation::Result<DocumentFrameCompositeResult> composite_document_frame(
return pp::foundation::Result<DocumentFrameCompositeResult>::success(std::move(result));
}
pp::foundation::Result<DocumentFrameCompositeResult> composite_document_layer_frame(
const pp::document::CanvasDocument& document,
std::size_t layer_index,
std::size_t frame_index,
pp::paint::Rgba clear_color)
{
if (layer_index >= document.layers().size()) {
return pp::foundation::Result<DocumentFrameCompositeResult>::failure(
pp::foundation::Status::out_of_range("document layer export index is outside the document"));
}
if (frame_index >= document.frames().size()) {
return pp::foundation::Result<DocumentFrameCompositeResult>::failure(
pp::foundation::Status::out_of_range("document layer export frame index is outside the document"));
}
const pp::renderer::Extent2D extent {
.width = document.width(),
.height = document.height(),
};
const auto pixel_count = expected_pixel_count(extent);
if (!pixel_count) {
return pp::foundation::Result<DocumentFrameCompositeResult>::failure(pixel_count.status());
}
const auto& source_layer = document.layers()[layer_index];
auto export_layer = source_layer;
export_layer.visible = true;
export_layer.opacity = 1.0F;
export_layer.blend_mode = pp::paint::BlendMode::normal;
DocumentFrameCompositeResult result;
result.extent = extent;
result.visited_layer_count = 1U;
for (std::uint32_t face_index = 0; face_index < pp::document::cube_face_count; ++face_index) {
DocumentFaceCompositeResult face;
face.extent = extent;
face.pixels.assign(pixel_count.value(), clear_color);
face.visited_layer_count = 1U;
if (frame_index < source_layer.frames.size()) {
bool composited_face = false;
const auto& frame = source_layer.frames[frame_index];
for (const auto& payload : frame.face_pixels) {
if (payload.face_index != face_index) {
continue;
}
const auto status = composite_face_payload(face.pixels, extent, payload, export_layer);
if (!status.ok()) {
return pp::foundation::Result<DocumentFrameCompositeResult>::failure(status);
}
composited_face = true;
++face.face_payload_count;
++result.face_payload_count;
}
if (composited_face) {
face.composited_layer_count = 1U;
++result.composited_layer_face_count;
}
}
result.faces[face_index] = std::move(face);
}
return pp::foundation::Result<DocumentFrameCompositeResult>::success(std::move(result));
}
pp::foundation::Result<DocumentFrameUploadResult> upload_document_frame_faces(
pp::renderer::IRenderDevice& device,
DocumentFrameUploadRequest request)
@@ -612,6 +683,91 @@ export_document_frame_equirectangular_png(DocumentFrameCompositeRequest request)
return export_document_frame_equirectangular_png(composite.value());
}
pp::foundation::Result<DocumentLayerEquirectangularPngExportResult>
export_document_layers_equirectangular_pngs(DocumentLayerEquirectangularPngExportRequest request)
{
if (request.document == nullptr) {
return pp::foundation::Result<DocumentLayerEquirectangularPngExportResult>::failure(
pp::foundation::Status::invalid_argument("document layer export request requires a document"));
}
if (request.frame_index >= request.document->frames().size()) {
return pp::foundation::Result<DocumentLayerEquirectangularPngExportResult>::failure(
pp::foundation::Status::out_of_range("document layer export frame index is outside the document"));
}
DocumentLayerEquirectangularPngExportResult result;
result.layers.reserve(request.document->layers().size());
for (std::size_t layer_index = 0; layer_index < request.document->layers().size(); ++layer_index) {
auto composite = composite_document_layer_frame(
*request.document,
layer_index,
request.frame_index,
request.clear_color);
if (!composite) {
return pp::foundation::Result<DocumentLayerEquirectangularPngExportResult>::failure(composite.status());
}
auto exported = export_document_frame_equirectangular_png(composite.value());
if (!exported) {
return pp::foundation::Result<DocumentLayerEquirectangularPngExportResult>::failure(exported.status());
}
DocumentLayerEquirectangularPng layer;
layer.layer_index = layer_index;
layer.layer_name = request.document->layers()[layer_index].name;
layer.face_extent = exported.value().face_extent;
layer.equirectangular_extent = exported.value().equirectangular_extent;
layer.encoded_bytes = exported.value().encoded_bytes;
layer.face_payload_count = exported.value().face_payload_count;
layer.composited_layer_face_count = exported.value().composited_layer_face_count;
layer.png = std::move(exported.value().png);
result.encoded_bytes += layer.encoded_bytes;
result.layers.push_back(std::move(layer));
}
result.layer_count = result.layers.size();
return pp::foundation::Result<DocumentLayerEquirectangularPngExportResult>::success(std::move(result));
}
pp::foundation::Result<DocumentAnimationFrameEquirectangularPngExportResult>
export_document_animation_frames_equirectangular_pngs(
DocumentAnimationFrameEquirectangularPngExportRequest request)
{
if (request.document == nullptr) {
return pp::foundation::Result<DocumentAnimationFrameEquirectangularPngExportResult>::failure(
pp::foundation::Status::invalid_argument("document animation-frame export request requires a document"));
}
DocumentAnimationFrameEquirectangularPngExportResult result;
result.frames.reserve(request.document->frames().size());
for (std::size_t frame_index = 0; frame_index < request.document->frames().size(); ++frame_index) {
auto exported = export_document_frame_equirectangular_png(DocumentFrameCompositeRequest {
.document = request.document,
.frame_index = frame_index,
.clear_color = request.clear_color,
});
if (!exported) {
return pp::foundation::Result<DocumentAnimationFrameEquirectangularPngExportResult>::failure(
exported.status());
}
DocumentAnimationFrameEquirectangularPng frame;
frame.frame_index = frame_index;
frame.face_extent = exported.value().face_extent;
frame.equirectangular_extent = exported.value().equirectangular_extent;
frame.encoded_bytes = exported.value().encoded_bytes;
frame.face_payload_count = exported.value().face_payload_count;
frame.composited_layer_face_count = exported.value().composited_layer_face_count;
frame.png = std::move(exported.value().png);
result.encoded_bytes += frame.encoded_bytes;
result.frames.push_back(std::move(frame));
}
result.frame_count = result.frames.size();
return pp::foundation::Result<DocumentAnimationFrameEquirectangularPngExportResult>::success(std::move(result));
}
pp::foundation::Result<DocumentFrameExportReadinessResult> prepare_document_frame_export_readiness(
DocumentFrameCompositeRequest request)
{

View File

@@ -10,6 +10,7 @@
#include <cstdint>
#include <memory>
#include <span>
#include <string>
#include <vector>
namespace pp::paint_renderer {
@@ -155,6 +156,50 @@ struct DocumentFrameEquirectangularPngExportResult {
std::size_t composited_layer_face_count = 0;
};
struct DocumentLayerEquirectangularPngExportRequest {
const pp::document::CanvasDocument* document = nullptr;
std::size_t frame_index = 0;
pp::paint::Rgba clear_color {};
};
struct DocumentLayerEquirectangularPng {
std::size_t layer_index = 0;
std::string layer_name;
pp::renderer::Extent2D face_extent {};
pp::renderer::Extent2D equirectangular_extent {};
std::vector<std::byte> png;
std::uint64_t encoded_bytes = 0;
std::size_t face_payload_count = 0;
std::size_t composited_layer_face_count = 0;
};
struct DocumentLayerEquirectangularPngExportResult {
std::vector<DocumentLayerEquirectangularPng> layers;
std::uint64_t encoded_bytes = 0;
std::size_t layer_count = 0;
};
struct DocumentAnimationFrameEquirectangularPngExportRequest {
const pp::document::CanvasDocument* document = nullptr;
pp::paint::Rgba clear_color {};
};
struct DocumentAnimationFrameEquirectangularPng {
std::size_t frame_index = 0;
pp::renderer::Extent2D face_extent {};
pp::renderer::Extent2D equirectangular_extent {};
std::vector<std::byte> png;
std::uint64_t encoded_bytes = 0;
std::size_t face_payload_count = 0;
std::size_t composited_layer_face_count = 0;
};
struct DocumentAnimationFrameEquirectangularPngExportResult {
std::vector<DocumentAnimationFrameEquirectangularPng> frames;
std::uint64_t encoded_bytes = 0;
std::size_t frame_count = 0;
};
struct DocumentFrameExportReadinessResult {
RecordedDocumentFrameUploadResult recorded_upload {};
DocumentFrameFacePngExportResult face_pngs {};
@@ -187,6 +232,13 @@ export_document_frame_equirectangular_png(const DocumentFrameCompositeResult& co
[[nodiscard]] pp::foundation::Result<DocumentFrameEquirectangularPngExportResult>
export_document_frame_equirectangular_png(DocumentFrameCompositeRequest request);
[[nodiscard]] pp::foundation::Result<DocumentLayerEquirectangularPngExportResult>
export_document_layers_equirectangular_pngs(DocumentLayerEquirectangularPngExportRequest request);
[[nodiscard]] pp::foundation::Result<DocumentAnimationFrameEquirectangularPngExportResult>
export_document_animation_frames_equirectangular_pngs(
DocumentAnimationFrameEquirectangularPngExportRequest request);
[[nodiscard]] pp::foundation::Result<DocumentFrameExportReadinessResult> prepare_document_frame_export_readiness(
DocumentFrameCompositeRequest request);