#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 #include #include #ifdef __APPLE__ #include #endif namespace { GLenum depth_test_state() { return static_cast(pp::renderer::gl::depth_test_state()); } GLenum blend_state() { return static_cast(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(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 query_canvas_clear_color() { return pp::legacy::ui_gl::query_clear_color("Canvas"); } void apply_canvas_clear_color(std::array 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 /*= 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 /*= 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(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(texture_format.internal_format), static_cast(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 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"( equirectangular True 0 0 0 0 0 PanoPainter )"; 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 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 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 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 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(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 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 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 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 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 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 compressed; auto callback = [](void* context, void* data, int size) { std::vector* buffer = static_cast*>(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(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(true); info.props["version"] = std::make_shared(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 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 pb; if (App::I->layout.m_loaded) { pb = std::make_shared(); 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> 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(); 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 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 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("has_encoder")) //{ // m_encoder = std::make_unique(); // 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("has-track-360")) { m_encoder = std::make_unique(); 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; }