#include "pch.h" #include "legacy_document_export_services.h" #include "app.h" #include "legacy_document_canvas_services.h" #include "paint_renderer/compositor.h" #include #include #include #include #include #include namespace pp::panopainter { namespace { void show_export_success_dialog( App& app, const pp::app::DocumentExportSuccessDialogPlan& plan) { if (plan.show_dialog) { app.message_box(plan.dialog.title, plan.dialog.message, plan.dialog.show_cancel); } } bool is_png_export_target(std::string_view path) noexcept { if (path.size() < 4U) { return false; } const auto extension = path.substr(path.size() - 4U); return extension[0] == '.' && (extension[1] == 'p' || extension[1] == 'P') && (extension[2] == 'n' || extension[2] == 'N') && (extension[3] == 'g' || extension[3] == 'G'); } struct LegacyDocumentExportSnapshotReports { pp::app::DocumentCanvasSnapshotResult snapshot; pp::paint_renderer::DocumentFrameFacePngExportResult face_pngs; }; pp::foundation::Status write_export_binary_file(std::string_view path, std::span bytes) { if (path.empty()) { return pp::foundation::Status::invalid_argument("export path must not be empty"); } if (bytes.size() > static_cast(std::numeric_limits::max())) { return pp::foundation::Status::out_of_range("export payload is too large to write"); } std::ofstream stream(std::string(path), std::ios::binary); if (!stream) { return pp::foundation::Status::invalid_argument("export path could not be opened for writing"); } stream.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); if (!stream) { return pp::foundation::Status::invalid_argument("export payload could not be written"); } return pp::foundation::Status::success(); } class LegacyExportWriteServices final : public pp::app::DocumentCubeFaceExportWriteServices , public pp::app::DocumentDepthExportWriteServices , public pp::app::DocumentExportCollectionWriteServices { public: explicit LegacyExportWriteServices(App& app) noexcept : app_(app) { } pp::foundation::Status write_binary_file( std::string_view path, std::span bytes) override { return write_export_binary_file(path, bytes); } void publish_exported_image(std::string_view path) override { app_.publish_exported_image(std::string(path)); } private: App& app_; }; pp::foundation::Result prepare_legacy_document_export_snapshot( App& app, const char* context) { auto snapshot = capture_legacy_canvas_document_payload_snapshot(app); if (!snapshot) { LOG("%s document export snapshot failed: %s", context, snapshot.status().message); return pp::foundation::Result::failure(snapshot.status()); } const auto report = pp::app::make_document_canvas_save_snapshot_report(snapshot.value()); LOG( "%s document export snapshot: %ux%u layers=%zu frames=%zu capturedFaces=%zu pendingFaces=%zu ppiReady=%s", context, report.width, report.height, report.layer_count, report.frame_count, report.captured_face_payloads, report.pending_face_payloads, report.can_export_ppi ? "true" : "false"); LegacyDocumentExportSnapshotReports reports { .snapshot = std::move(snapshot.value()), }; if (!report.can_export_ppi) { return pp::foundation::Result::success(std::move(reports)); } auto readiness = pp::paint_renderer::prepare_document_frame_export_readiness( pp::paint_renderer::DocumentFrameCompositeRequest { .document = &reports.snapshot.document, .frame_index = reports.snapshot.document.active_frame_index(), .clear_color = {}, }); if (!readiness) { LOG("%s document export renderer readiness failed: %s", context, readiness.status().message); return pp::foundation::Result::failure(readiness.status()); } auto readiness_value = std::move(readiness.value()); const auto& recorded_upload = readiness_value.recorded_upload; const auto& uploaded = recorded_upload.upload; LOG( "%s document export renderer upload: textures=%zu bytes=%llu transitions=%zu facePayloads=%zu commands=%zu uploadCommands=%zu transitionCommands=%zu", context, uploaded.texture_count, static_cast(uploaded.uploaded_bytes), uploaded.transition_count, uploaded.composite.face_payload_count, recorded_upload.command_count, recorded_upload.upload_command_count, recorded_upload.transition_command_count); LOG( "%s document export face PNG export: faces=%zu bytes=%llu facePayloads=%zu compositedLayerFaces=%zu", context, readiness_value.face_pngs.face_count, static_cast(readiness_value.face_pngs.encoded_bytes), readiness_value.face_pngs.composite.face_payload_count, readiness_value.face_pngs.composite.composited_layer_face_count); reports.face_pngs = std::move(readiness_value.face_pngs); return pp::foundation::Result::success(std::move(reports)); } void prepare_legacy_document_export_snapshot_or_continue(App& app, const char* context) { const auto prepared = prepare_legacy_document_export_snapshot(app, context); if (!prepared) { LOG( "%s document export snapshot bridge retained legacy export after failure: %s", context, prepared.status().message); } } pp::foundation::Status export_cube_faces_from_document_snapshot( App& app, std::string_view document_name, const LegacyDocumentExportSnapshotReports& reports) { const auto target = pp::app::make_document_cube_face_export_target(app.work_path, document_name); if (!target) { return target.status(); } if (reports.face_pngs.face_count != pp::document::cube_face_count) { return pp::foundation::Status::invalid_argument("document snapshot did not produce all cube face PNGs"); } std::array payloads; for (std::size_t face_index = 0; face_index < payloads.size(); ++face_index) { payloads[face_index].bytes = std::span(reports.face_pngs.face_pngs[face_index]); } 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(exported_value.encoded_bytes), reports.snapshot.document.active_frame_index()); std::vector 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(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(exported_value.encoded_bytes)); std::vector 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(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, const LegacyDocumentExportSnapshotReports& reports) { if (!is_png_export_target(target.path)) { return pp::foundation::Status::invalid_argument( "document snapshot equirectangular export currently supports PNG targets only"); } auto exported = pp::paint_renderer::export_document_frame_equirectangular_png(reports.face_pngs.composite); if (!exported) { return exported.status(); } auto exported_value = std::move(exported.value()); LOG( "export-equirectangular document export PNG writer: %ux%u bytes=%llu facePayloads=%zu compositedLayerFaces=%zu", exported_value.equirectangular_extent.width, exported_value.equirectangular_extent.height, static_cast(exported_value.encoded_bytes), exported_value.face_payload_count, exported_value.composited_layer_face_count); const auto write_status = write_export_binary_file( target.path, std::span(exported_value.png.data(), exported_value.png.size())); if (!write_status.ok()) { return write_status; } app.publish_exported_image(target.path); return pp::foundation::Status::success(); } class LegacyDocumentExportServices final : public pp::app::DocumentExportServices { public: explicit LegacyDocumentExportServices(App& app) noexcept : app_(app) { } bool create_directory(std::string_view directory) override { return Asset::create_dir(std::string(directory)); } void export_equirectangular(const pp::app::DocumentExportFileTarget& target) override { auto* app = &app_; #if !__WEB__ if (is_png_export_target(target.path)) { const auto prepared = prepare_legacy_document_export_snapshot(app_, "export-equirectangular"); if (prepared) { const auto exported = export_equirectangular_png_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::equirectangular, pp::app::document_export_equirectangular_platform_destination(), app_.work_path)); return; } LOG( "export-equirectangular document export writer retained legacy export after failure: %s", exported.message); } else { LOG( "export-equirectangular document export snapshot bridge retained legacy export after failure: %s", prepared.status().message); } } else #endif { prepare_legacy_document_export_snapshot_or_continue(app_, "export-equirectangular"); } app_.canvas->m_canvas->export_equirectangular(target.path, [app, target] { #if __WEB__ app->ui_task([app, target] { app->save_prepared_file(target.path, target.suggested_name, [](const std::string&, bool) { }); }); #else (void)target; show_export_success_dialog( *app, pp::app::plan_document_export_success_dialog( pp::app::DocumentExportSuccessKind::equirectangular, pp::app::document_export_equirectangular_platform_destination(), app->work_path)); #endif }); } 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, pp::app::plan_document_export_success_dialog( pp::app::DocumentExportSuccessKind::layers, pp::app::DocumentExportSuccessDestination::path, target.stem_path)); }); } 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, pp::app::plan_document_export_success_dialog( pp::app::DocumentExportSuccessKind::layers, pp::app::DocumentExportSuccessDestination::files_panopainter)); }); } 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, pp::app::plan_document_export_success_dialog( pp::app::DocumentExportSuccessKind::animation_frames, pp::app::DocumentExportSuccessDestination::path, target.stem_path)); }); } 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, pp::app::plan_document_export_success_dialog( pp::app::DocumentExportSuccessKind::animation_frames, pp::app::DocumentExportSuccessDestination::files_panopainter)); }); } void export_depth(std::string_view document_name) override { auto* app = &app_; #if !__WEB__ const auto target = pp::app::make_document_depth_export_target(app_.work_path, document_name); if (target) { LOG( "export-depth document export target: image=%s depth=%s", target.value().image_path.c_str(), target.value().depth_path.c_str()); } else { LOG("export-depth document export target planning failed: %s", target.status().message); } const auto prepared = prepare_legacy_document_export_snapshot(app_, "export-depth"); if (prepared) { const auto plan = pp::paint_renderer::plan_document_depth_export_render( pp::paint_renderer::DocumentDepthExportRenderPlanRequest { .document = &prepared.value().snapshot.document, .frame_index = prepared.value().snapshot.document.active_frame_index(), }); if (plan) { LOG( "export-depth document export render plan: output=%ux%u mergedFaceDraws=%zu layerDepthDraws=%zu visitedLayers=%zu visibleLayers=%zu facePayloads=%zu requiresRendererReadback=%s", plan.value().output_extent.width, plan.value().output_extent.height, plan.value().merged_face_draw_count, plan.value().layer_depth_draw_count, plan.value().visited_layer_count, plan.value().visible_layer_count, plan.value().face_payload_count, plan.value().requires_renderer_readback ? "true" : "false"); } else { LOG( "export-depth document export render plan retained legacy export after failure: %s", plan.status().message); } } else { LOG( "export-depth document export snapshot bridge retained legacy export after failure: %s", prepared.status().message); } #else prepare_legacy_document_export_snapshot_or_continue(app_, "export-depth"); #endif app_.canvas->m_canvas->export_depth(std::string(document_name), [app] { show_export_success_dialog( *app, pp::app::plan_document_export_success_dialog( pp::app::DocumentExportSuccessKind::depth, pp::app::document_export_media_platform_destination(), app->work_path)); }); } void export_cube_faces(std::string_view document_name) override { auto* app = &app_; const auto prepared = prepare_legacy_document_export_snapshot(app_, "export-cube-faces"); if (prepared) { const auto exported = export_cube_faces_from_document_snapshot(app_, document_name, prepared.value()); if (exported.ok()) { show_export_success_dialog( app_, pp::app::plan_document_export_success_dialog( pp::app::DocumentExportSuccessKind::cube_faces, pp::app::document_export_media_platform_destination(), app_.work_path)); return; } LOG( "export-cube-faces document export writer retained legacy export after failure: %s", exported.message); } else { LOG( "export-cube-faces document export snapshot bridge retained legacy export after failure: %s", prepared.status().message); } app_.canvas->m_canvas->export_cube_faces(std::string(document_name), [app] { show_export_success_dialog( *app, pp::app::plan_document_export_success_dialog( pp::app::DocumentExportSuccessKind::cube_faces, pp::app::document_export_media_platform_destination(), app->work_path)); }); } private: App& app_; }; class LegacyDocumentVideoExportServices final : public pp::app::DocumentVideoExportServices { public: LegacyDocumentVideoExportServices(App& app, bool asynchronous) noexcept : app_(app) , asynchronous_(asynchronous) { } void export_animation_mp4(std::string_view path) override { auto* app = &app_; auto path_string = std::string(path); if (asynchronous_) { Canvas::I->export_anim_mp4(path_string, [app, path_string] { show_export_success_dialog( *app, pp::app::plan_document_export_success_dialog( pp::app::DocumentExportSuccessKind::animation_mp4, pp::app::DocumentExportSuccessDestination::path, path_string)); }); return; } Canvas::I->export_anim_mp4(path_string, [app] { show_export_success_dialog( *app, pp::app::plan_document_export_success_dialog( pp::app::DocumentExportSuccessKind::animation_mp4, pp::app::DocumentExportSuccessDestination::generic_success)); }); } void export_timelapse_mp4(std::string_view path) override { auto* app = &app_; auto path_string = std::string(path); if (asynchronous_) { std::thread([app, path_string] { BT_SetTerminate(); app->rec_export(path_string); show_export_success_dialog( *app, pp::app::plan_document_export_success_dialog( pp::app::DocumentExportSuccessKind::timelapse, pp::app::DocumentExportSuccessDestination::path, path_string)); }).detach(); return; } app->rec_export(path_string); } void show_animation_export_success(std::string_view path) override { (void)path; } void show_timelapse_export_success(std::string_view path) override { if (asynchronous_) { (void)path; } else { (void)path; show_export_success_dialog( app_, pp::app::plan_document_export_success_dialog( pp::app::DocumentExportSuccessKind::timelapse, pp::app::DocumentExportSuccessDestination::generic_success)); } } private: App& app_; bool asynchronous_ = false; }; } // namespace pp::foundation::Status execute_legacy_document_export_file( App& app, const pp::app::DocumentExportFileTarget& target) { LegacyDocumentExportServices services(app); return pp::app::execute_document_export_file(target, services); } pp::foundation::Status execute_legacy_document_export_stem( App& app, pp::app::DocumentExportCollectionKind kind, const pp::app::DocumentExportStemTarget& target) { LegacyDocumentExportServices services(app); return pp::app::execute_document_export_stem(kind, target, services); } pp::foundation::Status execute_legacy_document_export_collection( App& app, pp::app::DocumentExportCollectionKind kind, const pp::app::DocumentExportCollectionTarget& target) { LegacyDocumentExportServices services(app); return pp::app::execute_document_export_collection(kind, target, services); } pp::foundation::Status execute_legacy_document_export_depth( App& app, std::string_view document_name) { LegacyDocumentExportServices services(app); return pp::app::execute_document_export_depth(document_name, services); } pp::foundation::Status execute_legacy_document_export_cube_faces( App& app, std::string_view document_name) { LegacyDocumentExportServices services(app); return pp::app::execute_document_export_cube_faces(document_name, services); } pp::foundation::Status execute_legacy_document_video_export( App& app, pp::app::DocumentVideoExportKind kind, std::string_view path, bool asynchronous) { LegacyDocumentVideoExportServices services(app, asynchronous); return pp::app::execute_document_video_export(kind, path, services); } } // namespace pp::panopainter