Pack bed texture and model into printer bundles (#15965)

* feat(preset): pack bed texture and model into printer bundles

Export copies bed_custom_texture and bed_custom_model into the .orca_printer zip under bed/. The manifest lists them in printer_asset. Import extracts each file into the bundle folder and rewrites the preset to that path. A missing file is skipped. A path that leaves the bundle directory is rejected. An old bundle with no printer_asset imports as before.

* Add includes flagged by clang-tidy misc-include-cleaner

* Gate bed asset packing on an export checkbox and key printer_asset per printer

The Export Configs checkbox can leave a custom bed out of a shared printer bundle. printer_asset is keyed by each printer_config zip entry, with files under bed/<n>/, so a later multi-printer bundle does not need a second manifest. The unshipped flat printer_asset object is not read.

---------

Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
This commit is contained in:
Klober81
2026-10-11 13:32:07 +08:00
committed by GitHub
co-authored by Rodrigo Faselli
parent 210bc927bc
commit 295094a594
7 changed files with 974 additions and 6 deletions
+1
View File
@@ -405,6 +405,7 @@ set(lisbslic3r_sources
Polyline.hpp
PresetBundle.cpp
PresetBundle.hpp
PrinterBedAssets.hpp
PresetCacheFormat.cpp
PresetCacheFormat.hpp
Preset.cpp
+308 -2
View File
@@ -18,6 +18,7 @@
#include <cstdint>
#include <cmath>
#include <ctime>
#include <ios>
#include <optional>
#include <exception>
#include <memory>
@@ -29,6 +30,7 @@
#include <sstream>
#include "PresetBundle.hpp"
#include "PrinterBedAssets.hpp"
#include "Config.hpp"
#include "Exception.hpp"
@@ -64,6 +66,7 @@
#include <boost/algorithm/clamp.hpp>
#include <boost/algorithm/string/predicate.hpp>
#include <boost/range/adaptor/transformed.hpp>
#include <boost/nowide/convert.hpp>
#include <boost/nowide/cstdio.hpp>
#include <boost/nowide/fstream.hpp>
#include <boost/property_tree/ini_parser.hpp>
@@ -1678,6 +1681,258 @@ bool PresetBundle::apply_vendor_config(
return true;
}
namespace {
// The two bed files a printer bundle can carry. Zip names are bed/<n>/<stem><ext>, never the source basename.
struct BedAssetSlot {
const char *option;
const char *stem;
};
constexpr BedAssetSlot k_bed_asset_slots[] = {
{"bed_custom_texture", "texture"},
{"bed_custom_model", "model"},
};
std::string zip_slashes(std::string name)
{
for (char &c : name)
if (c == '\\')
c = '/';
return name;
}
// The bed dialog loads only png/svg textures and stl models. Anything else is not a bed asset.
bool bed_extension_allowed(const char *option, const std::string &ext)
{
if (std::string(option) == "bed_custom_texture")
return boost::iequals(ext, ".png") || boost::iequals(ext, ".svg");
if (std::string(option) == "bed_custom_model")
return boost::iequals(ext, ".stl");
return false;
}
// Extension including the dot, or empty when this option may not use it.
std::string bed_asset_extension(const std::string &path, const char *option)
{
const auto slash = path.find_last_of("/\\");
const auto base = slash == std::string::npos ? 0 : slash + 1;
const auto dot = path.find_last_of('.');
if (dot == std::string::npos || dot < base || dot + 1 >= path.size())
return {};
const std::string ext = path.substr(dot);
if (!bed_extension_allowed(option, ext))
return {};
return ext;
}
bool utf8_is_file(const std::string &path)
{
if (path.empty())
return false;
boost::system::error_code ec;
#ifdef _WIN32
// A narrow path is the ANSI code page unless nowide_filesystem() has run.
const std::wstring wide = boost::nowide::widen(path);
if (wide.empty())
return false;
return boost::filesystem::is_regular_file(boost::filesystem::path(wide), ec) && !ec;
#else
return boost::filesystem::is_regular_file(path, ec) && !ec;
#endif
}
std::string bed_asset_dest_utf8(const fs::path &dest)
{
#ifdef _WIN32
return boost::nowide::narrow(dest.wstring());
#else
return dest.string();
#endif
}
// True only for bed/<digits>/<stem><ext> with an extension the bed dialog accepts for this field.
bool accepted_bed_zip_name(const std::string &raw, const BedAssetSlot &slot, const fs::path &bundle_dir,
std::string &zip_name, std::string &folder)
{
const std::string name = zip_slashes(raw);
if (name.find("..") != std::string::npos)
return false;
if (name.size() < 8 || name.compare(0, 4, "bed/") != 0)
return false;
const auto slash = name.find('/', 4);
if (slash == std::string::npos || slash == 4 || name.find('/', slash + 1) != std::string::npos)
return false;
const std::string digits = name.substr(4, slash - 4);
for (unsigned char c : digits) {
if (!std::isdigit(c))
return false;
}
const std::string filename = name.substr(slash + 1);
const std::string ext = bed_asset_extension(filename, slot.option);
if (ext.empty() || filename != std::string(slot.stem) + ext)
return false;
if (!is_path_within_root(name, bundle_dir))
return false;
zip_name = name;
folder = digits;
return true;
}
struct PendingBedAsset {
std::string option;
std::string zip_name;
std::string dest;
std::string printer_key;
fs::path final_path;
fs::path staged;
};
// Validated asset names only. A raw manifest value that fails the check is not skipped later,
// so a value like printer/Name.json cannot hide that preset.
// printer_asset is keyed by printer_config zip entry. A flat (unkeyed) object is not read.
std::vector<PendingBedAsset> pending_bed_assets(const json &bundle_json, const fs::path &bundle_dir,
std::unordered_set<std::string> &skip_names,
const std::unordered_set<std::string> &zip_entries)
{
std::vector<PendingBedAsset> pending;
if (!bundle_json.contains("printer_asset") || !bundle_json["printer_asset"].is_object())
return pending;
const json &assets = bundle_json["printer_asset"];
for (auto it = assets.begin(); it != assets.end(); ++it) {
const std::string printer_key = it.key();
if (zip_entries.count(printer_key) == 0) {
BOOST_LOG_TRIVIAL(warning) << "Printer bundle asset entry is ignored, no matching printer in the zip: " << printer_key;
continue;
}
if (!it.value().is_object())
continue;
const json &per_printer = it.value();
for (const BedAssetSlot &slot : k_bed_asset_slots) {
if (!per_printer.contains(slot.option) || !per_printer[slot.option].is_string())
continue;
const std::string raw = per_printer[slot.option].get<std::string>();
std::string zip_name;
std::string folder;
if (!accepted_bed_zip_name(raw, slot, bundle_dir, zip_name, folder)) {
BOOST_LOG_TRIVIAL(warning) << "Printer bundle asset path is rejected: " << raw;
continue;
}
const std::string filename = zip_name.substr(zip_name.rfind('/') + 1);
const fs::path final_path = fs::absolute(bundle_dir / "bed" / folder / filename);
pending.push_back({slot.option, zip_name, bed_asset_dest_utf8(final_path), printer_key, final_path, {}});
skip_names.insert(zip_name);
}
}
return pending;
}
// Stage under temp_folder. The bundle bed/ directory is not touched until a printer preset keeps the path.
bool stage_bed_asset(mz_zip_archive &zip, PendingBedAsset &asset, const fs::path &temp_folder)
{
size_t size = 0;
void *buf = mz_zip_reader_extract_file_to_heap(&zip, asset.zip_name.c_str(), &size, MZ_ZIP_FLAG_CASE_SENSITIVE);
if (buf == nullptr) {
BOOST_LOG_TRIVIAL(warning) << "Printer bundle asset is missing, skipping " << asset.option << ": " << asset.zip_name;
return false;
}
const std::string filename = asset.zip_name.substr(asset.zip_name.rfind('/') + 1);
asset.staged = temp_folder / "bed" / asset.final_path.parent_path().filename() / filename;
boost::system::error_code ec;
fs::create_directories(asset.staged.parent_path(), ec);
const std::string staged_utf8 = bed_asset_dest_utf8(asset.staged);
bool wrote = false;
if (!ec) {
boost::nowide::ofstream out(staged_utf8, std::ios::binary | std::ios::trunc);
wrote = static_cast<bool>(out);
if (wrote && size > 0)
wrote = static_cast<bool>(out.write(static_cast<const char *>(buf), static_cast<std::streamsize>(size)));
}
mz_free(buf);
if (!wrote) {
BOOST_LOG_TRIVIAL(warning) << "Printer bundle asset could not be written, skipping " << asset.option << ": " << staged_utf8;
fs::remove(asset.staged, ec);
return false;
}
return true;
}
bool install_bed_asset(const PendingBedAsset &asset)
{
boost::system::error_code ec;
fs::create_directories(asset.final_path.parent_path(), ec);
if (!ec)
fs::copy_file(asset.staged, asset.final_path, fs::copy_options::overwrite_existing, ec);
if (ec) {
BOOST_LOG_TRIVIAL(warning) << "Printer bundle asset could not be installed: " << asset.dest;
return false;
}
return true;
}
bool is_pending_bed_asset_entry(const std::string &entry, const std::unordered_set<std::string> &names)
{
return names.count(entry) != 0 || names.count(zip_slashes(entry)) != 0;
}
bool printer_kept_bed_paths(const PresetCollection &printers, const std::map<std::string, std::string> &paths)
{
if (paths.empty())
return false;
for (const Preset &preset : printers.get_presets()) {
for (const auto &entry : paths)
if (preset.config.opt_string(entry.first) == entry.second)
return true;
}
return false;
}
} // namespace
bool append_printer_bed_assets(mz_zip_archive &zip, const DynamicPrintConfig &config, json &bundle_structure,
const std::string &printer_config_entry, int printer_index)
{
if (printer_config_entry.empty() || printer_index < 0)
return true;
json assets = json::object();
for (const BedAssetSlot &slot : k_bed_asset_slots) {
if (!config.has(slot.option))
continue;
const std::string stored = config.opt_string(slot.option);
if (stored.empty())
continue;
if (!utf8_is_file(stored)) {
BOOST_LOG_TRIVIAL(warning) << "Bed asset file is missing, skipping " << slot.option << ": " << stored;
continue;
}
const std::string ext = bed_asset_extension(stored, slot.option);
if (ext.empty()) {
BOOST_LOG_TRIVIAL(warning) << "Bed asset extension is not packable, skipping " << slot.option << ": " << stored;
continue;
}
boost::nowide::ifstream in(stored, std::ios::binary);
if (!in) {
BOOST_LOG_TRIVIAL(warning) << "Bed asset file cannot be read, skipping " << slot.option << ": " << stored;
continue;
}
const std::string bytes((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
if (in.bad()) {
BOOST_LOG_TRIVIAL(warning) << "Bed asset file cannot be read, skipping " << slot.option << ": " << stored;
continue;
}
const std::string zip_name = std::string("bed/") + std::to_string(printer_index) + "/" + slot.stem + ext;
const void *data = bytes.empty() ? static_cast<const void *>("") : static_cast<const void *>(bytes.data());
if (mz_zip_writer_add_mem(&zip, zip_name.c_str(), data, bytes.size(), MZ_DEFAULT_COMPRESSION) == MZ_FALSE) {
BOOST_LOG_TRIVIAL(error) << "Failed to add bed asset to printer bundle: " << zip_name;
return false;
}
assets[slot.option] = zip_name;
}
if (!assets.empty())
bundle_structure["printer_asset"][printer_config_entry] = std::move(assets);
return true;
}
// Import presets from UI control
PresetsConfigSubstitutions PresetBundle::import_presets(std::vector<std::string> & files,
std::function<int(std::string const &)> override_confirm,
@@ -1773,14 +2028,44 @@ PresetsConfigSubstitutions PresetBundle::import_presets(std::vector<std::string>
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " No bundle_structure.json found, importing presets into the user preset directory";
}
json bundle_json = json::object();
std::unordered_set<std::string> bed_asset_entries;
std::vector<PendingBedAsset> bed_assets;
std::map<std::string, std::map<std::string, std::string>> bed_paths_by_printer;
bool bed_paths_loaded = false;
if (has_bundle_structure && !bundle_base_dir.empty()) {
try {
boost::nowide::ifstream manifest_in(metadata_path.string());
manifest_in >> bundle_json;
} catch (const std::exception &err) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " failed to read bundle manifest: " << err.what();
bundle_json = json::object();
}
std::unordered_set<std::string> zip_entries;
const int zip_file_count = mz_zip_reader_get_num_files(&zip_archive);
for (int zi = 0; zi < zip_file_count; ++zi) {
mz_zip_archive_file_stat st;
if (mz_zip_reader_file_stat(&zip_archive, zi, &st))
zip_entries.insert(zip_slashes(st.m_filename));
}
bed_assets = pending_bed_assets(bundle_json, bundle_base_dir, bed_asset_entries, zip_entries);
for (PendingBedAsset &asset : bed_assets)
if (stage_bed_asset(zip_archive, asset, temp_folder))
bed_paths_by_printer[asset.printer_key].emplace(asset.option, asset.dest);
}
// Extract Files
int num_files = mz_zip_reader_get_num_files(&zip_archive);
const std::map<std::string, std::string> no_bed_paths;
for (int i = 0; i < num_files; i++) {
mz_zip_archive_file_stat file_stat;
status = mz_zip_reader_file_stat(&zip_archive, i, &file_stat);
if (status) {
std::string file_name = file_stat.m_filename;
const std::string zip_entry = zip_slashes(file_name);
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " From zip file: " << file << ". Read file name: " << file_stat.m_filename;
if (is_pending_bed_asset_entry(file_name, bed_asset_entries))
continue;
size_t index = file_name.find_last_of("/\\");
if (std::string::npos != index) {
file_name = file_name.substr(index + 1);
@@ -1798,13 +2083,26 @@ PresetsConfigSubstitutions PresetBundle::import_presets(std::vector<std::string>
if (MZ_FALSE == status) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Failed to open target file: " << target_file_path;
} else {
const auto paths_it = bed_paths_by_printer.find(zip_entry);
const std::map<std::string, std::string> &this_bed_paths =
paths_it != bed_paths_by_printer.end() ? paths_it->second : no_bed_paths;
bool is_success = import_json_presets(substitutions, target_file_path, override_confirm, rule, overwrite, result,
has_bundle_structure ? bundle_base_dir.string() : std::string());
has_bundle_structure ? bundle_base_dir.string() : std::string(),
this_bed_paths, &bed_paths_loaded);
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " import target file: " << target_file_path << " import result" << is_success;
}
}
}
// Copy into bed/ only after this import loads a printer that still holds the staged path.
// A preset left from an earlier import must not count: declining this import leaves its files alone.
if (bed_paths_loaded) {
for (const PendingBedAsset &asset : bed_assets) {
if (printer_kept_bed_paths(printers, {{asset.option, asset.dest}}))
install_bed_asset(asset);
}
}
// Set imported_time to current time if not already set
if (metadata.imported_time == 0) {
metadata.imported_time = std::time(nullptr);
@@ -1846,7 +2144,9 @@ bool PresetBundle::import_json_presets(PresetsConfigSubstitutions & s
ForwardCompatibilitySubstitutionRule rule,
int & overwrite,
std::vector<std::string> & result,
const std::string & bundle_dir)
const std::string & bundle_dir,
const std::map<std::string, std::string> & bed_asset_paths,
bool * bed_paths_loaded)
{
try {
DynamicPrintConfig config;
@@ -1910,6 +2210,10 @@ bool PresetBundle::import_json_presets(PresetsConfigSubstitutions & s
if (inherit_preset)
inherits_value = inherit_preset->name; // keep the base_id redo below in sync
}
if (collection == &printers) {
for (const auto &entry : bed_asset_paths)
config.option<ConfigOptionString>(entry.first, true)->value = entry.second;
}
if (inherit_preset) {
new_config = inherit_preset->config;
new_config.apply(std::move(config));
@@ -1927,6 +2231,8 @@ bool PresetBundle::import_json_presets(PresetsConfigSubstitutions & s
new_config.apply(std::move(config));
extend_default_config_length(new_config, true, default_preset.config);
}
if (bed_paths_loaded != nullptr && collection == &printers && !bed_asset_paths.empty())
*bed_paths_loaded = true;
Preset &preset = collection->load_preset(collection->path_from_name(name, inherit_preset == nullptr), preset_name, std::move(new_config), false);
preset.bundle_id = load_origin.bundle_id;
+3 -1
View File
@@ -319,7 +319,9 @@ public:
ForwardCompatibilitySubstitutionRule rule,
int& overwrite,
std::vector<std::string>& result,
const std::string& bundle_dir = "");
const std::string& bundle_dir = "",
const std::map<std::string, std::string> &bed_asset_paths = {},
bool *bed_paths_loaded = nullptr);
void save_user_presets(AppConfig& config, std::map<std::string, std::string>& need_to_delete_list);
void check_and_fix_user_presets_syncinfo(const std::string& user_id);
+20
View File
@@ -0,0 +1,20 @@
#pragma once
#include "miniz_extension.hpp"
#include <miniz.h>
#include <nlohmann/json_fwd.hpp>
#include <string>
namespace Slic3r {
class DynamicPrintConfig;
// Not on PresetBundle.hpp: miniz.h's zlib-compat macros rename GUI methods (GLTexture::Compressor::compress).
// printer_config_entry is the zip name also pushed onto bundle_structure["printer_config"].
// printer_index is that entry's 0-based index in the exported printer_config list.
bool append_printer_bed_assets(mz_zip_archive &zip, const DynamicPrintConfig &config, nlohmann::json &bundle_structure,
const std::string &printer_config_entry, int printer_index);
} // namespace Slic3r
+32 -2
View File
@@ -65,6 +65,7 @@
#include <boost/nowide/cstdio.hpp>
#include <wx/valtext.h>
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/PrinterBedAssets.hpp"
#include "I18N.hpp"
#include "GUI_App.hpp"
#include "MsgDialog.hpp"
@@ -3674,9 +3675,20 @@ ExportConfigsDialog::~ExportConfigsDialog()
}
void ExportConfigsDialog::on_dpi_changed(const wxRect &suggested_rect) {
if (m_include_bed_assets)
m_include_bed_assets->Rescale();
Layout();
}
void ExportConfigsDialog::update_include_bed_assets_enabled()
{
const bool on = get_curr_radio_type(m_export_type_btns) == m_exprot_type.preset_bundle;
if (m_include_bed_assets)
m_include_bed_assets->Enable(on);
if (m_include_bed_assets_label)
m_include_bed_assets_label->Enable(on);
}
void ExportConfigsDialog::show_export_result(const ExportCase &export_case)
{
MessageDialog *msg_dlg = nullptr;
@@ -3819,6 +3831,17 @@ wxBoxSizer *ExportConfigsDialog::create_export_config_item(wxWindow *parent)
static_export_printer_preset_bundle_text->SetFont(Label::Body_12);
static_export_printer_preset_bundle_text->SetForegroundColour(wxColour("#6B6B6B"));
radioBoxSizer->Add(static_export_printer_preset_bundle_text, 0, wxEXPAND | wxLEFT, FromDIP(22));
wxBoxSizer *include_bed_sizer = new wxBoxSizer(wxHORIZONTAL);
m_include_bed_assets = new ::CheckBox(parent);
m_include_bed_assets->SetValue(true);
include_bed_sizer->Add(m_include_bed_assets, 0, wxALIGN_CENTER_VERTICAL, 0);
m_include_bed_assets_label = new wxStaticText(parent, wxID_ANY, _L("Include custom bed texture and model"));
m_include_bed_assets_label->SetFont(Label::Body_12);
m_include_bed_assets_label->SetForegroundColour(wxColour("#6B6B6B"));
include_bed_sizer->Add(m_include_bed_assets_label, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(5));
radioBoxSizer->Add(0, 0, 0, wxTOP, FromDIP(6));
radioBoxSizer->Add(include_bed_sizer, 0, wxEXPAND | wxLEFT, FromDIP(22));
update_include_bed_assets_enabled();
radioBoxSizer->Add(create_radio_item(m_exprot_type.filament_bundle, parent, wxEmptyString, m_export_type_btns), 0, wxEXPAND | wxTOP, FromDIP(10));
wxStaticText *static_export_filament_preset_bundle_text = new wxStaticText(parent, wxID_ANY, _L("User's filament preset set.\nCan be shared with others."),
wxDefaultPosition, wxDefaultSize);
@@ -3988,9 +4011,10 @@ void ExportConfigsDialog::select_curr_radiobox(std::vector<std::pair<RadioBox *,
radiobox_list[i].first->SetValue(false);
}
}
update_include_bed_assets_enabled();
}
ExportConfigsDialog::ExportCase ExportConfigsDialog::archive_preset_bundle_to_file(const wxString &path)
ExportConfigsDialog::ExportCase ExportConfigsDialog::archive_preset_bundle_to_file(const wxString &path, bool include_bed_assets)
{
std::string export_path = initial_file_path(path, "");
if (export_path.empty() || "initial_failed" == export_path) return ExportCase::EXPORT_CANCEL;
@@ -4092,6 +4116,12 @@ ExportConfigsDialog::ExportCase ExportConfigsDialog::archive_preset_bundle_to_fi
bundle_structure["filament_config"] = filament_configs;
bundle_structure["process_config"] = process_configs;
if (include_bed_assets &&
!append_printer_bed_assets(zip_archive, printer_preset->config, bundle_structure, printer_config_file_name, 0)) {
mz_zip_writer_end(&zip_archive);
return ExportCase::ADD_FILE_FAIL;
}
std::string bundle_structure_str = bundle_structure.dump();
status = mz_zip_writer_add_mem(&zip_archive, BUNDLE_STRUCTURE_JSON_NAME, bundle_structure_str.data(), bundle_structure_str.size(), MZ_DEFAULT_COMPRESSION);
if (MZ_FALSE == status) {
@@ -4339,7 +4369,7 @@ wxWindow *ExportConfigsDialog::create_dialog_buttons(wxWindow* parent)
const wxString curr_radio_type = get_curr_radio_type(m_export_type_btns);
if (curr_radio_type == m_exprot_type.preset_bundle) {
export_case = archive_preset_bundle_to_file(path);
export_case = archive_preset_bundle_to_file(path, m_include_bed_assets->GetValue());
} else if (curr_radio_type == m_exprot_type.filament_bundle) {
export_case = archive_filament_bundle_to_file(path);
} else if (curr_radio_type == m_exprot_type.printer_preset) {
+4 -1
View File
@@ -271,6 +271,7 @@ protected:
private:
void data_init();
void select_curr_radiobox(std::vector<std::pair<RadioBox *, wxString>> &radiobox_list, int btn_idx);
void update_include_bed_assets_enabled();
void on_dpi_changed(const wxRect &suggested_rect) override;
void show_export_result(const ExportCase &export_case);
bool has_check_box_selected();
@@ -284,7 +285,7 @@ private:
int initial_zip_archive(mz_zip_archive &zip_archive, const std::string &file_path);
ExportCase save_zip_archive_to_file(mz_zip_archive &zip_archive);
ExportCase save_presets_to_zip(const std::string &export_file, const std::vector<std::pair<std::string, std::string>> &config_paths);
ExportCase archive_preset_bundle_to_file(const wxString &path);
ExportCase archive_preset_bundle_to_file(const wxString &path, bool include_bed_assets);
ExportCase archive_filament_bundle_to_file(const wxString &path);
ExportCase archive_printer_preset_to_file(const wxString &path);
ExportCase archive_filament_preset_to_file(const wxString &path);
@@ -304,6 +305,8 @@ private:
wxGridSizer * m_preset_sizer = nullptr;
wxPanel * m_presets_window = nullptr;
wxStaticText * m_serial_text = nullptr;
::CheckBox * m_include_bed_assets = nullptr;
wxStaticText * m_include_bed_assets_label = nullptr;
};
class CreatePresetForPrinterDialog : public DPIDialog
@@ -6,6 +6,8 @@
#include <algorithm>
#include <boost/filesystem.hpp>
#include <boost/nowide/convert.hpp>
#include <boost/nowide/fstream.hpp>
#include <cstddef>
#include <fstream>
@@ -22,6 +24,7 @@
#include "libslic3r/TriangleMesh.hpp"
#include "libslic3r/Utils.hpp"
#include "libslic3r/miniz_extension.hpp"
#include "libslic3r/PrinterBedAssets.hpp"
#include "test_utils.hpp"
@@ -5853,6 +5856,609 @@ TEST_CASE("Config import confines zip entries, preset names and bundle ids to th
}
}
namespace {
const std::string kTextureBytes("\x89PNG\x00tex", 8);
const std::string kModelBytes("stl\x00model", 9);
const std::string kOtherBytes("\x89PNG\x00other", 10);
void write_bytes(const std::string &utf8_path, const std::string &bytes)
{
const auto slash = utf8_path.find_last_of("/\\");
if (slash != std::string::npos) {
#ifdef _WIN32
fs::create_directories(fs::path(boost::nowide::widen(utf8_path.substr(0, slash))));
#else
fs::create_directories(utf8_path.substr(0, slash));
#endif
}
boost::nowide::ofstream out(utf8_path, std::ios::binary | std::ios::trunc);
REQUIRE(out);
out.write(bytes.data(), static_cast<std::streamsize>(bytes.size()));
REQUIRE(out);
}
fs::path write_printer_json(const DynamicPrintConfig &defaults, const fs::path &file, const std::string &name,
const std::string &texture, const std::string &model, const std::string &inherits = {})
{
DynamicPrintConfig config(defaults);
config.option<ConfigOptionString>("printer_settings_id", true)->value = name;
config.option<ConfigOptionString>("inherits", true)->value = inherits;
if (texture.empty())
config.erase("bed_custom_texture");
else
config.option<ConfigOptionString>("bed_custom_texture", true)->value = texture;
if (model.empty())
config.erase("bed_custom_model");
else
config.option<ConfigOptionString>("bed_custom_model", true)->value = model;
fs::create_directories(file.parent_path());
config.save_to_json(file.string(), name, "User", "1.0.0");
return file;
}
nlohmann::json bed_manifest(const std::string &bundle_id)
{
nlohmann::json structure = nlohmann::json::object();
structure["id"] = bundle_id;
structure["bundle_type"] = "printer config bundle";
structure["printer_config"] = nlohmann::json::array({"printer/BedPrinter.json"});
return structure;
}
void set_printer_bed_asset(nlohmann::json &structure, const std::string &option, const std::string &zip_name,
const std::string &printer_entry = "printer/BedPrinter.json")
{
structure["printer_asset"][printer_entry][option] = zip_name;
}
nlohmann::json pack_bed_zip(const fs::path &zip_file, const fs::path &printer_json, const DynamicPrintConfig &defaults,
const std::string &texture, const std::string &model, const std::string &bundle_id,
bool include_bed_assets = true)
{
DynamicPrintConfig config(defaults);
if (!texture.empty())
config.option<ConfigOptionString>("bed_custom_texture", true)->value = texture;
if (!model.empty())
config.option<ConfigOptionString>("bed_custom_model", true)->value = model;
nlohmann::json structure = bed_manifest(bundle_id);
mz_zip_archive zip;
mz_zip_zero_struct(&zip);
const std::string printer_bytes = read_file(printer_json);
REQUIRE(open_zip_writer(&zip, zip_file.string()));
REQUIRE(mz_zip_writer_add_mem(&zip, "printer/BedPrinter.json", printer_bytes.data(), printer_bytes.size(),
MZ_DEFAULT_COMPRESSION));
if (include_bed_assets)
REQUIRE(append_printer_bed_assets(zip, config, structure, "printer/BedPrinter.json", 0));
const std::string manifest = structure.dump();
REQUIRE(mz_zip_writer_add_mem(&zip, BUNDLE_STRUCTURE_JSON_NAME, manifest.data(), manifest.size(),
MZ_DEFAULT_COMPRESSION));
REQUIRE(mz_zip_writer_finalize_archive(&zip));
REQUIRE(close_zip_writer(&zip));
return structure;
}
std::vector<std::string> zip_entry_names(const fs::path &zip_file)
{
mz_zip_archive zip;
mz_zip_zero_struct(&zip);
REQUIRE(open_zip_reader(&zip, zip_file.string()));
std::vector<std::string> names;
const int n = static_cast<int>(mz_zip_reader_get_num_files(&zip));
for (int i = 0; i < n; ++i) {
mz_zip_archive_file_stat st;
REQUIRE(mz_zip_reader_file_stat(&zip, i, &st));
names.emplace_back(st.m_filename);
}
REQUIRE(close_zip_reader(&zip));
return names;
}
const Preset *import_named_printer(PresetBundle &bundle, const fs::path &zip_file, const fs::path &data_root,
const std::string &bare_name, int confirm = 1)
{
ScopedDataDir scoped(data_root);
AppConfig config;
std::vector<std::string> files{zip_file.string()};
bundle.import_presets(files, [confirm](std::string const &) { return confirm; },
ForwardCompatibilitySubstitutionRule::Disable, config);
for (const Preset &preset : bundle.printers.get_presets())
if (get_preset_bare_name(preset.name) == bare_name && !preset.file.empty())
return &preset;
return nullptr;
}
const Preset *import_bed(PresetBundle &bundle, const fs::path &zip_file, const fs::path &data_root, int confirm = 1)
{
return import_named_printer(bundle, zip_file, data_root, "BedPrinter", confirm);
}
struct BedRoots {
ScopedTemporaryDir temp_dir;
fs::path src;
fs::path data_root;
PresetBundle bundle;
BedRoots()
: src(temp_dir.path() / "src")
, data_root(temp_dir.path() / "datadir")
{}
};
} // namespace
TEST_CASE("Printer bundle round trip packs bed texture and model and rewrites both keys", "[Preset][Bundle]")
{
// tex_rel "*" builds a non-ASCII source path under the temp directory.
const auto pack_import = [](const std::string &bundle_id, const std::string &tex_rel, const std::string &mod_rel,
const std::string &tex_bytes, const std::string &mod_bytes, bool tex_exists,
bool mod_exists, int imports) {
ScopedTemporaryDir temp_dir;
const fs::path src = temp_dir.path() / "src";
const fs::path data_root = temp_dir.path() / "datadir";
std::string texture = tex_rel.empty() ? std::string() : (src / tex_rel).make_preferred().string();
std::string model = mod_rel.empty() ? std::string() : (src / mod_rel).make_preferred().string();
if (tex_rel == "*") {
const fs::path dir = fs::path(src.wstring()) / fs::path(boost::nowide::widen("\xE5\xBA\x8A"));
texture = boost::nowide::narrow((dir / fs::path(boost::nowide::widen("\xE7\xBA\xB9\xE7\x90\x86.png"))).wstring());
}
if (tex_exists)
write_bytes(texture, tex_bytes);
if (mod_exists)
write_bytes(model, mod_bytes);
PresetBundle bundle;
const fs::path printer_json = write_printer_json(bundle.printers.default_preset().config, src / "BedPrinter.json",
"BedPrinter", texture, model);
const fs::path zip_file = src / "bundle.orca_printer";
const nlohmann::json structure = pack_bed_zip(zip_file, printer_json, bundle.printers.default_preset().config,
texture, model, bundle_id);
const Preset *saved = nullptr;
for (int i = 0; i < imports; ++i)
saved = import_bed(bundle, zip_file, data_root);
REQUIRE(saved != nullptr);
const std::string got[] = {saved->config.opt_string("bed_custom_texture"), saved->config.opt_string("bed_custom_model")};
const std::string rel[] = {tex_rel, mod_rel};
const std::string bytes[] = {tex_bytes, mod_bytes};
const std::string path[] = {texture, model};
const bool exists[] = {tex_exists, mod_exists};
const char *option[] = {"bed_custom_texture", "bed_custom_model"};
const char *stem[] = {"texture", "model"};
for (int slot = 0; slot < 2; ++slot) {
if (exists[slot]) {
const std::string ext = fs::path(rel[slot] == "*" ? "x.png" : rel[slot]).extension().string();
const std::string zip_name = std::string("bed/0/") + stem[slot] + ext;
REQUIRE(structure["printer_asset"]["printer/BedPrinter.json"][option[slot]] == zip_name);
REQUIRE(fs::path(got[slot]).filename().string() == std::string(stem[slot]) + fs::path(zip_name).extension().string());
REQUIRE(fs::path(got[slot]).parent_path().filename() == "0");
REQUIRE(fs::path(got[slot]).parent_path().parent_path().filename() == "bed");
REQUIRE(got[slot].find(bundle_id) != std::string::npos);
REQUIRE(read_file(got[slot]) == bytes[slot]);
REQUIRE(got[slot] != path[slot]);
} else if (!rel[slot].empty() && rel[slot] != "*") {
REQUIRE_FALSE(structure.value("printer_asset", nlohmann::json::object())
.value("printer/BedPrinter.json", nlohmann::json::object())
.contains(option[slot]));
REQUIRE(got[slot] == path[slot]);
} else if (rel[slot].empty()) {
REQUIRE(got[slot].empty());
}
}
if (imports > 1) {
int copies = 0;
for (fs::recursive_directory_iterator it(data_root), end; it != end; ++it)
if (it->path().filename() == "texture.png" || it->path().filename() == "model.stl")
++copies;
REQUIRE(copies == (tex_exists ? 1 : 0) + (mod_exists ? 1 : 0));
}
};
SECTION("both assets are packed from the preset config and rewritten on import") {
pack_import("bed-roundtrip", "fixture/baseline_bed_texture.png", "fixture/baseline_bed_model.stl",
kTextureBytes, kModelBytes, true, true, 1);
}
SECTION("texture only") {
pack_import("texture-only", "only.png", "", kTextureBytes, "", true, false, 1);
}
SECTION("model only") {
pack_import("model-only", "", "only.stl", "", kModelBytes, false, true, 1);
}
SECTION("texture and model share a source basename") {
pack_import("shared-basename", "a/plate.png", "b/plate.stl", kTextureBytes, kOtherBytes, true, true, 1);
}
SECTION("importing the same bundle twice keeps one copy of the assets") {
// First import installs kTextureBytes / kModelBytes. The second pack uses different bytes so
// the Yes-overwrite path (copy_file overwrite_existing) is actually proven.
BedRoots bed;
const std::string texture = (bed.src / "twice.png").make_preferred().string();
const std::string model = (bed.src / "twice.stl").make_preferred().string();
write_bytes(texture, kTextureBytes);
write_bytes(model, kModelBytes);
const fs::path printer_json = write_printer_json(bed.bundle.printers.default_preset().config,
bed.src / "BedPrinter.json", "BedPrinter", texture, model);
const fs::path zip_file = bed.src / "bundle.orca_printer";
pack_bed_zip(zip_file, printer_json, bed.bundle.printers.default_preset().config, texture, model, "reimport");
const Preset *saved = import_bed(bed.bundle, zip_file, bed.data_root);
REQUIRE(saved != nullptr);
const std::string got_tex = saved->config.opt_string("bed_custom_texture");
const std::string got_mod = saved->config.opt_string("bed_custom_model");
REQUIRE(read_file(got_tex) == kTextureBytes);
REQUIRE(read_file(got_mod) == kModelBytes);
write_bytes(texture, kOtherBytes);
write_bytes(model, std::string("stl\x00other", 9));
pack_bed_zip(zip_file, printer_json, bed.bundle.printers.default_preset().config, texture, model, "reimport");
saved = import_bed(bed.bundle, zip_file, bed.data_root);
REQUIRE(saved != nullptr);
REQUIRE(saved->config.opt_string("bed_custom_texture") == got_tex);
REQUIRE(saved->config.opt_string("bed_custom_model") == got_mod);
REQUIRE(read_file(got_tex) == kOtherBytes);
REQUIRE(read_file(got_mod) == std::string("stl\x00other", 9));
}
SECTION("a non-ASCII source path is packed") {
pack_import("non-ascii", "*", "", kTextureBytes, "", true, false, 1);
}
}
TEST_CASE("An old printer bundle without printer_asset imports unchanged", "[Preset][Bundle]")
{
BedRoots bed;
const std::string texture = (bed.src / "old.png").make_preferred().string();
const std::string model = (bed.src / "old.stl").make_preferred().string();
write_bytes(texture, kTextureBytes);
write_bytes(model, kModelBytes);
const fs::path printer_json = write_printer_json(bed.bundle.printers.default_preset().config, bed.src / "BedPrinter.json",
"BedPrinter", texture, model);
nlohmann::json structure = bed_manifest("old-bundle");
REQUIRE_FALSE(structure.contains("printer_asset"));
const fs::path zip_file = bed.src / "old.orca_printer";
write_zip(zip_file, {{"printer/BedPrinter.json", read_file(printer_json)}, {BUNDLE_STRUCTURE_JSON_NAME, structure.dump()}});
// A bed/ folder left by an earlier import into the same bundle folder must survive an asset-less import.
const fs::path old_bed = bed.data_root / "user" / "default" / "_local" / "old-bundle" / "bed";
write_bytes((old_bed / "texture.png").string(), kTextureBytes);
const Preset *saved = import_bed(bed.bundle, zip_file, bed.data_root);
REQUIRE(saved != nullptr);
REQUIRE(saved->config.opt_string("bed_custom_texture") == texture);
REQUIRE(saved->config.opt_string("bed_custom_model") == model);
CHECK(fs::exists(old_bed / "texture.png"));
}
TEST_CASE("Printer bundle asset names that escape the bundle directory are rejected", "[Preset][Bundle]")
{
const char *bad_names[] = {"../../pwned.png", "bed/../../pwned.png", "..\\..\\pwned.png", "C:/Windows/pwned.png",
"machine/texture.png", "/bed/texture.png"};
const std::string marker = "PWNED-BED-ASSET-MARKER";
for (const char *name : bad_names) {
DYNAMIC_SECTION("rejects " << name) {
BedRoots bed;
const std::string texture = (bed.src / "kept.png").make_preferred().string();
const std::string model = (bed.src / "kept.stl").make_preferred().string();
write_bytes(texture, kTextureBytes);
write_bytes(model, kModelBytes);
const fs::path printer_json = write_printer_json(bed.bundle.printers.default_preset().config,
bed.src / "BedPrinter.json", "BedPrinter", texture, model);
nlohmann::json structure = bed_manifest("traverse");
set_printer_bed_asset(structure, "bed_custom_texture", name);
std::vector<std::pair<std::string, std::string>> entries = {
{"printer/BedPrinter.json", read_file(printer_json)}, {BUNDLE_STRUCTURE_JSON_NAME, structure.dump()}};
if (name[0] != '/' && name[0] != '\\')
entries.emplace_back(name, marker);
const fs::path zip_file = bed.src / "bad.orca_printer";
write_zip(zip_file, entries);
const Preset *saved = import_bed(bed.bundle, zip_file, bed.data_root);
REQUIRE(saved != nullptr);
REQUIRE(saved->config.opt_string("bed_custom_texture") == texture);
REQUIRE(saved->config.opt_string("bed_custom_model") == model);
CHECK_FALSE(any_filename_contains(bed.data_root, fs::path(name).filename().string()));
if (fs::exists(bed.data_root))
for (fs::recursive_directory_iterator it(bed.data_root), end; it != end; ++it)
if (fs::is_regular_file(*it))
CHECK(read_file(*it).find(marker) == std::string::npos);
}
}
}
TEST_CASE("A printer bundle asset that is missing is skipped", "[Preset][Bundle]")
{
SECTION("a manifest entry with no zip bytes leaves the original path") {
BedRoots bed;
const std::string texture = (bed.src / "gone.png").make_preferred().string();
write_bytes(texture, kTextureBytes);
const fs::path printer_json = write_printer_json(bed.bundle.printers.default_preset().config, bed.src / "BedPrinter.json",
"BedPrinter", texture, {});
nlohmann::json structure = bed_manifest("missing-zip");
set_printer_bed_asset(structure, "bed_custom_texture", "bed/0/texture.png");
const fs::path zip_file = bed.src / "missing.orca_printer";
write_zip(zip_file, {{"printer/BedPrinter.json", read_file(printer_json)}, {BUNDLE_STRUCTURE_JSON_NAME, structure.dump()}});
const Preset *saved = import_bed(bed.bundle, zip_file, bed.data_root);
REQUIRE(saved != nullptr);
REQUIRE(saved->config.opt_string("bed_custom_texture") == texture);
CHECK_FALSE(any_filename_contains(bed.data_root, "texture.png"));
}
SECTION("a missing file at export is omitted and the export still succeeds") {
BedRoots bed;
const std::string missing = (bed.src / "missing.png").make_preferred().string();
const std::string model = (bed.src / "present.stl").make_preferred().string();
write_bytes(model, kModelBytes);
const fs::path printer_json = write_printer_json(bed.bundle.printers.default_preset().config, bed.src / "BedPrinter.json",
"BedPrinter", missing, model);
const fs::path zip_file = bed.src / "partial.orca_printer";
const nlohmann::json structure = pack_bed_zip(zip_file, printer_json, bed.bundle.printers.default_preset().config,
missing, model, "missing-export");
REQUIRE_FALSE(structure["printer_asset"]["printer/BedPrinter.json"].contains("bed_custom_texture"));
REQUIRE(structure["printer_asset"]["printer/BedPrinter.json"]["bed_custom_model"] == "bed/0/model.stl");
const Preset *saved = import_bed(bed.bundle, zip_file, bed.data_root);
REQUIRE(saved != nullptr);
REQUIRE(saved->config.opt_string("bed_custom_texture") == missing);
REQUIRE(read_file(saved->config.opt_string("bed_custom_model")) == kModelBytes);
REQUIRE(fs::path(saved->config.opt_string("bed_custom_model")).filename() == "model.stl");
CHECK_FALSE(any_filename_contains(bed.data_root, "texture.png"));
}
}
TEST_CASE("An inherited printer bundle rewrites bed paths taken from the parent", "[Preset][Bundle]")
{
BedRoots bed;
const std::string texture = (bed.src / "parent.png").make_preferred().string();
const std::string model = (bed.src / "parent.stl").make_preferred().string();
write_bytes(texture, kTextureBytes);
write_bytes(model, kModelBytes);
DynamicPrintConfig parent(bed.bundle.printers.default_preset().config);
parent.option<ConfigOptionString>("printer_settings_id", true)->value = "ParentPrinter";
parent.option<ConfigOptionString>("inherits", true)->value.clear();
parent.option<ConfigOptionString>("bed_custom_texture", true)->value = texture;
parent.option<ConfigOptionString>("bed_custom_model", true)->value = model;
bed.bundle.printers.load_preset((bed.src / "ParentPrinter.json").string(), "ParentPrinter", std::move(parent), false);
const fs::path child_json = write_printer_json(bed.bundle.printers.default_preset().config, bed.src / "BedPrinter.json",
"BedPrinter", {}, {}, "ParentPrinter");
const nlohmann::json written = nlohmann::json::parse(read_file(child_json));
REQUIRE_FALSE(written.contains("bed_custom_texture"));
REQUIRE_FALSE(written.contains("bed_custom_model"));
nlohmann::json structure = bed_manifest("inherit-bed");
set_printer_bed_asset(structure, "bed_custom_texture", "bed/0/texture.png");
set_printer_bed_asset(structure, "bed_custom_model", "bed/0/model.stl");
const fs::path zip_file = bed.src / "inherit.orca_printer";
write_zip(zip_file, {{"printer/BedPrinter.json", read_file(child_json)}, {BUNDLE_STRUCTURE_JSON_NAME, structure.dump()},
{"bed/0/texture.png", kTextureBytes}, {"bed/0/model.stl", kModelBytes}});
const Preset *saved = import_bed(bed.bundle, zip_file, bed.data_root);
REQUIRE(saved != nullptr);
REQUIRE(saved->config.opt_string("bed_custom_texture") != texture);
REQUIRE(saved->config.opt_string("bed_custom_model") != model);
REQUIRE(read_file(saved->config.opt_string("bed_custom_texture")) == kTextureBytes);
REQUIRE(read_file(saved->config.opt_string("bed_custom_model")) == kModelBytes);
REQUIRE(fs::path(saved->config.opt_string("bed_custom_texture")).filename() == "texture.png");
REQUIRE(fs::path(saved->config.opt_string("bed_custom_model")).filename() == "model.stl");
}
TEST_CASE("A printer bundle rejects bed assets whose extension the bed dialog does not allow", "[Preset][Bundle]")
{
BedRoots bed;
const fs::path printer_json = write_printer_json(bed.bundle.printers.default_preset().config, bed.src / "BedPrinter.json",
"BedPrinter", {}, {});
nlohmann::json structure = bed_manifest("bad-ext");
set_printer_bed_asset(structure, "bed_custom_texture", "bed/0/texture.stl");
set_printer_bed_asset(structure, "bed_custom_model", "bed/0/model.exe");
const fs::path zip_file = bed.src / "bad-ext.orca_printer";
write_zip(zip_file, {{"printer/BedPrinter.json", read_file(printer_json)}, {BUNDLE_STRUCTURE_JSON_NAME, structure.dump()},
{"bed/0/texture.stl", kTextureBytes}, {"bed/0/model.exe", std::string("MZ")}});
REQUIRE(import_bed(bed.bundle, zip_file, bed.data_root) != nullptr);
CHECK_FALSE(any_filename_contains(bed.data_root, "model.exe"));
CHECK_FALSE(any_filename_contains(bed.data_root, "texture.stl"));
}
TEST_CASE("A manifest asset name that points at a preset does not drop that preset", "[Preset][Bundle]")
{
BedRoots bed;
const fs::path printer_json = write_printer_json(bed.bundle.printers.default_preset().config, bed.src / "BedPrinter.json",
"BedPrinter", {}, {});
nlohmann::json structure = bed_manifest("name-clash");
set_printer_bed_asset(structure, "bed_custom_texture", "printer/BedPrinter.json");
const fs::path zip_file = bed.src / "clash.orca_printer";
write_zip(zip_file, {{"printer/BedPrinter.json", read_file(printer_json)}, {BUNDLE_STRUCTURE_JSON_NAME, structure.dump()}});
REQUIRE(import_bed(bed.bundle, zip_file, bed.data_root) != nullptr);
CHECK_FALSE(any_filename_contains(bed.data_root, "texture.png"));
}
TEST_CASE("A declined printer bundle import leaves existing bed files alone", "[Preset][Bundle]")
{
BedRoots bed;
const fs::path kept = bed.data_root / "user" / "default" / "_local" / "keep-bed" / "bed" / "texture.png";
const std::string original("kept-texture");
write_bytes(kept.string(), original);
DynamicPrintConfig existing(bed.bundle.printers.default_preset().config);
existing.option<ConfigOptionString>("printer_settings_id", true)->value = "BedPrinter";
existing.option<ConfigOptionString>("inherits", true)->value.clear();
bed.bundle.printers.load_preset((bed.src / "existing.json").string(), "_local/keep-bed/BedPrinter", std::move(existing),
false);
const fs::path printer_json = write_printer_json(bed.bundle.printers.default_preset().config, bed.src / "BedPrinter.json",
"BedPrinter", {}, {});
nlohmann::json structure = bed_manifest("keep-bed");
set_printer_bed_asset(structure, "bed_custom_texture", "bed/0/texture.png");
set_printer_bed_asset(structure, "bed_custom_model", "bed/0/model.stl");
const fs::path zip_file = bed.src / "keep.orca_printer";
write_zip(zip_file, {{"printer/BedPrinter.json", read_file(printer_json)}, {BUNDLE_STRUCTURE_JSON_NAME, structure.dump()},
{"bed/0/texture.png", kTextureBytes}, {"bed/0/model.stl", kModelBytes}});
import_bed(bed.bundle, zip_file, bed.data_root, 0);
// This import wrote nothing: the existing texture is unchanged and model.stl was not added.
REQUIRE(fs::exists(kept));
REQUIRE(read_file(kept) == original);
CHECK_FALSE(fs::exists(kept.parent_path() / "model.stl"));
CHECK_FALSE(fs::exists(kept.parent_path() / "0" / "model.stl"));
}
TEST_CASE("A declined re-import does not replace bed bytes an earlier import wrote", "[Preset][Bundle]")
{
BedRoots bed;
const fs::path printer_json = write_printer_json(bed.bundle.printers.default_preset().config, bed.src / "BedPrinter.json",
"BedPrinter", {}, {});
nlohmann::json structure = bed_manifest("reimport-no");
set_printer_bed_asset(structure, "bed_custom_texture", "bed/0/texture.png");
const std::string manifest = structure.dump();
const std::string printer_bytes = read_file(printer_json);
const fs::path first_zip = bed.src / "first.orca_printer";
const fs::path second_zip = bed.src / "second.orca_printer";
write_zip(first_zip, {{"printer/BedPrinter.json", printer_bytes}, {BUNDLE_STRUCTURE_JSON_NAME, manifest},
{"bed/0/texture.png", kTextureBytes}});
write_zip(second_zip, {{"printer/BedPrinter.json", printer_bytes}, {BUNDLE_STRUCTURE_JSON_NAME, manifest},
{"bed/0/texture.png", kOtherBytes}});
const Preset *saved = import_bed(bed.bundle, first_zip, bed.data_root);
REQUIRE(saved != nullptr);
const std::string installed = saved->config.opt_string("bed_custom_texture");
REQUIRE(read_file(installed) == kTextureBytes);
import_bed(bed.bundle, second_zip, bed.data_root, 0);
REQUIRE(fs::exists(installed));
REQUIRE(read_file(installed) == kTextureBytes);
}
TEST_CASE("Export with bed assets disabled packs no bed files and writes no printer_asset", "[Preset][Bundle]")
{
BedRoots bed;
const std::string texture = (bed.src / "off.png").make_preferred().string();
const std::string model = (bed.src / "off.stl").make_preferred().string();
write_bytes(texture, kTextureBytes);
write_bytes(model, kModelBytes);
const fs::path printer_json = write_printer_json(bed.bundle.printers.default_preset().config, bed.src / "BedPrinter.json",
"BedPrinter", texture, model);
const fs::path zip_file = bed.src / "off.orca_printer";
const nlohmann::json structure = pack_bed_zip(zip_file, printer_json, bed.bundle.printers.default_preset().config,
texture, model, "bed-off", false);
REQUIRE_FALSE(structure.contains("printer_asset"));
for (const std::string &name : zip_entry_names(zip_file))
REQUIRE(name.compare(0, 4, "bed/") != 0);
}
TEST_CASE("Two printers in one bundle keep separate bed folders", "[Preset][Bundle]")
{
BedRoots bed;
const std::string tex_a = (bed.src / "a.png").make_preferred().string();
const std::string mod_a = (bed.src / "a.stl").make_preferred().string();
const std::string tex_b = (bed.src / "b.png").make_preferred().string();
const std::string mod_b = (bed.src / "b.stl").make_preferred().string();
write_bytes(tex_a, kTextureBytes);
write_bytes(mod_a, kModelBytes);
write_bytes(tex_b, kOtherBytes);
write_bytes(mod_b, std::string("stl\x00other", 9));
const fs::path json_a = write_printer_json(bed.bundle.printers.default_preset().config, bed.src / "PrinterA.json",
"PrinterA", tex_a, mod_a);
const fs::path json_b = write_printer_json(bed.bundle.printers.default_preset().config, bed.src / "PrinterB.json",
"PrinterB", tex_b, mod_b);
nlohmann::json structure = nlohmann::json::object();
structure["id"] = "two-printers";
structure["bundle_type"] = "printer config bundle";
structure["printer_config"] = nlohmann::json::array({"printer/PrinterA.json", "printer/PrinterB.json"});
DynamicPrintConfig config_a(bed.bundle.printers.default_preset().config);
config_a.option<ConfigOptionString>("bed_custom_texture", true)->value = tex_a;
config_a.option<ConfigOptionString>("bed_custom_model", true)->value = mod_a;
DynamicPrintConfig config_b(bed.bundle.printers.default_preset().config);
config_b.option<ConfigOptionString>("bed_custom_texture", true)->value = tex_b;
config_b.option<ConfigOptionString>("bed_custom_model", true)->value = mod_b;
const fs::path zip_file = bed.src / "two.orca_printer";
mz_zip_archive zip;
mz_zip_zero_struct(&zip);
const std::string bytes_a = read_file(json_a);
const std::string bytes_b = read_file(json_b);
REQUIRE(open_zip_writer(&zip, zip_file.string()));
REQUIRE(mz_zip_writer_add_mem(&zip, "printer/PrinterA.json", bytes_a.data(), bytes_a.size(), MZ_DEFAULT_COMPRESSION));
REQUIRE(mz_zip_writer_add_mem(&zip, "printer/PrinterB.json", bytes_b.data(), bytes_b.size(), MZ_DEFAULT_COMPRESSION));
REQUIRE(append_printer_bed_assets(zip, config_a, structure, "printer/PrinterA.json", 0));
REQUIRE(append_printer_bed_assets(zip, config_b, structure, "printer/PrinterB.json", 1));
const std::string manifest = structure.dump();
REQUIRE(mz_zip_writer_add_mem(&zip, BUNDLE_STRUCTURE_JSON_NAME, manifest.data(), manifest.size(),
MZ_DEFAULT_COMPRESSION));
REQUIRE(mz_zip_writer_finalize_archive(&zip));
REQUIRE(close_zip_writer(&zip));
REQUIRE(structure["printer_asset"]["printer/PrinterA.json"]["bed_custom_texture"] == "bed/0/texture.png");
REQUIRE(structure["printer_asset"]["printer/PrinterB.json"]["bed_custom_texture"] == "bed/1/texture.png");
ScopedDataDir scoped(bed.data_root);
AppConfig app_config;
std::vector<std::string> files{zip_file.string()};
bed.bundle.import_presets(files, [](std::string const &) { return 1; },
ForwardCompatibilitySubstitutionRule::Disable, app_config);
const Preset *saved_a = nullptr;
const Preset *saved_b = nullptr;
for (const Preset &preset : bed.bundle.printers.get_presets()) {
if (preset.file.empty())
continue;
if (get_preset_bare_name(preset.name) == "PrinterA")
saved_a = &preset;
if (get_preset_bare_name(preset.name) == "PrinterB")
saved_b = &preset;
}
REQUIRE(saved_a != nullptr);
REQUIRE(saved_b != nullptr);
const std::string got_tex_a = saved_a->config.opt_string("bed_custom_texture");
const std::string got_tex_b = saved_b->config.opt_string("bed_custom_texture");
REQUIRE(fs::path(got_tex_a).parent_path().filename() == "0");
REQUIRE(fs::path(got_tex_b).parent_path().filename() == "1");
REQUIRE(got_tex_a != got_tex_b);
REQUIRE(read_file(got_tex_a) == kTextureBytes);
REQUIRE(read_file(got_tex_b) == kOtherBytes);
REQUIRE(read_file(saved_a->config.opt_string("bed_custom_model")) == kModelBytes);
REQUIRE(read_file(saved_b->config.opt_string("bed_custom_model")) == std::string("stl\x00other", 9));
}
TEST_CASE("A printer_asset entry keyed to a printer not in the zip is ignored", "[Preset][Bundle]")
{
BedRoots bed;
const std::string texture = (bed.src / "kept.png").make_preferred().string();
write_bytes(texture, kTextureBytes);
const fs::path printer_json = write_printer_json(bed.bundle.printers.default_preset().config, bed.src / "BedPrinter.json",
"BedPrinter", texture, {});
nlohmann::json structure = bed_manifest("ghost-key");
set_printer_bed_asset(structure, "bed_custom_texture", "bed/0/texture.png", "printer/Ghost.json");
const fs::path zip_file = bed.src / "ghost.orca_printer";
write_zip(zip_file, {{"printer/BedPrinter.json", read_file(printer_json)}, {BUNDLE_STRUCTURE_JSON_NAME, structure.dump()},
{"bed/0/texture.png", kOtherBytes}});
const Preset *saved = import_bed(bed.bundle, zip_file, bed.data_root);
REQUIRE(saved != nullptr);
REQUIRE(saved->config.opt_string("bed_custom_texture") == texture);
CHECK_FALSE(any_filename_contains(bed.data_root, "texture.png"));
}
TEST_CASE("Keyed printer_asset rejects malformed bed paths", "[Preset][Bundle]")
{
const char *bad_names[] = {"bed/texture.png", "bed/x/texture.png", "bed/0/../texture.png", "bed/0/model.png"};
const std::string marker = "PWNED-BED-ASSET-MARKER";
for (const char *name : bad_names) {
DYNAMIC_SECTION("rejects " << name) {
BedRoots bed;
const std::string texture = (bed.src / "kept.png").make_preferred().string();
write_bytes(texture, kTextureBytes);
const fs::path printer_json = write_printer_json(bed.bundle.printers.default_preset().config,
bed.src / "BedPrinter.json", "BedPrinter", texture, {});
nlohmann::json structure = bed_manifest("bad-keyed");
set_printer_bed_asset(structure, "bed_custom_texture", name);
std::vector<std::pair<std::string, std::string>> entries = {
{"printer/BedPrinter.json", read_file(printer_json)}, {BUNDLE_STRUCTURE_JSON_NAME, structure.dump()},
{name, marker}};
const fs::path zip_file = bed.src / "bad-keyed.orca_printer";
write_zip(zip_file, entries);
const Preset *saved = import_bed(bed.bundle, zip_file, bed.data_root);
REQUIRE(saved != nullptr);
REQUIRE(saved->config.opt_string("bed_custom_texture") == texture);
if (fs::exists(bed.data_root))
for (fs::recursive_directory_iterator it(bed.data_root), end; it != end; ++it)
if (fs::is_regular_file(*it))
CHECK(read_file(*it).find(marker) == std::string::npos);
}
}
SECTION("field name does not match file kind") {
BedRoots bed;
const std::string texture = (bed.src / "kept.png").make_preferred().string();
write_bytes(texture, kTextureBytes);
const fs::path printer_json = write_printer_json(bed.bundle.printers.default_preset().config, bed.src / "BedPrinter.json",
"BedPrinter", texture, {});
nlohmann::json structure = bed_manifest("kind-mismatch");
set_printer_bed_asset(structure, "bed_custom_texture", "bed/0/model.stl");
const fs::path zip_file = bed.src / "kind.orca_printer";
write_zip(zip_file, {{"printer/BedPrinter.json", read_file(printer_json)}, {BUNDLE_STRUCTURE_JSON_NAME, structure.dump()},
{"bed/0/model.stl", kModelBytes}});
const Preset *saved = import_bed(bed.bundle, zip_file, bed.data_root);
REQUIRE(saved != nullptr);
REQUIRE(saved->config.opt_string("bed_custom_texture") == texture);
CHECK_FALSE(any_filename_contains(bed.data_root, "model.stl"));
}
}
TEST_CASE("A saved printer preset reloads the tool layout it was saved with", "[Preset][Bundle][IMEX]")
{
ScopedTemporaryDir temp_dir;