Files
panopainter/src/legacy_canvas_document_io_services.cpp

1196 lines
39 KiB
C++

#include "pch.h"
#include "log.h"
#include "canvas.h"
#include "app.h"
#include "legacy_canvas_draw_merge_services.h"
#include "legacy_ui_gl_dispatch.h"
#include "legacy_ui_overlay_services.h"
#include "app_core/document_canvas.h"
#include "texture.h"
#include "node_progress_bar.h"
#include "paint_renderer/compositor.h"
#include "renderer_gl/opengl_capabilities.h"
#include "util.h"
#include <array>
#include <cstdint>
#include <numeric>
#ifdef __APPLE__
#include <Foundation/Foundation.h>
#endif
namespace {
GLenum depth_test_state()
{
return static_cast<GLenum>(pp::renderer::gl::depth_test_state());
}
GLenum blend_state()
{
return static_cast<GLenum>(pp::renderer::gl::blend_state());
}
pp::renderer::gl::OpenGlPixelFormat texture_format_for_image_channels(int channel_count)
{
return pp::renderer::gl::texture_format_for_channel_count(static_cast<std::uint32_t>(channel_count));
}
void set_active_texture_unit(std::uint32_t unit_index)
{
pp::legacy::ui_gl::activate_texture_unit(unit_index, "Canvas");
}
void apply_canvas_viewport(std::int32_t x, std::int32_t y, std::int32_t width, std::int32_t height)
{
pp::legacy::ui_gl::apply_viewport(x, y, width, height, "Canvas");
}
pp::renderer::gl::OpenGlViewportRect query_canvas_viewport()
{
return pp::legacy::ui_gl::query_viewport_rect("Canvas");
}
std::array<float, 4> query_canvas_clear_color()
{
return pp::legacy::ui_gl::query_clear_color("Canvas");
}
void apply_canvas_clear_color(std::array<float, 4> color)
{
pp::legacy::ui_gl::set_clear_color(color, "Canvas");
}
void apply_canvas_capability(std::uint32_t state, bool enabled)
{
pp::legacy::ui_gl::set_capability(state, enabled, "Canvas");
}
bool query_canvas_capability(std::uint32_t state)
{
return pp::legacy::ui_gl::query_capability(state, "Canvas");
}
pp::renderer::RenderDeviceFeatures canvas_render_device_features() noexcept
{
return ShaderManager::render_device_features();
}
pp::paint_renderer::CanvasStrokeRasterizationPlan canvas_stroke_rasterization_plan(
int width,
int height) noexcept
{
return pp::panopainter::plan_legacy_canvas_stroke_rasterization(
canvas_render_device_features(),
width,
height);
}
pp::paint_renderer::CanvasStrokeFeedbackPlan canvas_destination_feedback_plan(
int width,
int height) noexcept
{
return canvas_stroke_rasterization_plan(width, height).feedback;
}
} // namespace
void Canvas::import_equirectangular(std::string file_path, std::shared_ptr<Layer> layer /*= nullptr*/)
{
if (App::I->check_license())
{
App::I->runtime().canvas_async_task([this, file_path = std::move(file_path), layer = std::move(layer)] {
BT_SetTerminate();
import_equirectangular_thread(file_path, layer);
});
}
}
void Canvas::import_equirectangular_thread(std::string file_path, std::shared_ptr<Layer> layer /*= nullptr*/, int frame /*= -1*/)
{
Image img;
if (!img.load_file(file_path))
return;
if (!layer)
layer = m_layers[m_current_layer_idx];
if (frame == -1)
frame = layer->m_frame_index;
auto a = new ActionImportEquirect;
a->m_layer = layer;
a->m_frame = frame;
a->m_snap = std::make_shared<LayerFrame::Snapshot>(layer->snapshot(frame));
a->m_path = file_path;
ActionManager::add(a);
m_unsaved = true;
if (img.width == img.height / 6)
{
Texture2D tex;
static const GLint indices[] = { 5, 0, 4, 1, 2, 3 };
const auto texture_format = texture_format_for_image_channels(img.comp);
tex.create(
img.width,
img.width,
static_cast<GLint>(texture_format.internal_format),
static_cast<GLint>(texture_format.pixel_format));
int stride = img.width * img.width * img.comp;
Plane plane;
plane.create<1>(2, 2);
draw_objects([&](const glm::mat4& camera, const glm::mat4& proj, int i) {
apply_canvas_capability(depth_test_state(), false);
tex.update(img.m_data.get() + indices[i] * stride);
m_sampler.bind(0);
set_active_texture_unit(0);
tex.bind();
pp::panopainter::setup_legacy_canvas_draw_merge_texture_shader(
pp::panopainter::LegacyCanvasDrawMergeTextureUniforms {
.mvp = glm::scale(glm::vec3(-1, -1, 1)),
.texture_slot = 0,
});
plane.draw_fill();
tex.unbind();
m_sampler.unbind();
}, frame, false);
plane.destroy();
}
else
{
Texture2D tex;
tex.load_file(file_path);
Sphere sphere;
sphere.create<64, 64>(2.f);
draw_objects([&](const glm::mat4& camera, const glm::mat4& proj, int i) {
apply_canvas_capability(depth_test_state(), false);
m_sampler.bind(0);
set_active_texture_unit(0);
tex.bind();
pp::panopainter::setup_legacy_canvas_draw_merge_texture_shader(
pp::panopainter::LegacyCanvasDrawMergeTextureUniforms {
.mvp = proj * camera *
glm::eulerAngleY(glm::radians(180.f)) * glm::scale(glm::vec3(1, -1, 1)),
.texture_slot = 0,
});
sphere.draw_fill();
tex.unbind();
m_sampler.unbind();
}, frame, false);
sphere.destroy();
}
for (int i = 0; i < 6; i++)
{
layer->box(i) = glm::vec4(0, 0, m_width, m_height);
layer->face(i) = true;
}
}
void Canvas::export_equirectangular(std::string file_path, std::function<void()> on_complete)
{
if (App::I->check_license())
{
App::I->runtime().canvas_async_task([this, file_path = std::move(file_path), on_complete = std::move(on_complete)]() mutable {
BT_SetTerminate();
export_equirectangular_thread(file_path);
if (on_complete)
App::I->ui_task([on_complete = std::move(on_complete)]() mutable { on_complete(); });
});
}
}
void Canvas::export_equirectangular_thread(std::string file_path)
{
Image data;
App::I->render_task([&]
{
draw_merge(false);
Texture2D equirect = m_layers_merge.gen_equirect();
data = equirect.get_image();
});
LOG("writing %s", file_path.c_str());
if (file_path.substr(file_path.size() - 4) == ".jpg")
{
data.save_jpg(file_path, 100);
inject_xmp(file_path);
}
else if (file_path.substr(file_path.size() - 4) == ".png")
{
data.save_png(file_path);
}
App::I->publish_exported_image(file_path);
}
void Canvas::inject_xmp(std::string jpg_path)
{
static const char xmp[] =
"http://ns.adobe.com/xap/1.0/\0" R"(<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
<x:xmpmeta xmlns:x="adobe:ns:meta/" xmptk="SAMSUNG 360CAM">
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description rdf:about="" xmlns:GPano="http://ns.google.com/photos/1.0/panorama/">
<GPano:ProjectionType>equirectangular</GPano:ProjectionType>
<GPano:UsePanoramaViewer>True</GPano:UsePanoramaViewer>
<GPano:CroppedAreaLeftPixels>0</GPano:CroppedAreaLeftPixels>
<GPano:CroppedAreaTopPixels>0</GPano:CroppedAreaTopPixels>
<GPano:PoseHeadingDegrees>0</GPano:PoseHeadingDegrees>
<GPano:PosePitchDegrees>0</GPano:PosePitchDegrees>
<GPano:PoseRollDegrees>0</GPano:PoseRollDegrees>
<GPano:StitchingSoftware>PanoPainter</GPano:StitchingSoftware>
</rdf:Description>
</rdf:RDF>
</x:xmpmeta>
<?xpacket end="r"?>)";
FILE* fp = fopen(jpg_path.c_str(), "rb");
fseek(fp, 0, SEEK_END);
long len = ftell(fp);
fseek(fp, 0, SEEK_SET);
unsigned char* jpeg_data = (unsigned char*)malloc(len);
fread(jpeg_data, len, 1, fp);
fclose(fp);
fp = fopen(jpg_path.c_str(), "wb");
int i = 0;
while (i < len && !(jpeg_data[i] == 0xff && jpeg_data[i + 1] == 0xd8)) i++;
i += 2;
unsigned char* xmp_section = (unsigned char*)malloc(sizeof(xmp) + 4);
xmp_section[0] = 0xff;
xmp_section[1] = 0xe1;
xmp_section[2] = ((int)sizeof(xmp) + 2) >> 8;
xmp_section[3] = ((int)sizeof(xmp) + 2) >> 0;
memcpy(xmp_section + 4, xmp, sizeof(xmp));
fwrite(jpeg_data, 1, i, fp);
fwrite(xmp_section, 1, sizeof(xmp) + 4, fp);
fwrite(jpeg_data + i, 1, len - i, fp);
fclose(fp);
}
void Canvas::export_depth(std::string file_name, std::function<void()> on_complete)
{
if (App::I->check_license())
{
App::I->runtime().canvas_async_task([this, file_name = std::move(file_name), on_complete = std::move(on_complete)]() mutable {
BT_SetTerminate();
export_depth_thread(file_name);
if (on_complete)
App::I->ui_task([on_complete = std::move(on_complete)]() mutable { on_complete(); });
});
}
}
void Canvas::export_depth_thread(std::string file_name)
{
RTT rtt;
rtt.create(1024, 1024);
glm::mat4 proj = glm::perspective(glm::radians(m_cam_fov), (float)rtt.getWidth() / (float)rtt.getHeight(), 0.1f, 100.f);
glm::mat4 camera = m_cam_rot;
App::I->render_task([&]
{
draw_merge(false);
rtt.bindFramebuffer();
rtt.clear({ 0, 0, 0, 1 });
apply_canvas_capability(blend_state(), true);
apply_canvas_capability(depth_test_state(), false);
apply_canvas_viewport(0, 0, rtt.getWidth(), rtt.getHeight());
for (int plane_index = 0; plane_index < 6; plane_index++)
{
auto plane_mvp_z = proj * camera *
m_plane_transform[plane_index] *
glm::translate(glm::vec3(0, 0, -1)) *
glm::scale(glm::vec3(2));
m_sampler.bind(0);
pp::panopainter::setup_legacy_canvas_draw_merge_texture_alpha_shader(
pp::panopainter::LegacyCanvasDrawMergeTextureAlphaUniforms {
.mvp = plane_mvp_z,
.texture_slot = 0,
.alpha = 1.f,
.highlight = false,
});
set_active_texture_unit(0);
m_layers_merge.rtt(plane_index).bindTexture();
m_plane.draw_fill();
m_layers_merge.rtt(plane_index).unbindTexture();
}
rtt.unbindFramebuffer();
});
uint8_t* rgba_data = rtt.readTextureData();
stbi_flip_vertically_on_write(true);
std::string path_rgba = App::I->work_path + "/" + file_name + ".png";
stbi_write_jpg(path_rgba.c_str(), rtt.getWidth(), rtt.getHeight(), 4, rgba_data, 100);
delete rgba_data;
App::I->render_task([&]
{
rtt.bindFramebuffer();
rtt.clear({ 0, 0, 0, 1 });
apply_canvas_capability(blend_state(), true);
apply_canvas_capability(depth_test_state(), false);
apply_canvas_viewport(0, 0, rtt.getWidth(), rtt.getHeight());
for (int layer_index = 0; layer_index < m_layers.size(); layer_index++)
{
for (int plane_index = 0; plane_index < 6; plane_index++)
{
if ((!m_layers[layer_index]->m_visible ||
m_layers[layer_index]->m_opacity == .0f ||
!m_layers[layer_index]->face(plane_index)))
continue;
auto plane_mvp_z = proj * camera *
m_plane_transform[plane_index] *
glm::translate(glm::vec3(0, 0, -1)) *
glm::scale(glm::vec3(2));
m_sampler.bind(0);
pp::panopainter::setup_legacy_canvas_draw_merge_texture_colorize_shader(
pp::panopainter::LegacyCanvasDrawMergeTextureColorizeUniforms {
.mvp = plane_mvp_z,
.texture_slot = 0,
.color = { glm::vec3((float)(layer_index + 1) / (float)(m_layers.size() + 1)), 1.f },
});
set_active_texture_unit(0);
m_layers[layer_index]->rtt(plane_index).bindTexture();
m_plane.draw_fill();
m_layers[layer_index]->rtt(plane_index).unbindTexture();
}
}
rtt.unbindFramebuffer();
});
uint8_t* depth_data = rtt.readTextureData();
std::string path_depth = App::I->work_path + "/" + file_name + "_depth.png";
stbi_write_jpg(path_depth.c_str(), rtt.getWidth(), rtt.getHeight(), 4, depth_data, 100);
delete depth_data;
stbi_flip_vertically_on_write(false);
rtt.destroy();
}
void Canvas::export_layers(std::string path, std::function<void()> on_complete)
{
if (App::I->check_license())
{
App::I->runtime().canvas_async_task([this, path = std::move(path), on_complete = std::move(on_complete)]() mutable {
BT_SetTerminate();
export_layers_thread(path);
if (on_complete)
App::I->ui_task([on_complete = std::move(on_complete)]() mutable { on_complete(); });
});
}
}
void Canvas::export_layers_thread(std::string path)
{
auto pb = App::I->show_progress("Export Layers", m_layers.size());
for (int i = 0; i < m_layers.size(); i++)
{
auto l = m_layers[i];
Image img = l->gen_equirect().get_image();
img.save_png(fmt::format("{}-layer{:02d}-{}.png", path, i, l->m_name));
pb->increment();
}
pp::panopainter::close_legacy_dialog_node(*pb);
}
void Canvas::export_anim_frames(std::string path, std::function<void()> on_complete)
{
if (App::I->check_license())
{
App::I->runtime().canvas_async_task([this, path = std::move(path), on_complete = std::move(on_complete)]() mutable {
BT_SetTerminate();
export_anim_frames_thread(path);
if (on_complete)
App::I->ui_task([on_complete = std::move(on_complete)]() mutable { on_complete(); });
});
}
}
void Canvas::export_anim_frames_thread(std::string path)
{
auto pb = App::I->show_progress("Export Frames", anim_duration());
for (int i = 0; i < anim_duration(); i++)
{
anim_goto_frame(i);
export_equirectangular_thread(fmt::format("{}-{:02d}.png", path, i));
pb->increment();
}
pp::panopainter::close_legacy_dialog_node(*pb);
}
void Canvas::export_anim_mp4(std::string path, std::function<void()> on_complete)
{
if (App::I->check_license())
{
App::I->runtime().canvas_async_task([this, path = std::move(path), on_complete = std::move(on_complete)]() mutable {
BT_SetTerminate();
export_anim_mp4_thread(path);
if (on_complete)
App::I->ui_task([on_complete = std::move(on_complete)]() mutable { on_complete(); });
});
}
}
void Canvas::export_anim_mp4_thread(std::string path)
{
auto pb = App::I->show_progress("Export Animation", anim_duration());
int fps = App::I->animation->get_fps();
MP4Encoder mp4;
int res = std::min<int>(1024, m_width);
mp4.init(res * 4, res * 2, 30, 2 << 20);
for (int i = 0; i < anim_duration(); i++)
{
Image data;
App::I->render_task([&]
{
anim_goto_frame(i);
draw_merge(false);
Texture2D equirect = m_layers_merge.gen_equirect({ res, res });
data = equirect.get_image();
});
for (int j = 0; j < 30/fps; j++)
mp4.encode(data);
pb->increment();
}
mp4.write_mp4(path);
pp::panopainter::close_legacy_dialog_node(*pb);
}
void Canvas::export_cube_faces(std::string file_name, std::function<void()> on_complete)
{
if (App::I->check_license())
{
App::I->runtime().canvas_async_task([this, file_name = std::move(file_name), on_complete = std::move(on_complete)]() mutable {
BT_SetTerminate();
export_cube_faces_thread(file_name);
if (on_complete)
App::I->ui_task([on_complete = std::move(on_complete)]() mutable { on_complete(); });
});
}
}
void Canvas::export_cube_faces_thread(std::string file_name)
{
#ifdef __OBJC__
NSMutableArray* files = [NSMutableArray array];
#endif
static std::array<const char*, 6> plane_names{ "front", "right", "back", "left", "top", "bottom" };
auto pb = App::I->show_progress("Export Cube Faces", 7);
App::I->render_task([this] {
draw_merge(false);
});
pb->increment();
for (int i = 0; i < 6; i++)
{
Image face = m_layers_merge.rtt(i).get_image();
std::string path = fmt::format("{}/{}-{}.png", App::I->work_path, file_name, plane_names[i]);
face.save_png(path);
pb->increment();
App::I->publish_exported_image(path);
#ifdef __OBJC__
[files addObject : [NSString stringWithUTF8String:path.c_str()] ];
#endif
}
pp::panopainter::close_legacy_dialog_node(*pb);
#ifdef __OBJC__
static char name[128];
sprintf(name, "%s.zip", App::I->work_path.c_str());
auto zip_path = [NSString stringWithUTF8String:name];
//[SSZipArchive createZipFileAtPath:zip_path withFilesAtPaths:files];
//for (NSString* f : files)
// [[NSFileManager defaultManager] removeItemAtPath:f error:nil];
#endif
}
void Canvas::project_save(std::function<void(bool)> on_complete)
{
if (App::I->check_license())
{
const auto file_path = App::I->doc_path;
App::I->runtime().canvas_async_task([this, file_path, on_complete = std::move(on_complete)]() mutable {
BT_SetTerminate();
bool ret = project_save_thread(file_path, true);
if (on_complete)
App::I->ui_task([on_complete = std::move(on_complete), ret]() mutable { on_complete(ret); });
});
}
}
void Canvas::project_save(std::string file_path, std::function<void(bool)> on_complete)
{
LOG("saving %s", file_path.c_str());
if (App::I->check_license())
{
App::I->runtime().canvas_async_task([this, file_path = std::move(file_path), on_complete = std::move(on_complete)]() mutable {
BT_SetTerminate();
bool ret = project_save_thread(file_path, true);
if (on_complete)
App::I->ui_task([on_complete = std::move(on_complete), ret]() mutable { on_complete(ret); });
});
}
else
{
LOG("no license, no save");
}
}
bool Canvas::project_save_thread(std::string file_path, bool show_progress)
{
// already saved, nothing to do
if (!m_unsaved && file_path == App::I->doc_path)
{
LOG("already saved");
return true;
}
// static char name[128];
// sprintf(name, "%s/latlong.ppi", data_path.c_str());
FILE* fp = nullptr;
const auto save_target = pp::app::plan_document_canvas_project_save_target(App::I->data_path, file_path);
if (!save_target) {
LOG("cannot plan project save target for %s: %s", file_path.c_str(), save_target.status().message);
return false;
}
const auto& save_paths = save_target.value();
const std::string& file_name = save_paths.file_name;
const std::string& tmp_path = save_paths.temporary_path;
const std::string& lapse_path = save_paths.timelapse_path;
LOG("file name %s", file_name.c_str());
LOG("tmp path %s", tmp_path.c_str());
bool target_exists = false;
if ((fp = fopen(file_path.c_str(), "rb"))) {
fclose(fp);
fp = nullptr;
target_exists = true;
}
const auto write_plan = pp::app::plan_document_canvas_project_save_write(save_paths, target_exists);
if (!write_plan) {
LOG("cannot plan project save write for %s: %s", file_path.c_str(), write_plan.status().message);
return false;
}
bool use_tmp = write_plan.value().uses_temporary;
if (write_plan.value().uses_temporary)
{
LOG("use tmp file");
fp = fopen(write_plan.value().write_path.c_str(), "wb");
if (!fp)
{
LOG("cannot write tmp project to %s", tmp_path.c_str());
use_tmp = false;
}
}
LOG("save first time");
if (!fp)
{
// write directly to the new file
if (!(fp = fopen(file_path.c_str(), "wb")))
{
LOG("cannot write project to %s", file_path.c_str());
return false;
}
LOG("unsafe mode saving directly to %s", file_path.c_str());
}
PPIHeader ppi_header;
fwrite(&ppi_header, sizeof(PPIHeader), 1, fp);
// load thumbnail
Image thumb = thumbnail_generate(ppi_header.thumb_header.width, ppi_header.thumb_header.height);
std::shared_ptr<NodeProgressBar> pb;
if (show_progress)
pb = App::I->show_progress("Saving Pano Project");
thumb.flip();
fwrite(thumb.data(), thumb.size(), 1, fp);
fwrite(&m_width, sizeof(int), 1, fp);
fwrite(&m_height, sizeof(int), 1, fp);
int n_layers = (int)m_layers.size();
fwrite(&n_layers, sizeof(int), 1, fp);
int n_frames = std::accumulate(m_layers.begin(), m_layers.end(), 0,
[](int tot, auto& l) { return tot + l->frames_count(); });
if (ppi_header.doc_version.minor >= 3)
fwrite(&n_frames, sizeof(int), 1, fp);
int progress = 0;
int total = n_frames * 6;
for (int i = 0; i < (int)m_layers.size(); i++)
{
int n_order = i;
fwrite(&n_order, sizeof(int), 1, fp);
float layer_alpha = m_layers[i]->m_opacity;
fwrite(&layer_alpha, sizeof(float), 1, fp);
int name_len = (int)m_layers[i]->m_name.size();
fwrite(&name_len, sizeof(int), 1, fp);
fwrite(m_layers[i]->m_name.data(), name_len, 1, fp);
if (ppi_header.doc_version.minor >= 2)
{
fwrite(&m_layers[i]->m_blend_mode, sizeof(int), 1, fp);
fwrite(&m_layers[i]->m_alpha_locked, sizeof(bool), 1, fp);
fwrite(&m_layers[i]->m_visible, sizeof(bool), 1, fp);
}
int frames = 1;
if (ppi_header.doc_version.minor >= 3)
{
frames = (int)m_layers[i]->frames_count();
fwrite(&frames, sizeof(int), 1, fp);
}
for (int fi = 0; fi < frames; fi++)
{
if (ppi_header.doc_version.minor >= 3)
{
int duration = m_layers[i]->frame_duration(fi);
fwrite(&duration, sizeof(int), 1, fp);
}
bool gpu = m_layers[i]->frame(fi).gpu_load();
m_layers[i]->optimize(fi);
auto snap = m_layers[i]->snapshot(fi);
for (int plane_index = 0; plane_index < 6; plane_index++)
{
int has_data = snap.m_dirty_face[plane_index] ? 1 : 0;
fwrite(&has_data, sizeof(int), 1, fp);
if (has_data)
{
glm::ivec4 b = snap.m_dirty_box[plane_index];
glm::vec2 sz = zw(b) - xy(b);
int box[4] = { b.x, b.y, b.z, b.w };
fwrite(&box, sizeof(box), 1, fp);
std::vector<uint8_t> compressed;
auto callback = [](void* context, void* data, int size)
{
std::vector<uint8_t>* buffer = static_cast<std::vector<uint8_t>*>(context);
buffer->insert(buffer->end(), (uint8_t*)data, (uint8_t*)data + size);
};
int ret = stbi_write_png_to_func(callback, &compressed, sz.x, sz.y, 4, snap.image[plane_index].get(), sz.x * 4);
int data_size = (int)compressed.size();
fwrite(&data_size, sizeof(int), 1, fp);
fwrite(compressed.data(), 1, compressed.size(), fp);
}
progress++;
float p = (float)progress / total * 100.f;
if (show_progress)
pb->m_progress->SetWidthP(p);
LOG("progress: %f", p);
}
if (!gpu)
m_layers[i]->frame(fi).gpu_unload();
}
}
if (ppi_header.doc_version.minor >= 4)
{
BinaryStreamWriter sw;
sw.init(BinaryStream::ByteOrder::LittleEndian);
Serializer::Descriptor info;
info.class_id = "ppi_info";
info.name = L"info header";
//info.props["has_encoder"] = std::make_shared<Serializer::Boolean>(m_encoder != nullptr);
sw << info;
//if (m_encoder != nullptr)
// sw << *m_encoder;
int bytes = sw.m_data.size();
fwrite(&bytes, sizeof(int), 1, fp);
fwrite((char*)sw.m_data.data(), sw.m_data.size(), 1, fp);
}
fclose(fp);
bool target_remove_attempted = false;
bool target_remove_succeeded = false;
bool temporary_rename_attempted = false;
bool temporary_rename_succeeded = false;
if (use_tmp)
{
LOG("project saved tmp to %s", tmp_path.c_str());
LOG("swapping to %s", file_path.c_str());
target_remove_attempted = true;
target_remove_succeeded = std::remove(file_path.c_str()) == 0;
if (target_remove_succeeded)
{
temporary_rename_attempted = true;
temporary_rename_succeeded = std::rename(tmp_path.c_str(), file_path.c_str()) == 0;
}
}
const auto commit_plan = pp::app::plan_document_canvas_project_save_commit(
pp::app::DocumentCanvasProjectSaveCommitInput {
.used_temporary = use_tmp,
.target_remove_attempted = target_remove_attempted,
.target_remove_succeeded = target_remove_succeeded,
.temporary_rename_attempted = temporary_rename_attempted,
.temporary_rename_succeeded = temporary_rename_succeeded,
});
const bool success = commit_plan.saved;
if (commit_plan.saved && commit_plan.temporary_renamed) {
LOG("tmp file swapped succesfully");
} else if (!commit_plan.saved && commit_plan.target_may_be_missing) {
LOG("tmp file NOT swapped, original removed");
} else if (!commit_plan.saved && commit_plan.used_temporary) {
LOG("could not remove %s", file_path.c_str());
} else if (commit_plan.saved) {
LOG("project saved to %s", file_path.c_str());
}
const auto post_commit_plan = pp::app::plan_document_canvas_project_save_post_commit(
pp::app::DocumentCanvasProjectSavePostCommitInput {
.save_succeeded = success,
.timelapse_encoder_available = Canvas::I->m_encoder != nullptr,
.progress_ui_visible = show_progress,
});
if (post_commit_plan.marks_document_clean)
{
m_unsaved = false;
}
if (post_commit_plan.marks_new_document_committed)
{
m_newdoc = false;
}
if (post_commit_plan.saves_timelapse_sidecar)
{
BinaryStreamWriter sw;
sw.init(BinaryStream::ByteOrder::LittleEndian);
Serializer::Descriptor info;
info.class_id = "tracks-info";
info.name = L"Timelapse Tracks";
info.props["has-track-360"] = std::make_shared<Serializer::Boolean>(true);
info.props["version"] = std::make_shared<Serializer::Integer>(1);
sw << info;
sw << *Canvas::I->m_encoder;
if (!sw.save(lapse_path))
LOG("cannot save timelase to %s", lapse_path.c_str());
}
if (post_commit_plan.flushes_platform_storage)
{
App::I->flush_platform_storage();
}
if (post_commit_plan.dismisses_progress_ui)
{
pp::panopainter::close_legacy_dialog_node(*pb);
}
if (post_commit_plan.updates_title)
{
App::I->title_update();
}
return success;
}
void Canvas::project_open(std::string file_path, std::function<void(bool)> on_complete)
{
App::I->runtime().canvas_async_task([this, file_path = std::move(file_path), on_complete = std::move(on_complete)]() mutable {
BT_SetTerminate();
bool result = project_open_thread(file_path);
if (on_complete)
App::I->ui_task([on_complete = std::move(on_complete), result]() mutable { on_complete(result); });
});
}
bool Canvas::project_open_thread(std::string file_path)
{
FILE* fp = fopen(file_path.c_str(), "rb");
if (!fp)
{
LOG("cannot write project to %s", file_path.c_str());
return false; // should probably return a bool
}
PPIHeader ppi_header;
fread(&ppi_header, sizeof(PPIHeader), 1, fp);
if (!ppi_header.valid())
{
LOG("INVALID PPI HEADER");
return false;
}
std::shared_ptr<NodeProgressBar> pb;
if (App::I->layout.m_loaded)
{
pb = std::make_shared<NodeProgressBar>();
pb->set_manager(&App::I->layout);
pb->init();
pb->create();
pb->loaded();
pb->m_progress->SetWidthP(0);
pb->m_title->set_text("Opening Pano Project");
App::I->layout[App::I->main_id]->add_child(pb);
}
// skip thumbnail
Image thumb;
thumb.width = ppi_header.thumb_header.width;
thumb.height = ppi_header.thumb_header.height;
thumb.comp = ppi_header.thumb_header.comp;
fseek(fp, thumb.size(), SEEK_CUR);
fread(&m_width, sizeof(int), 1, fp);
fread(&m_height, sizeof(int), 1, fp);
int n_layers = 0;
fread(&n_layers, sizeof(int), 1, fp);
int n_frames = 1;
if (ppi_header.doc_version.minor >= 3)
fread(&n_frames, sizeof(int), 1, fp);
const int bytes = m_width * m_height * 4;
LayerFrame::Snapshot snap;
snap.create(m_width, m_height); // allocate single data, no box should be bigger
int progress = 0;
int total = n_frames * 6;
for (auto& l : m_layers)
l->destroy();
m_layers.clear();
//clear_all();
resize(m_width, m_height);
std::vector<std::shared_ptr<Layer>> tmp_layers(n_layers);
for (int i = 0; i < n_layers; i++)
{
int n_order;
fread(&n_order, sizeof(int), 1, fp);
//if (ppi_header.doc_version.minor > 1)
// n_order = i;
tmp_layers[n_order] = std::make_unique<Layer>();
auto& layer = tmp_layers[n_order];
fread(&layer->m_opacity, sizeof(float), 1, fp);
int name_len;
fread(&name_len, sizeof(int), 1, fp);
std::string name(name_len, '\0');
fread((char*)name.data(), name_len, 1, fp);
if (ppi_header.doc_version.minor >= 2)
{
fread(&layer->m_blend_mode, sizeof(int), 1, fp);
fread(&layer->m_alpha_locked, sizeof(bool), 1, fp);
fread(&layer->m_visible, sizeof(bool), 1, fp);
}
int frames = 1;
if (ppi_header.doc_version.minor >= 3)
fread(&frames, sizeof(int), 1, fp);
layer->create(m_width, m_height, name.c_str());
for (int fi = 0; fi < frames; fi++)
{
if (fi > 0)
layer->add_frame();
if (ppi_header.doc_version.minor >= 3)
{
int duration = layer->frame_duration(fi);
fread(&duration, sizeof(int), 1, fp);
}
snap.clear();
for (int plane_index = 0; plane_index < 6; plane_index++)
{
int has_data;
fread(&has_data, sizeof(int), 1, fp);
snap.m_dirty_face[plane_index] = has_data;
if (has_data)
{
int b[4];
fread(&b, sizeof(b), 1, fp);
snap.m_dirty_box[plane_index] = glm::vec4(b[0], b[1], b[2], b[3]);
glm::vec2 sz = zw(snap.m_dirty_box[plane_index]) - xy(snap.m_dirty_box[plane_index]);
int data_size;
fread(&data_size, sizeof(int), 1, fp);
std::vector<uint8_t> compressed(data_size);
fread(compressed.data(), 1, data_size, fp);
int imgw, imgh, imgc;
uint8_t* rgba = stbi_load_from_memory(compressed.data(), data_size, &imgw, &imgh, &imgc, 4);
if (rgba)
{
std::copy(rgba, rgba + (imgw * imgh * 4), snap.image[plane_index].get());
delete rgba;
}
}
progress++;
float p = (float)progress / total * 100.f;
LOG("progress: %f", p);
if (App::I->layout.m_loaded)
{
pb->m_progress->SetWidthP(p);
}
}
layer->restore(snap, fi);
}
}
std::swap(tmp_layers, m_layers);
if (ppi_header.doc_version.minor >= 4)
{
int bytes = 0;
fread(&bytes, sizeof(int), 1, fp);
std::vector<uint8_t> data(bytes);
fread(data.data(), bytes, 1, fp);
BinaryStreamReader sr;
sr.init(data.data(), data.size(), BinaryStream::ByteOrder::LittleEndian);
Serializer::Descriptor info;
sr >> info;
//if (info.value<Serializer::Boolean>("has_encoder"))
//{
// m_encoder = std::make_unique<MP4Encoder>();
// sr >> *m_encoder;
// m_encoder->init();
//}
//else
//{
// timelapse_reset_encoder();
//}
}
//else
//{
// timelapse_reset_encoder();
//}
fclose(fp);
LOG("project restore from %s", file_path.c_str());
auto start = file_path.rfind('/') + 1;
std::string file_name = file_path.substr(start, file_path.length() - start - strlen(".ppi"));
std::string lapse_path = App::I->data_path + '/' + file_name + ".pptl";
if (Asset::exist(lapse_path))
{
BinaryStreamReader sr;
sr.load(lapse_path, BinaryStream::ByteOrder::LittleEndian);
Serializer::Descriptor info;
sr >> info;
if (info.value<Serializer::Boolean>("has-track-360"))
{
m_encoder = std::make_unique<MP4Encoder>();
sr >> *m_encoder;
m_encoder->init();
}
}
else
{
timelapse_reset_encoder();
}
m_current_layer_idx = 0;
m_current_stroke = nullptr;
m_dual_stroke = nullptr;
m_show_tmp = false;
m_smask_active = false;
m_smask_mode = 0;
m_dirty = false;
m_commit_delayed = false;
m_dirty_stroke = false;
memset(m_dirty_face, 0, sizeof(bool) * 6);
memset(m_pick_ready, 0, sizeof(bool) * 6);
m_unsaved = false;
m_newdoc = false;
if (App::I->layout.m_loaded)
{
pp::panopainter::close_legacy_dialog_node(*pb);
App::I->ui_task([] {
App::I->title_update();
App::I->update_rec_frames();
Canvas::I->anim_update();
App::I->animation->load_layers();
});
}
return true;
}
Image Canvas::thumbnail_generate(int w, int h)
{
Image image;
image.create(w, h);
App::I->render_task([this, w, h, &image]
{
// save viewport and clear color states
const auto vp = query_canvas_viewport();
const auto cc = query_canvas_clear_color();
auto blend = query_canvas_capability(blend_state());
// prepare common states
apply_canvas_viewport(0, 0, w, h);
RTT fb;
fb.create(w, h);
fb.bindFramebuffer();
Plane m_face_plane;
m_face_plane.create<1>(2, 2);
Texture2D blendtex;
blendtex.create(w, h);
const auto layer_feedback = canvas_destination_feedback_plan(w, h);
const bool copy_layer_destination = !layer_feedback.reads_destination_color;
// recalculate because of different aspect ratio than the m_proj matrix
glm::mat4 proj = glm::perspective(glm::radians(m_cam_fov), (float)w / (float)h, 0.1f, 1000.f);
fb.clear({ 1, 1, 1, 0 });
for (int i = 0; i < 6; i++)
{
apply_canvas_capability(blend_state(), false);
auto plane_mvp = proj * m_mv * m_plane_transform[i] * glm::translate(glm::vec3(0, 0, -1));
if (copy_layer_destination)
{
set_active_texture_unit(2);
blendtex.bind();
m_sampler_nearest.bind(2);
}
m_sampler_nearest.bind(0); // nearest
for (int layer_index = 0; layer_index < m_layers.size(); layer_index++)
{
if (!m_layers[layer_index]->m_visible ||
m_layers[layer_index]->m_opacity == 0.f ||
!m_layers[layer_index]->face(i))
continue;
if (copy_layer_destination)
{
set_active_texture_unit(2);
copy_framebuffer_to_texture_2d(0, 0, 0, 0, w, h);
}
pp::panopainter::setup_legacy_canvas_draw_merge_texture_blend_shader(
pp::panopainter::LegacyCanvasDrawMergeTextureBlendUniforms {
.mvp = plane_mvp,
.texture_slot = 0,
.destination_texture_slot = 2,
.use_destination_texture = copy_layer_destination,
.blend_mode = m_layers[layer_index]->m_blend_mode,
.alpha = m_layers[layer_index]->m_opacity,
});
set_active_texture_unit(0);
m_layers[layer_index]->rtt(i).bindTexture();
m_face_plane.draw_fill();
m_layers[layer_index]->rtt(i).unbindTexture();
}
if (copy_layer_destination)
{
set_active_texture_unit(2);
blendtex.unbind();
}
set_active_texture_unit(0);
blendtex.bind();
// copy the content of the fb before drawing the grid
copy_framebuffer_to_texture_2d(0, 0, 0, 0, w, h);
// draw the grid
pp::panopainter::setup_legacy_canvas_draw_merge_checkerboard_shader(
pp::panopainter::LegacyCanvasDrawMergeCheckerboardUniforms {
.mvp = plane_mvp,
});
m_face_plane.draw_fill();
// now blend with the background
apply_canvas_capability(blend_state(), true);
pp::panopainter::setup_legacy_canvas_draw_merge_texture_shader(
pp::panopainter::LegacyCanvasDrawMergeTextureUniforms {
.mvp = glm::ortho(-.5f, .5f, -.5f, .5f, -1.f, 1.f),
.texture_slot = 0,
});
m_sampler.bind(0); // linear
m_plane.draw_fill();
blendtex.unbind();
}
fb.unbindFramebuffer();
// read the rendered image
fb.readTextureData((uint8_t*)image.data());
fb.destroy();
blendtex.destroy();
// restore viewport and clear color states
blend ? apply_canvas_capability(blend_state(), true) : apply_canvas_capability(blend_state(), false);
apply_canvas_viewport(vp.x, vp.y, vp.width, vp.height);
apply_canvas_clear_color(cc);
set_active_texture_unit(0);
});
return image;
}
Image Canvas::thumbnail_read(std::string file_path)
{
// static char name[128];
// sprintf(name, "%s/latlong.ppi", data_path.c_str());
FILE* fp = fopen(file_path.c_str(), "rb");
if (!fp)
{
LOG("cannot read project %s", file_path.c_str());
return {}; // return empty image
}
PPIHeader ppi_header;
fread(&ppi_header, sizeof(PPIHeader), 1, fp);
if (!ppi_header.valid())
return {};
Image thumb;
thumb.width = ppi_header.thumb_header.width;
thumb.height = ppi_header.thumb_header.height;
thumb.comp = ppi_header.thumb_header.comp;
thumb.create();
fread((uint8_t*)thumb.data(), thumb.size(), 1, fp);
fclose(fp);
LOG("project thumbnail read from %s", file_path.c_str());
return thumb;
}