Merge branch 'master' of https://github.com/prusa3d/PrusaSlicer into et_gcode_viewer
This commit is contained in:
commit
b563010bf1
17 changed files with 589 additions and 368 deletions
|
@ -35,6 +35,11 @@ option(SLIC3R_ASAN "Enable ASan on Clang and GCC" 0)
|
|||
|
||||
set(SLIC3R_GTK "2" CACHE STRING "GTK version to use with wxWidgets on Linux")
|
||||
|
||||
if (APPLE)
|
||||
set(CMAKE_FIND_FRAMEWORK LAST)
|
||||
set(CMAKE_FIND_APPBUNDLE LAST)
|
||||
endif ()
|
||||
|
||||
# Proposal for C++ unit tests and sandboxes
|
||||
option(SLIC3R_BUILD_SANDBOXES "Build development sandboxes" OFF)
|
||||
option(SLIC3R_BUILD_TESTS "Build unit tests" ON)
|
||||
|
@ -386,7 +391,7 @@ if (NOT EXPAT_FOUND)
|
|||
set(EXPAT_LIBRARIES expat)
|
||||
endif ()
|
||||
|
||||
find_package(PNG)
|
||||
find_package(PNG REQUIRED)
|
||||
|
||||
find_package(OpenGL REQUIRED)
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ add_executable(opencsg_example WIN32
|
|||
main.cpp
|
||||
Engine.hpp Engine.cpp
|
||||
ShaderCSGDisplay.hpp ShaderCSGDisplay.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src/slic3r/GUI/Jobs/Job.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src/slic3r/GUI/ProgressStatusBar.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src/slic3r/GUI/I18N.hpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src/slic3r/GUI/I18N.cpp)
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
#include "libslic3r/Format/3mf.hpp"
|
||||
#include "libslic3r/SLAPrint.hpp"
|
||||
|
||||
#include "slic3r/GUI/Job.hpp"
|
||||
#include "slic3r/GUI/Jobs/Job.hpp"
|
||||
#include "slic3r/GUI/ProgressStatusBar.hpp"
|
||||
|
||||
using namespace Slic3r::GL;
|
||||
|
|
|
@ -163,6 +163,8 @@ add_library(libslic3r STATIC
|
|||
PrintConfig.hpp
|
||||
PrintObject.cpp
|
||||
PrintRegion.cpp
|
||||
PNGRead.hpp
|
||||
PNGRead.cpp
|
||||
Semver.cpp
|
||||
ShortestPath.cpp
|
||||
ShortestPath.hpp
|
||||
|
@ -310,6 +312,8 @@ target_link_libraries(libslic3r
|
|||
TBB::tbb
|
||||
libslic3r_cgal
|
||||
${CMAKE_DL_LIBS}
|
||||
PNG::PNG
|
||||
ZLIB::ZLIB
|
||||
)
|
||||
|
||||
if (TARGET OpenVDB::openvdb)
|
||||
|
|
|
@ -8,8 +8,316 @@
|
|||
#include "libslic3r/Zipper.hpp"
|
||||
#include "libslic3r/SLAPrint.hpp"
|
||||
|
||||
#include <sstream>
|
||||
|
||||
#include "libslic3r/SlicesToTriangleMesh.hpp"
|
||||
#include "libslic3r/MarchingSquares.hpp"
|
||||
#include "libslic3r/ClipperUtils.hpp"
|
||||
#include "libslic3r/MTUtils.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include "libslic3r/SLA/RasterBase.hpp"
|
||||
#include "libslic3r/miniz_extension.hpp"
|
||||
#include "libslic3r/PNGRead.hpp"
|
||||
|
||||
#include <boost/property_tree/ini_parser.hpp>
|
||||
#include <boost/filesystem/path.hpp>
|
||||
#include <boost/algorithm/string.hpp>
|
||||
|
||||
namespace marchsq {
|
||||
|
||||
template<> struct _RasterTraits<Slic3r::png::ImageGreyscale> {
|
||||
using Rst = Slic3r::png::ImageGreyscale;
|
||||
|
||||
// The type of pixel cell in the raster
|
||||
using ValueType = uint8_t;
|
||||
|
||||
// Value at a given position
|
||||
static uint8_t get(const Rst &rst, size_t row, size_t col)
|
||||
{
|
||||
return rst.get(row, col);
|
||||
}
|
||||
|
||||
// Number of rows and cols of the raster
|
||||
static size_t rows(const Rst &rst) { return rst.rows; }
|
||||
static size_t cols(const Rst &rst) { return rst.cols; }
|
||||
};
|
||||
|
||||
} // namespace marchsq
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
namespace {
|
||||
|
||||
struct PNGBuffer { std::vector<uint8_t> buf; std::string fname; };
|
||||
struct ArchiveData {
|
||||
boost::property_tree::ptree profile, config;
|
||||
std::vector<PNGBuffer> images;
|
||||
};
|
||||
|
||||
static const constexpr char *CONFIG_FNAME = "config.ini";
|
||||
static const constexpr char *PROFILE_FNAME = "prusaslicer.ini";
|
||||
|
||||
boost::property_tree::ptree read_ini(const mz_zip_archive_file_stat &entry,
|
||||
MZ_Archive & zip)
|
||||
{
|
||||
std::string buf(size_t(entry.m_uncomp_size), '\0');
|
||||
|
||||
if (!mz_zip_reader_extract_file_to_mem(&zip.arch, entry.m_filename,
|
||||
buf.data(), buf.size(), 0))
|
||||
throw std::runtime_error(zip.get_errorstr());
|
||||
|
||||
boost::property_tree::ptree tree;
|
||||
std::stringstream ss(buf);
|
||||
boost::property_tree::read_ini(ss, tree);
|
||||
return tree;
|
||||
}
|
||||
|
||||
PNGBuffer read_png(const mz_zip_archive_file_stat &entry,
|
||||
MZ_Archive & zip,
|
||||
const std::string & name)
|
||||
{
|
||||
std::vector<uint8_t> buf(entry.m_uncomp_size);
|
||||
|
||||
if (!mz_zip_reader_extract_file_to_mem(&zip.arch, entry.m_filename,
|
||||
buf.data(), buf.size(), 0))
|
||||
throw std::runtime_error(zip.get_errorstr());
|
||||
|
||||
return {std::move(buf), (name.empty() ? entry.m_filename : name)};
|
||||
}
|
||||
|
||||
ArchiveData extract_sla_archive(const std::string &zipfname,
|
||||
const std::string &exclude)
|
||||
{
|
||||
ArchiveData arch;
|
||||
|
||||
// Little RAII
|
||||
struct Arch: public MZ_Archive {
|
||||
Arch(const std::string &fname) {
|
||||
if (!open_zip_reader(&arch, fname))
|
||||
throw std::runtime_error(get_errorstr());
|
||||
}
|
||||
|
||||
~Arch() { close_zip_reader(&arch); }
|
||||
} zip (zipfname);
|
||||
|
||||
mz_uint num_entries = mz_zip_reader_get_num_files(&zip.arch);
|
||||
|
||||
for (mz_uint i = 0; i < num_entries; ++i)
|
||||
{
|
||||
mz_zip_archive_file_stat entry;
|
||||
|
||||
if (mz_zip_reader_file_stat(&zip.arch, i, &entry))
|
||||
{
|
||||
std::string name = entry.m_filename;
|
||||
boost::algorithm::to_lower(name);
|
||||
|
||||
if (boost::algorithm::contains(name, exclude)) continue;
|
||||
|
||||
if (name == CONFIG_FNAME) arch.config = read_ini(entry, zip);
|
||||
if (name == PROFILE_FNAME) arch.profile = read_ini(entry, zip);
|
||||
|
||||
if (boost::filesystem::path(name).extension().string() == ".png") {
|
||||
auto it = std::lower_bound(
|
||||
arch.images.begin(), arch.images.end(), PNGBuffer{{}, name},
|
||||
[](const PNGBuffer &r1, const PNGBuffer &r2) {
|
||||
return std::less<std::string>()(r1.fname, r2.fname);
|
||||
});
|
||||
|
||||
arch.images.insert(it, read_png(entry, zip, name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return arch;
|
||||
}
|
||||
|
||||
ExPolygons rings_to_expolygons(const std::vector<marchsq::Ring> &rings,
|
||||
double px_w, double px_h)
|
||||
{
|
||||
ExPolygons polys; polys.reserve(rings.size());
|
||||
|
||||
for (const marchsq::Ring &ring : rings) {
|
||||
Polygon poly; Points &pts = poly.points;
|
||||
pts.reserve(ring.size());
|
||||
|
||||
for (const marchsq::Coord &crd : ring)
|
||||
pts.emplace_back(scaled(crd.c * px_w), scaled(crd.r * px_h));
|
||||
|
||||
polys.emplace_back(poly);
|
||||
}
|
||||
|
||||
// reverse the raster transformations
|
||||
return union_ex(polys);
|
||||
}
|
||||
|
||||
template<class Fn> void foreach_vertex(ExPolygon &poly, Fn &&fn)
|
||||
{
|
||||
for (auto &p : poly.contour.points) fn(p);
|
||||
for (auto &h : poly.holes)
|
||||
for (auto &p : h.points) fn(p);
|
||||
}
|
||||
|
||||
void invert_raster_trafo(ExPolygons & expolys,
|
||||
const sla::RasterBase::Trafo &trafo,
|
||||
coord_t width,
|
||||
coord_t height)
|
||||
{
|
||||
if (trafo.flipXY) std::swap(height, width);
|
||||
|
||||
for (auto &expoly : expolys) {
|
||||
if (trafo.mirror_y)
|
||||
foreach_vertex(expoly, [height](Point &p) {p.y() = height - p.y(); });
|
||||
|
||||
if (trafo.mirror_x)
|
||||
foreach_vertex(expoly, [width](Point &p) {p.x() = width - p.x(); });
|
||||
|
||||
expoly.translate(-trafo.center_x, -trafo.center_y);
|
||||
|
||||
if (trafo.flipXY)
|
||||
foreach_vertex(expoly, [](Point &p) { std::swap(p.x(), p.y()); });
|
||||
|
||||
if ((trafo.mirror_x + trafo.mirror_y + trafo.flipXY) % 2) {
|
||||
expoly.contour.reverse();
|
||||
for (auto &h : expoly.holes) h.reverse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RasterParams {
|
||||
sla::RasterBase::Trafo trafo; // Raster transformations
|
||||
coord_t width, height; // scaled raster dimensions (not resolution)
|
||||
double px_h, px_w; // pixel dimesions
|
||||
marchsq::Coord win; // marching squares window size
|
||||
};
|
||||
|
||||
RasterParams get_raster_params(const DynamicPrintConfig &cfg)
|
||||
{
|
||||
auto *opt_disp_cols = cfg.option<ConfigOptionInt>("display_pixels_x");
|
||||
auto *opt_disp_rows = cfg.option<ConfigOptionInt>("display_pixels_y");
|
||||
auto *opt_disp_w = cfg.option<ConfigOptionFloat>("display_width");
|
||||
auto *opt_disp_h = cfg.option<ConfigOptionFloat>("display_height");
|
||||
auto *opt_mirror_x = cfg.option<ConfigOptionBool>("display_mirror_x");
|
||||
auto *opt_mirror_y = cfg.option<ConfigOptionBool>("display_mirror_y");
|
||||
auto *opt_orient = cfg.option<ConfigOptionEnum<SLADisplayOrientation>>("display_orientation");
|
||||
|
||||
if (!opt_disp_cols || !opt_disp_rows || !opt_disp_w || !opt_disp_h ||
|
||||
!opt_mirror_x || !opt_mirror_y || !opt_orient)
|
||||
throw std::runtime_error("Invalid SL1 file");
|
||||
|
||||
RasterParams rstp;
|
||||
|
||||
rstp.px_w = opt_disp_w->value / (opt_disp_cols->value - 1);
|
||||
rstp.px_h = opt_disp_h->value / (opt_disp_rows->value - 1);
|
||||
|
||||
rstp.trafo = sla::RasterBase::Trafo{opt_orient->value == sladoLandscape ?
|
||||
sla::RasterBase::roLandscape :
|
||||
sla::RasterBase::roPortrait,
|
||||
{opt_mirror_x->value, opt_mirror_y->value}};
|
||||
|
||||
rstp.height = scaled(opt_disp_h->value);
|
||||
rstp.width = scaled(opt_disp_w->value);
|
||||
|
||||
return rstp;
|
||||
}
|
||||
|
||||
struct SliceParams { double layerh = 0., initial_layerh = 0.; };
|
||||
|
||||
SliceParams get_slice_params(const DynamicPrintConfig &cfg)
|
||||
{
|
||||
auto *opt_layerh = cfg.option<ConfigOptionFloat>("layer_height");
|
||||
auto *opt_init_layerh = cfg.option<ConfigOptionFloat>("initial_layer_height");
|
||||
|
||||
if (!opt_layerh || !opt_init_layerh)
|
||||
throw std::runtime_error("Invalid SL1 file");
|
||||
|
||||
return SliceParams{opt_layerh->getFloat(), opt_init_layerh->getFloat()};
|
||||
}
|
||||
|
||||
std::vector<ExPolygons> extract_slices_from_sla_archive(
|
||||
ArchiveData & arch,
|
||||
const RasterParams & rstp,
|
||||
std::function<bool(int)> progr)
|
||||
{
|
||||
auto jobdir = arch.config.get<std::string>("jobDir");
|
||||
for (auto &c : jobdir) c = std::tolower(c);
|
||||
|
||||
std::vector<ExPolygons> slices(arch.images.size());
|
||||
|
||||
struct Status
|
||||
{
|
||||
double incr, val, prev;
|
||||
bool stop = false;
|
||||
tbb::spin_mutex mutex;
|
||||
} st {100. / slices.size(), 0., 0.};
|
||||
|
||||
tbb::parallel_for(size_t(0), arch.images.size(),
|
||||
[&arch, &slices, &st, &rstp, progr](size_t i) {
|
||||
// Status indication guarded with the spinlock
|
||||
{
|
||||
std::lock_guard<tbb::spin_mutex> lck(st.mutex);
|
||||
if (st.stop) return;
|
||||
|
||||
st.val += st.incr;
|
||||
double curr = std::round(st.val);
|
||||
if (curr > st.prev) {
|
||||
st.prev = curr;
|
||||
st.stop = !progr(int(curr));
|
||||
}
|
||||
}
|
||||
|
||||
png::ImageGreyscale img;
|
||||
png::ReadBuf rb{arch.images[i].buf.data(), arch.images[i].buf.size()};
|
||||
if (!png::decode_png(rb, img)) return;
|
||||
|
||||
auto rings = marchsq::execute(img, 128, rstp.win);
|
||||
ExPolygons expolys = rings_to_expolygons(rings, rstp.px_w, rstp.px_h);
|
||||
|
||||
// Invert the raster transformations indicated in
|
||||
// the profile metadata
|
||||
invert_raster_trafo(expolys, rstp.trafo, rstp.width, rstp.height);
|
||||
|
||||
slices[i] = std::move(expolys);
|
||||
});
|
||||
|
||||
if (st.stop) slices = {};
|
||||
|
||||
return slices;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void import_sla_archive(const std::string &zipfname, DynamicPrintConfig &out)
|
||||
{
|
||||
ArchiveData arch = extract_sla_archive(zipfname, "png");
|
||||
out.load(arch.profile);
|
||||
}
|
||||
|
||||
void import_sla_archive(
|
||||
const std::string & zipfname,
|
||||
Vec2i windowsize,
|
||||
TriangleMesh & out,
|
||||
DynamicPrintConfig & profile,
|
||||
std::function<bool(int)> progr)
|
||||
{
|
||||
// Ensure minimum window size for marching squares
|
||||
windowsize.x() = std::max(2, windowsize.x());
|
||||
windowsize.y() = std::max(2, windowsize.y());
|
||||
|
||||
ArchiveData arch = extract_sla_archive(zipfname, "thumbnail");
|
||||
profile.load(arch.profile);
|
||||
|
||||
RasterParams rstp = get_raster_params(profile);
|
||||
rstp.win = {windowsize.y(), windowsize.x()};
|
||||
|
||||
SliceParams slicp = get_slice_params(profile);
|
||||
|
||||
std::vector<ExPolygons> slices =
|
||||
extract_slices_from_sla_archive(arch, rstp, progr);
|
||||
|
||||
if (!slices.empty())
|
||||
out = slices_to_triangle_mesh(slices, 0, slicp.layerh, slicp.initial_layerh);
|
||||
}
|
||||
|
||||
using ConfMap = std::map<std::string, std::string>;
|
||||
|
||||
namespace {
|
||||
|
|
|
@ -38,6 +38,24 @@ public:
|
|||
}
|
||||
};
|
||||
|
||||
void import_sla_archive(const std::string &zipfname, DynamicPrintConfig &out);
|
||||
|
||||
void import_sla_archive(
|
||||
const std::string & zipfname,
|
||||
Vec2i windowsize,
|
||||
TriangleMesh & out,
|
||||
DynamicPrintConfig & profile,
|
||||
std::function<bool(int)> progr = [](int) { return true; });
|
||||
|
||||
inline void import_sla_archive(
|
||||
const std::string & zipfname,
|
||||
Vec2i windowsize,
|
||||
TriangleMesh & out,
|
||||
std::function<bool(int)> progr = [](int) { return true; })
|
||||
{
|
||||
DynamicPrintConfig profile;
|
||||
import_sla_archive(zipfname, windowsize, out, profile, progr);
|
||||
}
|
||||
|
||||
} // namespace Slic3r::sla
|
||||
|
||||
|
|
100
src/libslic3r/PNGRead.cpp
Normal file
100
src/libslic3r/PNGRead.cpp
Normal file
|
@ -0,0 +1,100 @@
|
|||
#include "PNGRead.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <cstdio>
|
||||
#include <png.h>
|
||||
|
||||
namespace Slic3r { namespace png {
|
||||
|
||||
struct PNGDescr {
|
||||
png_struct *png = nullptr; png_info *info = nullptr;
|
||||
|
||||
PNGDescr() = default;
|
||||
PNGDescr(const PNGDescr&) = delete;
|
||||
PNGDescr(PNGDescr&&) = delete;
|
||||
PNGDescr& operator=(const PNGDescr&) = delete;
|
||||
PNGDescr& operator=(PNGDescr&&) = delete;
|
||||
|
||||
~PNGDescr()
|
||||
{
|
||||
if (png && info) png_destroy_info_struct(png, &info);
|
||||
if (png) png_destroy_read_struct( &png, nullptr, nullptr);
|
||||
}
|
||||
};
|
||||
|
||||
bool is_png(const ReadBuf &rb)
|
||||
{
|
||||
static const constexpr int PNG_SIG_BYTES = 8;
|
||||
|
||||
#if PNG_LIBPNG_VER_MINOR <= 2
|
||||
// Earlier libpng versions had png_sig_cmp(png_bytep, ...) which is not
|
||||
// a const pointer. It is not possible to cast away the const qualifier from
|
||||
// the input buffer so... yes... life is challenging...
|
||||
png_byte buf[PNG_SIG_BYTES];
|
||||
auto inbuf = static_cast<const std::uint8_t *>(rb.buf);
|
||||
std::copy(inbuf, inbuf + PNG_SIG_BYTES, buf);
|
||||
#else
|
||||
auto buf = static_cast<png_const_bytep>(rb.buf);
|
||||
#endif
|
||||
|
||||
return rb.sz >= PNG_SIG_BYTES && !png_sig_cmp(buf, 0, PNG_SIG_BYTES);
|
||||
}
|
||||
|
||||
// Buffer read callback for libpng. It provides an allocated output buffer and
|
||||
// the amount of data it desires to read from the input.
|
||||
void png_read_callback(png_struct *png_ptr,
|
||||
png_bytep outBytes,
|
||||
png_size_t byteCountToRead)
|
||||
{
|
||||
// Retrieve our input buffer through the png_ptr
|
||||
auto reader = static_cast<IStream *>(png_get_io_ptr(png_ptr));
|
||||
|
||||
if (!reader || !reader->is_ok()) return;
|
||||
|
||||
reader->read(static_cast<std::uint8_t *>(outBytes), byteCountToRead);
|
||||
}
|
||||
|
||||
bool decode_png(IStream &in_buf, ImageGreyscale &out_img)
|
||||
{
|
||||
static const constexpr int PNG_SIG_BYTES = 8;
|
||||
|
||||
std::vector<png_byte> sig(PNG_SIG_BYTES, 0);
|
||||
in_buf.read(sig.data(), PNG_SIG_BYTES);
|
||||
if (!png_check_sig(sig.data(), PNG_SIG_BYTES))
|
||||
return false;
|
||||
|
||||
PNGDescr dsc;
|
||||
dsc.png = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr,
|
||||
nullptr);
|
||||
|
||||
if(!dsc.png) return false;
|
||||
|
||||
dsc.info = png_create_info_struct(dsc.png);
|
||||
if(!dsc.info) return false;
|
||||
|
||||
png_set_read_fn(dsc.png, static_cast<void *>(&in_buf), png_read_callback);
|
||||
|
||||
// Tell that we have already read the first bytes to check the signature
|
||||
png_set_sig_bytes(dsc.png, PNG_SIG_BYTES);
|
||||
|
||||
png_read_info(dsc.png, dsc.info);
|
||||
|
||||
out_img.cols = png_get_image_width(dsc.png, dsc.info);
|
||||
out_img.rows = png_get_image_height(dsc.png, dsc.info);
|
||||
size_t color_type = png_get_color_type(dsc.png, dsc.info);
|
||||
size_t bit_depth = png_get_bit_depth(dsc.png, dsc.info);
|
||||
|
||||
if (color_type != PNG_COLOR_TYPE_GRAY || bit_depth != 8)
|
||||
return false;
|
||||
|
||||
out_img.buf.resize(out_img.rows * out_img.cols);
|
||||
|
||||
auto readbuf = static_cast<png_bytep>(out_img.buf.data());
|
||||
for (size_t r = 0; r < out_img.rows; ++r)
|
||||
png_read_row(dsc.png, readbuf + r * out_img.cols, nullptr);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}} // namespace Slic3r::png
|
70
src/libslic3r/PNGRead.hpp
Normal file
70
src/libslic3r/PNGRead.hpp
Normal file
|
@ -0,0 +1,70 @@
|
|||
#ifndef PNGREAD_HPP
|
||||
#define PNGREAD_HPP
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <istream>
|
||||
|
||||
namespace Slic3r { namespace png {
|
||||
|
||||
// Interface for an input stream of encoded png image data.
|
||||
struct IStream {
|
||||
virtual ~IStream() = default;
|
||||
virtual size_t read(std::uint8_t *outp, size_t amount) = 0;
|
||||
virtual bool is_ok() const = 0;
|
||||
};
|
||||
|
||||
// The output format of decode_png: a 2D pixel matrix stored continuously row
|
||||
// after row (row major layout).
|
||||
template<class PxT> struct Image {
|
||||
std::vector<PxT> buf;
|
||||
size_t rows, cols;
|
||||
PxT get(size_t row, size_t col) const { return buf[row * cols + col]; }
|
||||
};
|
||||
|
||||
using ImageGreyscale = Image<uint8_t>;
|
||||
|
||||
// Only decodes true 8 bit grayscale png images. Returns false for other formats
|
||||
// TODO (if needed): implement transformation of rgb images into grayscale...
|
||||
bool decode_png(IStream &stream, ImageGreyscale &out_img);
|
||||
|
||||
// TODO (if needed)
|
||||
// struct RGB { uint8_t r, g, b; };
|
||||
// using ImageRGB = Image<RGB>;
|
||||
// bool decode_png(IStream &stream, ImageRGB &img);
|
||||
|
||||
|
||||
// Encoded png data buffer: a simple read-only buffer and its size.
|
||||
struct ReadBuf { const void *buf = nullptr; const size_t sz = 0; };
|
||||
|
||||
bool is_png(const ReadBuf &pngbuf);
|
||||
|
||||
template<class Img> bool decode_png(const ReadBuf &in_buf, Img &out_img)
|
||||
{
|
||||
struct ReadBufStream: public IStream {
|
||||
const ReadBuf &rbuf_ref; size_t pos = 0;
|
||||
|
||||
explicit ReadBufStream(const ReadBuf &buf): rbuf_ref{buf} {}
|
||||
|
||||
size_t read(std::uint8_t *outp, size_t amount) override
|
||||
{
|
||||
if (amount > rbuf_ref.sz - pos) return 0;
|
||||
|
||||
auto buf = static_cast<const std::uint8_t *>(rbuf_ref.buf);
|
||||
std::copy(buf + pos, buf + (pos + amount), outp);
|
||||
pos += amount;
|
||||
|
||||
return amount;
|
||||
}
|
||||
|
||||
bool is_ok() const override { return pos < rbuf_ref.sz; }
|
||||
} stream{in_buf};
|
||||
|
||||
return decode_png(stream, out_img);
|
||||
}
|
||||
|
||||
// TODO: std::istream of FILE* could be similarly adapted in case its needed...
|
||||
|
||||
}} // namespace Slic3r::png
|
||||
|
||||
#endif // PNGREAD_HPP
|
|
@ -128,12 +128,13 @@ protected:
|
|||
}
|
||||
|
||||
public:
|
||||
template<class GammaFn> AGGRaster(const Resolution &res,
|
||||
template<class GammaFn>
|
||||
AGGRaster(const Resolution &res,
|
||||
const PixelDim & pd,
|
||||
const Trafo & trafo,
|
||||
const TColor & foreground,
|
||||
const TColor & background,
|
||||
GammaFn && gammafn)
|
||||
const TColor & foreground,
|
||||
const TColor & background,
|
||||
GammaFn && gammafn)
|
||||
: m_resolution(res)
|
||||
, m_pxdim_scaled(SCALING_FACTOR / pd.w_mm, SCALING_FACTOR / pd.h_mm)
|
||||
, m_buf(res.pixels())
|
||||
|
|
|
@ -193,8 +193,6 @@ set(SLIC3R_GUI_SOURCES
|
|||
Utils/HexFile.cpp
|
||||
Utils/HexFile.hpp
|
||||
Utils/Thread.hpp
|
||||
Utils/SLAImport.hpp
|
||||
Utils/SLAImport.cpp
|
||||
)
|
||||
|
||||
if (APPLE)
|
||||
|
|
|
@ -2209,7 +2209,7 @@ void ObjectList::load_shape_object(const std::string& type_name)
|
|||
load_mesh_object(mesh, _(L("Shape")) + "-" + _(type_name));
|
||||
}
|
||||
|
||||
void ObjectList::load_mesh_object(const TriangleMesh &mesh, const wxString &name)
|
||||
void ObjectList::load_mesh_object(const TriangleMesh &mesh, const wxString &name, bool center)
|
||||
{
|
||||
// Add mesh to model as a new object
|
||||
Model& model = wxGetApp().plater()->model();
|
||||
|
@ -2219,6 +2219,7 @@ void ObjectList::load_mesh_object(const TriangleMesh &mesh, const wxString &name
|
|||
#endif /* _DEBUG */
|
||||
|
||||
std::vector<size_t> object_idxs;
|
||||
auto bb = mesh.bounding_box();
|
||||
ModelObject* new_object = model.add_object();
|
||||
new_object->name = into_u8(name);
|
||||
new_object->add_instance(); // each object should have at list one instance
|
||||
|
@ -2228,13 +2229,17 @@ void ObjectList::load_mesh_object(const TriangleMesh &mesh, const wxString &name
|
|||
// set a default extruder value, since user can't add it manually
|
||||
new_volume->config.set_key_value("extruder", new ConfigOptionInt(0));
|
||||
new_object->invalidate_bounding_box();
|
||||
|
||||
new_object->center_around_origin();
|
||||
new_object->translate(-bb.center());
|
||||
|
||||
if (center) {
|
||||
const BoundingBoxf bed_shape = wxGetApp().plater()->bed_shape_bb();
|
||||
new_object->instances[0]->set_offset(Slic3r::to_3d(bed_shape.center().cast<double>(), -new_object->origin_translation(2)));
|
||||
} else {
|
||||
new_object->instances[0]->set_offset(bb.center());
|
||||
}
|
||||
|
||||
new_object->ensure_on_bed();
|
||||
|
||||
const BoundingBoxf bed_shape = wxGetApp().plater()->bed_shape_bb();
|
||||
new_object->instances[0]->set_offset(Slic3r::to_3d(bed_shape.center().cast<double>(), -new_object->origin_translation(2)));
|
||||
|
||||
|
||||
object_idxs.push_back(model.objects.size() - 1);
|
||||
#ifdef _DEBUG
|
||||
check_model_ids_validity(model);
|
||||
|
|
|
@ -294,7 +294,7 @@ public:
|
|||
void load_part(ModelObject* model_object, std::vector<std::pair<wxString, bool>> &volumes_info, ModelVolumeType type);
|
||||
void load_generic_subobject(const std::string& type_name, const ModelVolumeType type);
|
||||
void load_shape_object(const std::string &type_name);
|
||||
void load_mesh_object(const TriangleMesh &mesh, const wxString &name);
|
||||
void load_mesh_object(const TriangleMesh &mesh, const wxString &name, bool center = true);
|
||||
void del_object(const int obj_idx);
|
||||
void del_subobject_item(wxDataViewItem& item);
|
||||
void del_settings_from_config(const wxDataViewItem& parent_item);
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
#include "SLAImportJob.hpp"
|
||||
|
||||
#include "libslic3r/Format/SL1.hpp"
|
||||
|
||||
#include "slic3r/GUI/GUI.hpp"
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
#include "slic3r/GUI/Plater.hpp"
|
||||
#include "slic3r/GUI/GUI_ObjectList.hpp"
|
||||
#include "slic3r/Utils/SLAImport.hpp"
|
||||
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/PresetBundle.hpp"
|
||||
|
@ -218,8 +219,10 @@ void SLAImportJob::finalize()
|
|||
wxGetApp().load_current_presets();
|
||||
}
|
||||
|
||||
if (!p->mesh.empty())
|
||||
p->plater->sidebar().obj_list()->load_mesh_object(p->mesh, name);
|
||||
if (!p->mesh.empty()) {
|
||||
bool is_centered = false;
|
||||
p->plater->sidebar().obj_list()->load_mesh_object(p->mesh, name, is_centered);
|
||||
}
|
||||
|
||||
reset();
|
||||
}
|
||||
|
|
|
@ -1,314 +0,0 @@
|
|||
#include "SLAImport.hpp"
|
||||
|
||||
#include <sstream>
|
||||
|
||||
#include "libslic3r/SlicesToTriangleMesh.hpp"
|
||||
#include "libslic3r/MarchingSquares.hpp"
|
||||
#include "libslic3r/ClipperUtils.hpp"
|
||||
#include "libslic3r/MTUtils.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include "libslic3r/SLA/RasterBase.hpp"
|
||||
#include "libslic3r/miniz_extension.hpp"
|
||||
|
||||
#include <boost/property_tree/ini_parser.hpp>
|
||||
#include <boost/filesystem/path.hpp>
|
||||
#include <boost/algorithm/string.hpp>
|
||||
|
||||
#include <wx/image.h>
|
||||
#include <wx/mstream.h>
|
||||
|
||||
namespace marchsq {
|
||||
|
||||
// Specialize this struct to register a raster type for the Marching squares alg
|
||||
template<> struct _RasterTraits<wxImage> {
|
||||
using Rst = wxImage;
|
||||
|
||||
// The type of pixel cell in the raster
|
||||
using ValueType = uint8_t;
|
||||
|
||||
// Value at a given position
|
||||
static uint8_t get(const Rst &rst, size_t row, size_t col)
|
||||
{
|
||||
return rst.GetRed(col, row);
|
||||
}
|
||||
|
||||
// Number of rows and cols of the raster
|
||||
static size_t rows(const Rst &rst) { return rst.GetHeight(); }
|
||||
static size_t cols(const Rst &rst) { return rst.GetWidth(); }
|
||||
};
|
||||
|
||||
} // namespace marchsq
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
namespace {
|
||||
|
||||
struct ArchiveData {
|
||||
boost::property_tree::ptree profile, config;
|
||||
std::vector<sla::EncodedRaster> images;
|
||||
};
|
||||
|
||||
static const constexpr char *CONFIG_FNAME = "config.ini";
|
||||
static const constexpr char *PROFILE_FNAME = "prusaslicer.ini";
|
||||
|
||||
boost::property_tree::ptree read_ini(const mz_zip_archive_file_stat &entry,
|
||||
MZ_Archive & zip)
|
||||
{
|
||||
std::string buf(size_t(entry.m_uncomp_size), '\0');
|
||||
|
||||
if (!mz_zip_reader_extract_file_to_mem(&zip.arch, entry.m_filename,
|
||||
buf.data(), buf.size(), 0))
|
||||
throw std::runtime_error(zip.get_errorstr());
|
||||
|
||||
boost::property_tree::ptree tree;
|
||||
std::stringstream ss(buf);
|
||||
boost::property_tree::read_ini(ss, tree);
|
||||
return tree;
|
||||
}
|
||||
|
||||
sla::EncodedRaster read_png(const mz_zip_archive_file_stat &entry,
|
||||
MZ_Archive & zip,
|
||||
const std::string & name)
|
||||
{
|
||||
std::vector<uint8_t> buf(entry.m_uncomp_size);
|
||||
|
||||
if (!mz_zip_reader_extract_file_to_mem(&zip.arch, entry.m_filename,
|
||||
buf.data(), buf.size(), 0))
|
||||
throw std::runtime_error(zip.get_errorstr());
|
||||
|
||||
return sla::EncodedRaster(std::move(buf),
|
||||
name.empty() ? entry.m_filename : name);
|
||||
}
|
||||
|
||||
ArchiveData extract_sla_archive(const std::string &zipfname,
|
||||
const std::string &exclude)
|
||||
{
|
||||
ArchiveData arch;
|
||||
|
||||
// Little RAII
|
||||
struct Arch: public MZ_Archive {
|
||||
Arch(const std::string &fname) {
|
||||
if (!open_zip_reader(&arch, fname))
|
||||
throw std::runtime_error(get_errorstr());
|
||||
}
|
||||
|
||||
~Arch() { close_zip_reader(&arch); }
|
||||
} zip (zipfname);
|
||||
|
||||
mz_uint num_entries = mz_zip_reader_get_num_files(&zip.arch);
|
||||
|
||||
for (mz_uint i = 0; i < num_entries; ++i)
|
||||
{
|
||||
mz_zip_archive_file_stat entry;
|
||||
|
||||
if (mz_zip_reader_file_stat(&zip.arch, i, &entry))
|
||||
{
|
||||
std::string name = entry.m_filename;
|
||||
boost::algorithm::to_lower(name);
|
||||
|
||||
if (boost::algorithm::contains(name, exclude)) continue;
|
||||
|
||||
if (name == CONFIG_FNAME) arch.config = read_ini(entry, zip);
|
||||
if (name == PROFILE_FNAME) arch.profile = read_ini(entry, zip);
|
||||
|
||||
if (boost::filesystem::path(name).extension().string() == ".png") {
|
||||
auto it = std::lower_bound(
|
||||
arch.images.begin(), arch.images.end(), sla::EncodedRaster({}, name),
|
||||
[](const sla::EncodedRaster &r1, const sla::EncodedRaster &r2) {
|
||||
return std::less<std::string>()(r1.extension(), r2.extension());
|
||||
});
|
||||
|
||||
arch.images.insert(it, read_png(entry, zip, name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return arch;
|
||||
}
|
||||
|
||||
ExPolygons rings_to_expolygons(const std::vector<marchsq::Ring> &rings,
|
||||
double px_w, double px_h)
|
||||
{
|
||||
ExPolygons polys; polys.reserve(rings.size());
|
||||
|
||||
for (const marchsq::Ring &ring : rings) {
|
||||
Polygon poly; Points &pts = poly.points;
|
||||
pts.reserve(ring.size());
|
||||
|
||||
for (const marchsq::Coord &crd : ring)
|
||||
pts.emplace_back(scaled(crd.c * px_w), scaled(crd.r * px_h));
|
||||
|
||||
polys.emplace_back(poly);
|
||||
}
|
||||
|
||||
// reverse the raster transformations
|
||||
return union_ex(polys);
|
||||
}
|
||||
|
||||
template<class Fn> void foreach_vertex(ExPolygon &poly, Fn &&fn)
|
||||
{
|
||||
for (auto &p : poly.contour.points) fn(p);
|
||||
for (auto &h : poly.holes)
|
||||
for (auto &p : h.points) fn(p);
|
||||
}
|
||||
|
||||
void invert_raster_trafo(ExPolygons & expolys,
|
||||
const sla::RasterBase::Trafo &trafo,
|
||||
coord_t width,
|
||||
coord_t height)
|
||||
{
|
||||
for (auto &expoly : expolys) {
|
||||
if (trafo.mirror_y)
|
||||
foreach_vertex(expoly, [height](Point &p) {p.y() = height - p.y(); });
|
||||
|
||||
if (trafo.mirror_x)
|
||||
foreach_vertex(expoly, [width](Point &p) {p.x() = width - p.x(); });
|
||||
|
||||
expoly.translate(-trafo.center_x, -trafo.center_y);
|
||||
|
||||
if (trafo.flipXY)
|
||||
foreach_vertex(expoly, [](Point &p) { std::swap(p.x(), p.y()); });
|
||||
|
||||
if ((trafo.mirror_x + trafo.mirror_y + trafo.flipXY) % 2) {
|
||||
expoly.contour.reverse();
|
||||
for (auto &h : expoly.holes) h.reverse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RasterParams {
|
||||
sla::RasterBase::Trafo trafo; // Raster transformations
|
||||
coord_t width, height; // scaled raster dimensions (not resolution)
|
||||
double px_h, px_w; // pixel dimesions
|
||||
marchsq::Coord win; // marching squares window size
|
||||
};
|
||||
|
||||
RasterParams get_raster_params(const DynamicPrintConfig &cfg)
|
||||
{
|
||||
auto *opt_disp_cols = cfg.option<ConfigOptionInt>("display_pixels_x");
|
||||
auto *opt_disp_rows = cfg.option<ConfigOptionInt>("display_pixels_y");
|
||||
auto *opt_disp_w = cfg.option<ConfigOptionFloat>("display_width");
|
||||
auto *opt_disp_h = cfg.option<ConfigOptionFloat>("display_height");
|
||||
auto *opt_mirror_x = cfg.option<ConfigOptionBool>("display_mirror_x");
|
||||
auto *opt_mirror_y = cfg.option<ConfigOptionBool>("display_mirror_y");
|
||||
auto *opt_orient = cfg.option<ConfigOptionEnum<SLADisplayOrientation>>("display_orientation");
|
||||
|
||||
if (!opt_disp_cols || !opt_disp_rows || !opt_disp_w || !opt_disp_h ||
|
||||
!opt_mirror_x || !opt_mirror_y || !opt_orient)
|
||||
throw std::runtime_error("Invalid SL1 file");
|
||||
|
||||
RasterParams rstp;
|
||||
|
||||
rstp.px_w = opt_disp_w->value / (opt_disp_cols->value - 1);
|
||||
rstp.px_h = opt_disp_h->value / (opt_disp_rows->value - 1);
|
||||
|
||||
sla::RasterBase::Trafo trafo{opt_orient->value == sladoLandscape ?
|
||||
sla::RasterBase::roLandscape :
|
||||
sla::RasterBase::roPortrait,
|
||||
{opt_mirror_x->value, opt_mirror_y->value}};
|
||||
|
||||
rstp.height = scaled(opt_disp_h->value);
|
||||
rstp.width = scaled(opt_disp_w->value);
|
||||
|
||||
return rstp;
|
||||
}
|
||||
|
||||
struct SliceParams { double layerh = 0., initial_layerh = 0.; };
|
||||
|
||||
SliceParams get_slice_params(const DynamicPrintConfig &cfg)
|
||||
{
|
||||
auto *opt_layerh = cfg.option<ConfigOptionFloat>("layer_height");
|
||||
auto *opt_init_layerh = cfg.option<ConfigOptionFloat>("initial_layer_height");
|
||||
|
||||
if (!opt_layerh || !opt_init_layerh)
|
||||
throw std::runtime_error("Invalid SL1 file");
|
||||
|
||||
return SliceParams{opt_layerh->getFloat(), opt_init_layerh->getFloat()};
|
||||
}
|
||||
|
||||
std::vector<ExPolygons> extract_slices_from_sla_archive(
|
||||
ArchiveData & arch,
|
||||
const RasterParams & rstp,
|
||||
std::function<bool(int)> progr)
|
||||
{
|
||||
auto jobdir = arch.config.get<std::string>("jobDir");
|
||||
for (auto &c : jobdir) c = std::tolower(c);
|
||||
|
||||
std::vector<ExPolygons> slices(arch.images.size());
|
||||
|
||||
struct Status
|
||||
{
|
||||
double incr, val, prev;
|
||||
bool stop = false;
|
||||
tbb::spin_mutex mutex;
|
||||
} st {100. / slices.size(), 0., 0.};
|
||||
|
||||
tbb::parallel_for(size_t(0), arch.images.size(),
|
||||
[&arch, &slices, &st, &rstp, progr](size_t i) {
|
||||
// Status indication guarded with the spinlock
|
||||
{
|
||||
std::lock_guard<tbb::spin_mutex> lck(st.mutex);
|
||||
if (st.stop) return;
|
||||
|
||||
st.val += st.incr;
|
||||
double curr = std::round(st.val);
|
||||
if (curr > st.prev) {
|
||||
st.prev = curr;
|
||||
st.stop = !progr(int(curr));
|
||||
}
|
||||
}
|
||||
|
||||
auto &buf = arch.images[i];
|
||||
wxMemoryInputStream stream{buf.data(), buf.size()};
|
||||
wxImage img{stream};
|
||||
|
||||
auto rings = marchsq::execute(img, 128, rstp.win);
|
||||
ExPolygons expolys = rings_to_expolygons(rings, rstp.px_w, rstp.px_h);
|
||||
|
||||
// Invert the raster transformations indicated in
|
||||
// the profile metadata
|
||||
invert_raster_trafo(expolys, rstp.trafo, rstp.width, rstp.height);
|
||||
|
||||
slices[i] = std::move(expolys);
|
||||
});
|
||||
|
||||
if (st.stop) slices = {};
|
||||
|
||||
return slices;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void import_sla_archive(const std::string &zipfname, DynamicPrintConfig &out)
|
||||
{
|
||||
ArchiveData arch = extract_sla_archive(zipfname, "png");
|
||||
out.load(arch.profile);
|
||||
}
|
||||
|
||||
void import_sla_archive(
|
||||
const std::string & zipfname,
|
||||
Vec2i windowsize,
|
||||
TriangleMesh & out,
|
||||
DynamicPrintConfig & profile,
|
||||
std::function<bool(int)> progr)
|
||||
{
|
||||
// Ensure minimum window size for marching squares
|
||||
windowsize.x() = std::max(2, windowsize.x());
|
||||
windowsize.y() = std::max(2, windowsize.y());
|
||||
|
||||
ArchiveData arch = extract_sla_archive(zipfname, "thumbnail");
|
||||
profile.load(arch.profile);
|
||||
|
||||
RasterParams rstp = get_raster_params(profile);
|
||||
rstp.win = {windowsize.y(), windowsize.x()};
|
||||
|
||||
SliceParams slicp = get_slice_params(profile);
|
||||
|
||||
std::vector<ExPolygons> slices =
|
||||
extract_slices_from_sla_archive(arch, rstp, progr);
|
||||
|
||||
if (!slices.empty())
|
||||
out = slices_to_triangle_mesh(slices, 0, slicp.layerh, slicp.initial_layerh);
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
|
@ -1,35 +0,0 @@
|
|||
#ifndef SLAIMPORT_HPP
|
||||
#define SLAIMPORT_HPP
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include <libslic3r/Point.hpp>
|
||||
#include <libslic3r/PrintConfig.hpp>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class TriangleMesh;
|
||||
class DynamicPrintConfig;
|
||||
|
||||
void import_sla_archive(const std::string &zipfname, DynamicPrintConfig &out);
|
||||
|
||||
void import_sla_archive(
|
||||
const std::string & zipfname,
|
||||
Vec2i windowsize,
|
||||
TriangleMesh & out,
|
||||
DynamicPrintConfig & profile,
|
||||
std::function<bool(int)> progr = [](int) { return true; });
|
||||
|
||||
inline void import_sla_archive(
|
||||
const std::string & zipfname,
|
||||
Vec2i windowsize,
|
||||
TriangleMesh & out,
|
||||
std::function<bool(int)> progr = [](int) { return true; })
|
||||
{
|
||||
DynamicPrintConfig profile;
|
||||
import_sla_archive(zipfname, windowsize, out, profile, progr);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif // SLAIMPORT_HPP
|
|
@ -17,6 +17,8 @@ add_executable(${_TEST_NAME}_tests
|
|||
test_marchingsquares.cpp
|
||||
test_timeutils.cpp
|
||||
test_voronoi.cpp
|
||||
test_png_io.cpp
|
||||
test_timeutils.cpp
|
||||
)
|
||||
|
||||
if (TARGET OpenVDB::openvdb)
|
||||
|
|
55
tests/libslic3r/test_png_io.cpp
Normal file
55
tests/libslic3r/test_png_io.cpp
Normal file
|
@ -0,0 +1,55 @@
|
|||
#define NOMINMAX
|
||||
#include <catch2/catch.hpp>
|
||||
|
||||
#include <numeric>
|
||||
|
||||
#include "libslic3r/PNGRead.hpp"
|
||||
#include "libslic3r/SLA/AGGRaster.hpp"
|
||||
#include "libslic3r/BoundingBox.hpp"
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
static sla::RasterGrayscaleAA create_raster(const sla::RasterBase::Resolution &res)
|
||||
{
|
||||
sla::RasterBase::PixelDim pixdim{1., 1.};
|
||||
|
||||
auto bb = BoundingBox({0, 0}, {scaled(1.), scaled(1.)});
|
||||
sla::RasterBase::Trafo trafo;
|
||||
trafo.center_x = bb.center().x();
|
||||
trafo.center_y = bb.center().y();
|
||||
|
||||
return sla::RasterGrayscaleAA{res, pixdim, trafo, agg::gamma_threshold(.5)};
|
||||
}
|
||||
|
||||
TEST_CASE("PNG read", "[PNG]") {
|
||||
auto rst = create_raster({100, 100});
|
||||
|
||||
size_t rstsum = 0;
|
||||
for (size_t r = 0; r < rst.resolution().height_px; ++r)
|
||||
for (size_t c = 0; c < rst.resolution().width_px; ++c)
|
||||
rstsum += rst.read_pixel(c, r);
|
||||
|
||||
SECTION("Correct png buffer should be recognized as such.") {
|
||||
auto enc_rst = rst.encode(sla::PNGRasterEncoder{});
|
||||
REQUIRE(Slic3r::png::is_png({enc_rst.data(), enc_rst.size()}));
|
||||
}
|
||||
|
||||
SECTION("Fake png buffer should be recognized as such.") {
|
||||
std::vector<uint8_t> fake(10, '\0');
|
||||
REQUIRE(!Slic3r::png::is_png({fake.data(), fake.size()}));
|
||||
}
|
||||
|
||||
SECTION("Decoded PNG buffer resolution should match the original") {
|
||||
auto enc_rst = rst.encode(sla::PNGRasterEncoder{});
|
||||
|
||||
png::ImageGreyscale img;
|
||||
png::decode_png({enc_rst.data(), enc_rst.size()}, img);
|
||||
|
||||
REQUIRE(img.rows == rst.resolution().height_px);
|
||||
REQUIRE(img.cols == rst.resolution().width_px);
|
||||
|
||||
size_t sum = std::accumulate(img.buf.begin(), img.buf.end(), size_t(0));
|
||||
|
||||
REQUIRE(sum == rstsum);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue