1196 lines
39 KiB
C++
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;
|
|
}
|
|
|