Add fork features: host view sync, grouping, image compress, freistellen
Host can force all clients to their camera; selection Group/Ungroup with synced groupIds; WebP/PNG preprocess and Tower rembg/white-key freistellen on insert or via selection actions (save format 0.7 / INFPNT000007). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
df49687f51
commit
042fe8f5a6
19 changed files with 672 additions and 9 deletions
|
|
@ -115,6 +115,7 @@ set(sources "src/main.cpp"
|
|||
"src/CanvasComponents/ImageCanvasComponent.cpp"
|
||||
"src/CanvasComponents/BrushComponentCode.cpp"
|
||||
"src/CanvasComponents/CanvasComponent.cpp"
|
||||
"src/ImagePreprocess.cpp"
|
||||
"src/GUIStuff/GUIManager.cpp"
|
||||
"src/GUIStuff/GUIFloatAnimation.cpp"
|
||||
"src/GUIStuff/GUIManagerID.cpp"
|
||||
|
|
|
|||
33
FORK.md
Normal file
33
FORK.md
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# Fork features (copilot/infinipaint)
|
||||
|
||||
Upstream: [ErrorAtLine0/infinipaint](https://github.com/ErrorAtLine0/infinipaint)
|
||||
Forgejo: https://forgejo.fsociety00.cc/copilot/infinipaint
|
||||
|
||||
Save format: `INFPNT000007` / version `0.7.0-fork` (not network-compatible with stock 0.6.x peers).
|
||||
|
||||
## Bring Everyone Here (host)
|
||||
|
||||
In the player list, the lobby host gets **Bring Everyone Here**. Clients receive `CLIENT_FORCE_CAMERA_JUMP` and run the same `smooth_move_to` path as **Jump To**.
|
||||
|
||||
## Group / Ungroup
|
||||
|
||||
With a multi-selection (images, text boxes, shapes, …), use **Group** / **Ungroup** in the selection panel. Grouped objects auto-expand into the selection so they transform together. Membership syncs via `SET_COMPONENT_GROUP_IDS`.
|
||||
|
||||
## Image compression
|
||||
|
||||
Settings → **Images**:
|
||||
|
||||
- Compress on insert (default on)
|
||||
- Format: Keep / WebP / PNG
|
||||
- WebP quality
|
||||
|
||||
Selection toolbar: **Compress Images** re-encodes already placed images.
|
||||
|
||||
## Freistellen
|
||||
|
||||
Settings → **Images**:
|
||||
|
||||
- Freistellen on insert
|
||||
- Modes: White background (always), or rembg **Schnell** (`silueta`) / **Allgemein** (`bria-rmbg`) when Tower rembg is installed (`~/.local/share/tower-rembg` or `rembg` on `PATH`)
|
||||
|
||||
Selection: **Freistellen** applies the configured mode to selected images (then compresses).
|
||||
|
|
@ -19,6 +19,8 @@
|
|||
|
||||
InfiniPaint is a **collaborative, infinite** canvas note-taking/drawing app. Unlike many other infinite canvas apps, **there is no zoom-in or zoom-out limit** (at least up until the point your computer runs out of memory). This means that this app is very good at things such as drawing sketches of the solar system to scale, or just drawing any massive objects with tiny details. Of course, even though this is a feature, this app is also perfectly well suited for use as a normal canvas.
|
||||
|
||||
> **This repository is a fork** hosted at [forgejo.fsociety00.cc/copilot/infinipaint](https://forgejo.fsociety00.cc/copilot/infinipaint). Extra features (host view sync, grouping, image compression, freistellen) are documented in [FORK.md](FORK.md).
|
||||
|
||||
You can try the web version of this app at [infinipaint.com](https://infinipaint.com) (requires a WebGL2 capable browser). The web version was compiled from C++ to Javascript using Emscripten, and may contain some bugs, so if possible, please consider downloading the native version for a better experience.
|
||||
|
||||
This app was inspired by [Lorien](https://github.com/mbrlabs/Lorien), another great infinite canvas program.
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ CanvasComponentContainer::CanvasComponentContainer(NetworkingObjects::NetObjMana
|
|||
|
||||
void CanvasComponentContainer::register_class(World& w) {
|
||||
auto readConstructorFunc = [&w](const NetworkingObjects::NetObjTemporaryPtr<CanvasComponentContainer>& o, cereal::PortableBinaryInputArchive& a, const std::shared_ptr<NetServer::ClientData>& c) {
|
||||
a(o->coords);
|
||||
a(o->coords, o->groupId);
|
||||
o->compAllocator = o.get_obj_man()->read_create_message<CanvasComponentAllocator>(a, c);
|
||||
o->compAllocator->comp->compContainer = o.get();
|
||||
canvas_scale_up_check(*o, w, c);
|
||||
|
|
@ -72,6 +72,7 @@ std::unique_ptr<CanvasComponentContainer::CopyData> CanvasComponentContainer::ge
|
|||
auto toRet = std::make_unique<CanvasComponentContainer::CopyData>();
|
||||
toRet->coords = coords;
|
||||
toRet->obj = compAllocator->comp->get_data_copy();
|
||||
// groupId intentionally not copied — paste creates independent objects
|
||||
return toRet;
|
||||
}
|
||||
|
||||
|
|
@ -84,18 +85,22 @@ void CanvasComponentContainer::set_object_update_lock(DrawingProgram& drawP, boo
|
|||
}
|
||||
|
||||
void CanvasComponentContainer::write_constructor_func(const NetworkingObjects::NetObjTemporaryPtr<CanvasComponentContainer>& o, cereal::PortableBinaryOutputArchive& a) {
|
||||
a(o->coords);
|
||||
a(o->coords, o->groupId);
|
||||
o->compAllocator.write_create_message(a);
|
||||
}
|
||||
|
||||
void CanvasComponentContainer::save_file(cereal::PortableBinaryOutputArchive& a) const {
|
||||
a(coords);
|
||||
a(coords, groupId);
|
||||
compAllocator->save_file(a);
|
||||
}
|
||||
|
||||
void CanvasComponentContainer::load_file(cereal::PortableBinaryInputArchive& a, VersionNumber version, NetObjManager& objMan) {
|
||||
if(version >= VersionNumber(0, 4, 0)) {
|
||||
a(coords);
|
||||
if(version >= VersionNumber(0, 7, 0))
|
||||
a(groupId);
|
||||
else
|
||||
groupId = 0;
|
||||
compAllocator = objMan.make_obj_direct<CanvasComponentAllocator>();
|
||||
compAllocator->load_file(a, version);
|
||||
compAllocator->comp->compContainer = this;
|
||||
|
|
@ -104,6 +109,7 @@ void CanvasComponentContainer::load_file(cereal::PortableBinaryInputArchive& a,
|
|||
CanvasComponentType t;
|
||||
NetworkingObjects::NetObjID uselessID;
|
||||
a(t, uselessID, coords);
|
||||
groupId = 0;
|
||||
compAllocator = objMan.make_obj_direct<CanvasComponentAllocator>(t);
|
||||
compAllocator->comp->load_file(a, version);
|
||||
compAllocator->comp->compContainer = this;
|
||||
|
|
|
|||
|
|
@ -101,6 +101,8 @@ class CanvasComponentContainer {
|
|||
std::weak_ptr<DrawingProgramCacheBVHNode> cacheParentBvhNode;
|
||||
DrawingProgramLayerListItem* parentLayer = nullptr;
|
||||
CoordSpaceHelper coords;
|
||||
// 0 = ungrouped. Non-zero IDs move/transform together when any member is selected.
|
||||
uint64_t groupId = 0;
|
||||
ObjInfoIterator objInfo;
|
||||
private:
|
||||
friend class BrushStrokeCanvasComponent;
|
||||
|
|
|
|||
|
|
@ -28,7 +28,9 @@ enum ServerCommands : MessageCommandType {
|
|||
SERVER_UPDATE_NETWORK_OBJECT,
|
||||
SERVER_NEW_RESOURCE_ID,
|
||||
SERVER_NEW_RESOURCE_DATA,
|
||||
SERVER_TRANSFORM_MANY_COMPONENTS
|
||||
SERVER_TRANSFORM_MANY_COMPONENTS,
|
||||
SERVER_FORCE_CAMERA_JUMP,
|
||||
SERVER_SET_COMPONENT_GROUP_IDS
|
||||
};
|
||||
|
||||
enum ClientCommands : MessageCommandType {
|
||||
|
|
@ -39,5 +41,7 @@ enum ClientCommands : MessageCommandType {
|
|||
CLIENT_UPDATE_NETWORK_OBJECT,
|
||||
CLIENT_NEW_RESOURCE_ID,
|
||||
CLIENT_NEW_RESOURCE_DATA,
|
||||
CLIENT_TRANSFORM_MANY_COMPONENTS
|
||||
CLIENT_TRANSFORM_MANY_COMPONENTS,
|
||||
CLIENT_FORCE_CAMERA_JUMP,
|
||||
CLIENT_SET_COMPONENT_GROUP_IDS
|
||||
};
|
||||
|
|
|
|||
|
|
@ -48,6 +48,12 @@
|
|||
#include <chrono>
|
||||
#include "../CanvasComponents/ImageCanvasComponent.hpp"
|
||||
#include "Layers/DrawingProgramLayer.hpp"
|
||||
#include "../ImagePreprocess.hpp"
|
||||
#include <Helpers/Random.hpp>
|
||||
#include <cereal/types/utility.hpp>
|
||||
#ifdef __ANDROID__
|
||||
#include "../AndroidJNICalls.hpp"
|
||||
#endif
|
||||
#include "Layers/DrawingProgramLayerListItem.hpp"
|
||||
#include "../ScaleUpCanvas.hpp"
|
||||
|
||||
|
|
@ -336,6 +342,22 @@ void DrawingProgram::init_server_callbacks() {
|
|||
process_transform_message(transforms);
|
||||
world.netServer->send_items_to_all_clients(RELIABLE_COMMAND_CHANNEL, CLIENT_TRANSFORM_MANY_COMPONENTS, transforms);
|
||||
});
|
||||
world.netServer->add_recv_callback(SERVER_FORCE_CAMERA_JUMP, [&](std::shared_ptr<NetServer::ClientData> client, cereal::PortableBinaryInputArchive& message) {
|
||||
CoordSpaceHelper coords;
|
||||
Vector2f windowSize;
|
||||
message(coords, windowSize);
|
||||
// Only the host may force jumps; ignore client-originated requests.
|
||||
(void)client;
|
||||
(void)coords;
|
||||
(void)windowSize;
|
||||
});
|
||||
world.netServer->add_recv_callback(SERVER_SET_COMPONENT_GROUP_IDS, [&](std::shared_ptr<NetServer::ClientData> client, cereal::PortableBinaryInputArchive& message) {
|
||||
std::vector<std::pair<NetworkingObjects::NetObjID, uint64_t>> groupIds;
|
||||
message(groupIds);
|
||||
(void)client;
|
||||
process_group_id_message(groupIds);
|
||||
world.netServer->send_items_to_all_clients(RELIABLE_COMMAND_CHANNEL, CLIENT_SET_COMPONENT_GROUP_IDS, groupIds);
|
||||
});
|
||||
}
|
||||
|
||||
void DrawingProgram::init_client_callbacks() {
|
||||
|
|
@ -344,6 +366,51 @@ void DrawingProgram::init_client_callbacks() {
|
|||
message(transforms);
|
||||
process_transform_message(transforms);
|
||||
});
|
||||
world.netClient->add_recv_callback(CLIENT_FORCE_CAMERA_JUMP, [&](cereal::PortableBinaryInputArchive& message) {
|
||||
CoordSpaceHelper coords;
|
||||
Vector2f windowSize;
|
||||
message(coords, windowSize);
|
||||
process_force_camera_jump(coords, windowSize);
|
||||
});
|
||||
world.netClient->add_recv_callback(CLIENT_SET_COMPONENT_GROUP_IDS, [&](cereal::PortableBinaryInputArchive& message) {
|
||||
std::vector<std::pair<NetworkingObjects::NetObjID, uint64_t>> groupIds;
|
||||
message(groupIds);
|
||||
process_group_id_message(groupIds);
|
||||
});
|
||||
}
|
||||
|
||||
void DrawingProgram::process_force_camera_jump(const CoordSpaceHelper& coords, const Vector2f& windowSize) {
|
||||
world.drawData.cam.smooth_move_to(world, coords, windowSize);
|
||||
}
|
||||
|
||||
void DrawingProgram::force_clients_to_own_view() {
|
||||
if(!world.netServer)
|
||||
return;
|
||||
CoordSpaceHelper coords = world.drawData.cam.c;
|
||||
Vector2f windowSize = world.main.window.size.cast<float>().eval();
|
||||
world.netServer->send_items_to_all_clients(RELIABLE_COMMAND_CHANNEL, CLIENT_FORCE_CAMERA_JUMP, coords, windowSize);
|
||||
}
|
||||
|
||||
void DrawingProgram::process_group_id_message(const std::vector<std::pair<NetworkingObjects::NetObjID, uint64_t>>& groupIds) {
|
||||
for(auto& [id, groupId] : groupIds) {
|
||||
auto objPtr = world.netObjMan.get_obj_temporary_ref_from_id<CanvasComponentContainer>(id);
|
||||
if(!objPtr)
|
||||
continue;
|
||||
objPtr->groupId = groupId;
|
||||
}
|
||||
world.set_to_layout_gui_if_focus();
|
||||
}
|
||||
|
||||
void DrawingProgram::send_group_ids_for(const std::vector<CanvasComponentContainer::ObjInfo*>& objs) {
|
||||
if(!world.netObjMan.is_connected() || objs.empty())
|
||||
return;
|
||||
std::vector<std::pair<NetworkingObjects::NetObjID, uint64_t>> groupIds;
|
||||
for(auto& obj : objs)
|
||||
groupIds.emplace_back(obj->obj.get_net_id(), obj->obj->groupId);
|
||||
if(world.netObjMan.is_server())
|
||||
world.netServer->send_items_to_all_clients(RELIABLE_COMMAND_CHANNEL, CLIENT_SET_COMPONENT_GROUP_IDS, groupIds);
|
||||
else
|
||||
world.netClient->send_items_to_server(RELIABLE_COMMAND_CHANNEL, SERVER_SET_COMPONENT_GROUP_IDS, groupIds);
|
||||
}
|
||||
|
||||
void DrawingProgram::process_transform_message(const std::vector<std::pair<NetworkingObjects::NetObjID, CoordSpaceHelper>>& transforms) {
|
||||
|
|
@ -726,7 +793,23 @@ void DrawingProgram::input_add_file_to_canvas_callback(const CustomEvents::AddFi
|
|||
|
||||
void DrawingProgram::add_file_to_canvas_by_path(const std::filesystem::path& filePath, Vector2f dropPos) {
|
||||
if(layerMan.is_a_layer_being_edited()) {
|
||||
NetworkingObjects::NetObjTemporaryPtr<ResourceData> imageTempPtr = world.rMan.add_resource_file(filePath);
|
||||
ResourceData resource;
|
||||
#ifdef __ANDROID__
|
||||
resource.name = AndroidJNICalls::getFileNameFromURI(filePath.string());
|
||||
if(resource.name.empty())
|
||||
resource.name = "New Resource";
|
||||
#else
|
||||
resource.name = std::filesystem::path(filePath).filename().string();
|
||||
#endif
|
||||
try {
|
||||
resource.data = std::make_shared<std::string>(read_file_to_string(filePath));
|
||||
}
|
||||
catch(...) {
|
||||
Logger::get().log(Logger::LogType::INFO, "[DrawingProgram::add_file_to_canvas_by_path] Could not open file " + filePath.string());
|
||||
return;
|
||||
}
|
||||
ImagePreprocess::preprocess_resource(resource.name, resource.data, ImagePreprocess::options_from_config(world.main.conf));
|
||||
NetworkingObjects::NetObjTemporaryPtr<ResourceData> imageTempPtr = world.rMan.add_resource(resource);
|
||||
if(imageTempPtr) {
|
||||
NetworkingObjects::NetObjID imageID = imageTempPtr.get_net_id();
|
||||
ResourceDisplay* display = world.rMan.get_display_data(imageID);
|
||||
|
|
@ -753,6 +836,7 @@ CanvasComponentContainer::ObjInfo* DrawingProgram::add_file_to_canvas_by_data(co
|
|||
ResourceData newResource;
|
||||
newResource.data = std::make_shared<std::string>(fileBuffer);
|
||||
newResource.name = fileName;
|
||||
ImagePreprocess::preprocess_resource(newResource.name, newResource.data, ImagePreprocess::options_from_config(world.main.conf));
|
||||
NetworkingObjects::NetObjID imageID = world.rMan.add_resource(newResource).get_net_id();
|
||||
ResourceDisplay* display = world.rMan.get_display_data(imageID);
|
||||
Vector2f imTrueDim = display->get_dimensions();
|
||||
|
|
@ -772,6 +856,32 @@ CanvasComponentContainer::ObjInfo* DrawingProgram::add_file_to_canvas_by_data(co
|
|||
return nullptr;
|
||||
}
|
||||
|
||||
void DrawingProgram::process_selected_images(bool freistellen, bool compress) {
|
||||
auto selected = selection.get_selection_as_set();
|
||||
ImagePreprocess::Options opts = ImagePreprocess::options_from_config(world.main.conf);
|
||||
opts.freistellenOnInsert = freistellen;
|
||||
opts.compressOnInsert = compress;
|
||||
if(!freistellen && !compress)
|
||||
return;
|
||||
for(auto* objInfo : selected) {
|
||||
if(objInfo->obj->get_comp().get_type() != CanvasComponentType::IMAGE)
|
||||
continue;
|
||||
auto& img = static_cast<ImageCanvasComponent&>(objInfo->obj->get_comp());
|
||||
auto resource = world.netObjMan.get_obj_temporary_ref_from_id<ResourceData>(img.d.imageID);
|
||||
if(!resource || !resource->data)
|
||||
continue;
|
||||
ResourceData newRes;
|
||||
newRes.name = resource->name;
|
||||
newRes.data = std::make_shared<std::string>(*resource->data);
|
||||
if(!ImagePreprocess::preprocess_resource(newRes.name, newRes.data, opts))
|
||||
continue;
|
||||
img.d.imageID = world.rMan.add_resource(newRes).get_net_id();
|
||||
objInfo->obj->send_comp_update(*this, true);
|
||||
objInfo->obj->commit_update(*this);
|
||||
}
|
||||
world.rMan.clear_display_cache();
|
||||
}
|
||||
|
||||
float DrawingProgram::drag_point_radius() {
|
||||
return (world.main.g.gui.io.isTouchDevice ? 10.0f : 8.0f) * world.main.g.final_gui_scale();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,6 +79,9 @@ class DrawingProgram {
|
|||
void invalidate_cache_at_component(CanvasComponentContainer::ObjInfo* objToCheck);
|
||||
void preupdate_component(CanvasComponentContainer::ObjInfo* objToCheck);
|
||||
void send_transforms_for(const std::vector<CanvasComponentContainer::ObjInfo*>& objsToSendTransformsFor);
|
||||
void send_group_ids_for(const std::vector<CanvasComponentContainer::ObjInfo*>& objs);
|
||||
void force_clients_to_own_view();
|
||||
void process_selected_images(bool freistellen, bool compress);
|
||||
|
||||
void on_tab_out();
|
||||
void input_add_file_to_canvas_callback(const CustomEvents::AddFileToCanvasEvent& addFile);
|
||||
|
|
@ -105,6 +108,8 @@ class DrawingProgram {
|
|||
std::unique_ptr<DrawingProgramToolBase> drawTool;
|
||||
private:
|
||||
void process_transform_message(const std::vector<std::pair<NetworkingObjects::NetObjID, CoordSpaceHelper>>& transforms);
|
||||
void process_group_id_message(const std::vector<std::pair<NetworkingObjects::NetObjID, uint64_t>>& groupIds);
|
||||
void process_force_camera_jump(const CoordSpaceHelper& coords, const Vector2f& windowSize);
|
||||
|
||||
void drag_drop_update();
|
||||
void check_updateable_components();
|
||||
|
|
|
|||
|
|
@ -44,6 +44,9 @@
|
|||
#include "../GUIStuff/ElementHelpers/RadioButtonHelpers.hpp"
|
||||
#include "../GUIStuff/ElementHelpers/LayoutHelpers.hpp"
|
||||
#include "../GUIStuff/ElementHelpers/ButtonHelpers.hpp"
|
||||
#include <Helpers/Random.hpp>
|
||||
#include <limits>
|
||||
#include <unordered_set>
|
||||
|
||||
DrawingProgramSelection::DrawingProgramSelection(DrawingProgram& initDrawP):
|
||||
drawP(initDrawP)
|
||||
|
|
@ -75,6 +78,22 @@ void DrawingProgramSelection::selection_gui(Toolbar& t) {
|
|||
});
|
||||
text_label(gui, "Stroke Color");
|
||||
});
|
||||
left_to_right_line_layout(gui, [&]() {
|
||||
text_button(gui, "group selection", "Group", {
|
||||
.onClick = [&] { group_selection(); }
|
||||
});
|
||||
text_button(gui, "ungroup selection", "Ungroup", {
|
||||
.onClick = [&] { ungroup_selection(); }
|
||||
});
|
||||
});
|
||||
left_to_right_line_layout(gui, [&]() {
|
||||
text_button(gui, "compress selection images", "Compress Images", {
|
||||
.onClick = [&] { drawP.process_selected_images(false, true); }
|
||||
});
|
||||
text_button(gui, "freistellen selection images", "Freistellen", {
|
||||
.onClick = [&] { drawP.process_selected_images(true, true); }
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -327,14 +346,50 @@ std::function<bool(const std::shared_ptr<DrawingProgramCacheBVHNode>&)> DrawingP
|
|||
return toRet;
|
||||
}
|
||||
|
||||
void DrawingProgramSelection::expand_selection_with_groups() {
|
||||
std::unordered_set<uint64_t> groupIds;
|
||||
for(auto& c : selectedSet) {
|
||||
if(c->obj->groupId != 0)
|
||||
groupIds.insert(c->obj->groupId);
|
||||
}
|
||||
if(groupIds.empty())
|
||||
return;
|
||||
std::unordered_set<CanvasComponentContainer::ObjInfo*> already(selectedSet.begin(), selectedSet.end());
|
||||
for(auto* c : drawP.layerMan.get_flattened_component_list()) {
|
||||
if(c->obj->groupId != 0 && groupIds.contains(c->obj->groupId) && !already.contains(c)) {
|
||||
selectedSet.push_back(c);
|
||||
already.insert(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DrawingProgramSelection::group_selection() {
|
||||
if(selectedSet.size() < 2)
|
||||
return;
|
||||
uint64_t newGroupId = Random::get().int_range<uint64_t>(1, std::numeric_limits<uint64_t>::max());
|
||||
for(auto& c : selectedSet)
|
||||
c->obj->groupId = newGroupId;
|
||||
drawP.send_group_ids_for(selectedSet);
|
||||
}
|
||||
|
||||
void DrawingProgramSelection::ungroup_selection() {
|
||||
if(selectedSet.empty())
|
||||
return;
|
||||
for(auto& c : selectedSet)
|
||||
c->obj->groupId = 0;
|
||||
drawP.send_group_ids_for(selectedSet);
|
||||
}
|
||||
|
||||
void DrawingProgramSelection::add_to_selection(const std::vector<CanvasComponentContainer::ObjInfo*>& newSelection) {
|
||||
selectedSet.insert(selectedSet.end(), newSelection.begin(), newSelection.end());
|
||||
expand_selection_with_groups();
|
||||
sort_selection();
|
||||
calculate_aabb();
|
||||
}
|
||||
|
||||
void DrawingProgramSelection::set_to_selection(const std::vector<CanvasComponentContainer::ObjInfo*>& newSelection) {
|
||||
selectedSet = newSelection;
|
||||
expand_selection_with_groups();
|
||||
sort_selection();
|
||||
calculate_aabb();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,6 +82,9 @@ class DrawingProgramSelection {
|
|||
|
||||
void set_to_selection(const std::vector<CanvasComponentContainer::ObjInfo*>& newSelection);
|
||||
void add_to_selection(const std::vector<CanvasComponentContainer::ObjInfo*>& newSelection);
|
||||
void expand_selection_with_groups();
|
||||
void group_selection();
|
||||
void ungroup_selection();
|
||||
void calculate_aabb();
|
||||
void reset_all();
|
||||
void reset_transform_data();
|
||||
|
|
|
|||
|
|
@ -69,6 +69,12 @@ nlohmann::json GlobalConfig::get_config_json(const InputManager& input) const {
|
|||
toRet["flipZoomToolDirection"] = flipZoomToolDirection;
|
||||
toRet["realTimeEraser"] = realTimeEraser;
|
||||
toRet["disableTouchForDrawing"] = disableTouchForDrawing;
|
||||
toRet["imageCompressOnInsert"] = imageCompressOnInsert;
|
||||
toRet["imageCompressFormat"] = imageCompressFormat;
|
||||
toRet["imageWebpQuality"] = imageWebpQuality;
|
||||
toRet["imageFreistellenOnInsert"] = imageFreistellenOnInsert;
|
||||
toRet["imageFreistellenMode"] = imageFreistellenMode;
|
||||
toRet["imageWhiteKeyThreshold"] = imageWhiteKeyThreshold;
|
||||
#ifndef __EMSCRIPTEN__
|
||||
toRet["checkForUpdates"] = checkForUpdates;
|
||||
#endif
|
||||
|
|
@ -147,6 +153,12 @@ void GlobalConfig::set_config_json(InputManager& input, const nlohmann::json& j,
|
|||
try{j.at("defaultCanvasBackgroundColor").get_to(defaultCanvasBackgroundColor);} catch(...) {}
|
||||
try{j.at("flipZoomToolDirection").get_to(flipZoomToolDirection);} catch(...) {}
|
||||
try{j.at("realTimeEraser").get_to(realTimeEraser);} catch(...) {}
|
||||
try{j.at("imageCompressOnInsert").get_to(imageCompressOnInsert);} catch(...) {}
|
||||
try{j.at("imageCompressFormat").get_to(imageCompressFormat);} catch(...) {}
|
||||
try{j.at("imageWebpQuality").get_to(imageWebpQuality);} catch(...) {}
|
||||
try{j.at("imageFreistellenOnInsert").get_to(imageFreistellenOnInsert);} catch(...) {}
|
||||
try{j.at("imageFreistellenMode").get_to(imageFreistellenMode);} catch(...) {}
|
||||
try{j.at("imageWhiteKeyThreshold").get_to(imageWhiteKeyThreshold);} catch(...) {}
|
||||
#ifndef __EMSCRIPTEN__
|
||||
try{j.at("checkForUpdates").get_to(checkForUpdates);} catch(...) {}
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
#include <nlohmann/json.hpp>
|
||||
#include <Helpers/VersionNumber.hpp>
|
||||
#include "InputManager.hpp"
|
||||
#include "ImagePreprocess.hpp"
|
||||
|
||||
#define DEFAULT_CANVAS_BACKGROUND_COLOR Vector3f{0.07f, 0.07f, 0.07f}
|
||||
|
||||
|
|
@ -112,6 +113,14 @@ class GlobalConfig {
|
|||
std::string displayName;
|
||||
bool flipZoomToolDirection = false;
|
||||
|
||||
// Fork: image insert options
|
||||
bool imageCompressOnInsert = true;
|
||||
ImagePreprocess::CompressFormat imageCompressFormat = ImagePreprocess::CompressFormat::WEBP;
|
||||
int imageWebpQuality = 80;
|
||||
bool imageFreistellenOnInsert = false;
|
||||
ImagePreprocess::FreistellenMode imageFreistellenMode = ImagePreprocess::FreistellenMode::WHITE_KEY;
|
||||
int imageWhiteKeyThreshold = 245;
|
||||
|
||||
bool disableGraphicsDriverWorkarounds = false;
|
||||
int vsyncValue = 0;
|
||||
|
||||
|
|
@ -135,3 +144,16 @@ NLOHMANN_JSON_SERIALIZE_ENUM(GlobalConfig::AntiAliasing, {
|
|||
{GlobalConfig::AntiAliasing::SKIA, "Skia"},
|
||||
{GlobalConfig::AntiAliasing::DYNAMIC_MSAA, "Dynamic MSAA"},
|
||||
})
|
||||
|
||||
NLOHMANN_JSON_SERIALIZE_ENUM(ImagePreprocess::CompressFormat, {
|
||||
{ImagePreprocess::CompressFormat::KEEP, "Keep"},
|
||||
{ImagePreprocess::CompressFormat::WEBP, "WebP"},
|
||||
{ImagePreprocess::CompressFormat::PNG, "PNG"},
|
||||
})
|
||||
|
||||
NLOHMANN_JSON_SERIALIZE_ENUM(ImagePreprocess::FreistellenMode, {
|
||||
{ImagePreprocess::FreistellenMode::OFF, "Off"},
|
||||
{ImagePreprocess::FreistellenMode::WHITE_KEY, "WhiteKey"},
|
||||
{ImagePreprocess::FreistellenMode::REMBG_SCHNELL, "RembgSchnell"},
|
||||
{ImagePreprocess::FreistellenMode::REMBG_ALLGEMEIN, "RembgAllgemein"},
|
||||
})
|
||||
|
|
|
|||
315
src/ImagePreprocess.cpp
Normal file
315
src/ImagePreprocess.cpp
Normal file
|
|
@ -0,0 +1,315 @@
|
|||
/*
|
||||
* InfiniPaint (fork)
|
||||
* Image insert preprocessing: freistellen + compression options.
|
||||
*/
|
||||
|
||||
#include "ImagePreprocess.hpp"
|
||||
#include "GlobalConfig.hpp"
|
||||
#include <Helpers/Logger.hpp>
|
||||
|
||||
#include <include/codec/SkBmpDecoder.h>
|
||||
#include <include/codec/SkCodec.h>
|
||||
#include <include/codec/SkGifDecoder.h>
|
||||
#include <include/codec/SkIcoDecoder.h>
|
||||
#include <include/codec/SkJpegDecoder.h>
|
||||
#include <include/codec/SkPngDecoder.h>
|
||||
#include <include/codec/SkWbmpDecoder.h>
|
||||
#include <include/codec/SkWebpDecoder.h>
|
||||
#include <include/core/SkBitmap.h>
|
||||
#include <include/core/SkData.h>
|
||||
#include <include/core/SkPixmap.h>
|
||||
#include <include/core/SkStream.h>
|
||||
#include <include/encode/SkPngEncoder.h>
|
||||
#include <include/encode/SkWebpEncoder.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <optional>
|
||||
#include <random>
|
||||
#include <vector>
|
||||
|
||||
#ifndef __EMSCRIPTEN__
|
||||
#include <cstdlib>
|
||||
#include <array>
|
||||
#endif
|
||||
|
||||
namespace ImagePreprocess {
|
||||
namespace {
|
||||
|
||||
std::array<const SkCodecs::Decoder, 7> imageDecoders = {
|
||||
SkBmpDecoder::Decoder(),
|
||||
SkGifDecoder::Decoder(),
|
||||
SkIcoDecoder::Decoder(),
|
||||
SkJpegDecoder::Decoder(),
|
||||
SkPngDecoder::Decoder(),
|
||||
SkWbmpDecoder::Decoder(),
|
||||
SkWebpDecoder::Decoder()
|
||||
};
|
||||
|
||||
std::string to_lower(std::string s) {
|
||||
std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
return s;
|
||||
}
|
||||
|
||||
std::string replace_extension(std::string name, const char* newExt) {
|
||||
auto slash = name.find_last_of("/\\");
|
||||
auto baseStart = slash == std::string::npos ? 0 : slash + 1;
|
||||
auto dot = name.find_last_of('.');
|
||||
if(dot != std::string::npos && dot > baseStart)
|
||||
name = name.substr(0, dot);
|
||||
name += newExt;
|
||||
return name;
|
||||
}
|
||||
|
||||
bool decode_first_frame(const std::string& data, SkBitmap& outBitmap) {
|
||||
sk_sp<SkData> skData = SkData::MakeWithoutCopy(data.data(), data.size());
|
||||
std::unique_ptr<SkCodec> codec = SkCodec::MakeFromData(skData, imageDecoders);
|
||||
if(!codec)
|
||||
return false;
|
||||
SkImageInfo info = codec->getInfo().makeColorType(kRGBA_8888_SkColorType).makeAlphaType(kUnpremul_SkAlphaType);
|
||||
if(!outBitmap.tryAllocPixels(info))
|
||||
return false;
|
||||
SkCodec::Options opts;
|
||||
opts.fFrameIndex = 0;
|
||||
if(codec->getPixels(info, outBitmap.getPixels(), outBitmap.rowBytes(), &opts) != SkCodec::Result::kSuccess)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void apply_white_key(SkBitmap& bitmap, int threshold) {
|
||||
threshold = std::clamp(threshold, 0, 255);
|
||||
SkPixmap pm;
|
||||
if(!bitmap.peekPixels(&pm))
|
||||
return;
|
||||
const int w = pm.width();
|
||||
const int h = pm.height();
|
||||
for(int y = 0; y < h; y++) {
|
||||
auto* row = reinterpret_cast<uint8_t*>(pm.writable_addr(0, y));
|
||||
for(int x = 0; x < w; x++) {
|
||||
uint8_t* p = row + x * 4;
|
||||
if(p[0] >= threshold && p[1] >= threshold && p[2] >= threshold) {
|
||||
p[3] = 0;
|
||||
}
|
||||
else if(p[0] > threshold - 25 && p[1] > threshold - 25 && p[2] > threshold - 25) {
|
||||
// Soft edge: fade alpha near threshold
|
||||
int minC = std::min({p[0], p[1], p[2]});
|
||||
int fade = threshold - minC;
|
||||
int alpha = (fade * 255) / 25;
|
||||
p[3] = static_cast<uint8_t>(std::clamp(std::min<int>(p[3], alpha), 0, 255));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<std::string> encode_bitmap(const SkBitmap& bitmap, CompressFormat format, int webpQuality) {
|
||||
SkPixmap pm;
|
||||
if(!bitmap.peekPixels(&pm))
|
||||
return std::nullopt;
|
||||
SkDynamicMemoryWStream out;
|
||||
bool ok = false;
|
||||
if(format == CompressFormat::WEBP) {
|
||||
SkWebpEncoder::Options opts;
|
||||
opts.fCompression = SkWebpEncoder::Compression::kLossy;
|
||||
opts.fQuality = static_cast<float>(std::clamp(webpQuality, 0, 100));
|
||||
ok = SkWebpEncoder::Encode(&out, pm, opts);
|
||||
}
|
||||
else {
|
||||
ok = SkPngEncoder::Encode(&out, pm, {});
|
||||
}
|
||||
if(!ok)
|
||||
return std::nullopt;
|
||||
out.flush();
|
||||
auto skData = out.detachAsData();
|
||||
if(!skData)
|
||||
return std::nullopt;
|
||||
return std::string(reinterpret_cast<const char*>(skData->bytes()), skData->size());
|
||||
}
|
||||
|
||||
#ifndef __EMSCRIPTEN__
|
||||
std::optional<std::string> find_rembg_bin() {
|
||||
const char* envBin = std::getenv("TOWER_REMBG_BIN");
|
||||
if(envBin && envBin[0] && std::filesystem::is_regular_file(envBin))
|
||||
return std::string(envBin);
|
||||
const char* home = std::getenv("HOME");
|
||||
if(home) {
|
||||
std::filesystem::path venv = std::filesystem::path(home) / ".local/share/tower-rembg/bin/rembg";
|
||||
if(std::filesystem::is_regular_file(venv))
|
||||
return venv.string();
|
||||
std::filesystem::path local = std::filesystem::path(home) / ".local/bin/rembg";
|
||||
if(std::filesystem::is_regular_file(local))
|
||||
return local.string();
|
||||
}
|
||||
// PATH lookup via `command -v`
|
||||
FILE* pipe = popen("command -v rembg 2>/dev/null", "r");
|
||||
if(!pipe)
|
||||
return std::nullopt;
|
||||
char buf[512];
|
||||
std::string path;
|
||||
while(fgets(buf, sizeof(buf), pipe))
|
||||
path += buf;
|
||||
pclose(pipe);
|
||||
while(!path.empty() && (path.back() == '\n' || path.back() == '\r'))
|
||||
path.pop_back();
|
||||
if(!path.empty() && std::filesystem::is_regular_file(path))
|
||||
return path;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<std::string> run_rembg(const std::string& inputData, const std::string& model) {
|
||||
auto rembg = find_rembg_bin();
|
||||
if(!rembg) {
|
||||
Logger::get().log(Logger::LogType::INFO, "[ImagePreprocess] rembg not found — falling back to white-key");
|
||||
return std::nullopt;
|
||||
}
|
||||
std::error_code ec;
|
||||
auto tmpDir = std::filesystem::temp_directory_path(ec);
|
||||
if(ec)
|
||||
return std::nullopt;
|
||||
auto inPath = tmpDir / ("infinipaint-rembg-in-" + std::to_string(std::random_device{}()) + ".png");
|
||||
auto outPath = tmpDir / ("infinipaint-rembg-out-" + std::to_string(std::random_device{}()) + ".png");
|
||||
|
||||
{
|
||||
std::ofstream out(inPath, std::ios::binary);
|
||||
if(!out)
|
||||
return std::nullopt;
|
||||
out.write(inputData.data(), static_cast<std::streamsize>(inputData.size()));
|
||||
}
|
||||
|
||||
std::string cmd = "\"" + *rembg + "\" i -m " + model + " \"" + inPath.string() + "\" \"" + outPath.string() + "\" 2>/dev/null";
|
||||
int rc = std::system(cmd.c_str());
|
||||
std::optional<std::string> result;
|
||||
if(rc == 0 && std::filesystem::is_regular_file(outPath)) {
|
||||
std::ifstream in(outPath, std::ios::binary);
|
||||
if(in) {
|
||||
result = std::string(std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>());
|
||||
if(result->empty())
|
||||
result.reset();
|
||||
}
|
||||
}
|
||||
std::filesystem::remove(inPath, ec);
|
||||
std::filesystem::remove(outPath, ec);
|
||||
return result;
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
bool looks_like_raster_image(std::string_view name, std::string_view data) {
|
||||
std::string lower = to_lower(std::string(name));
|
||||
auto hasExt = [&](const char* ext) {
|
||||
return lower.size() >= std::strlen(ext) && lower.compare(lower.size() - std::strlen(ext), std::strlen(ext), ext) == 0;
|
||||
};
|
||||
if(hasExt(".png") || hasExt(".jpg") || hasExt(".jpeg") || hasExt(".webp") || hasExt(".bmp") || hasExt(".gif") || hasExt(".ico"))
|
||||
return true;
|
||||
if(data.size() >= 8) {
|
||||
// PNG / JPEG / RIFF WEBP / GIF / BMP magic
|
||||
const auto* b = reinterpret_cast<const unsigned char*>(data.data());
|
||||
if(b[0] == 0x89 && b[1] == 'P' && b[2] == 'N' && b[3] == 'G')
|
||||
return true;
|
||||
if(b[0] == 0xFF && b[1] == 0xD8)
|
||||
return true;
|
||||
if(b[0] == 'R' && b[1] == 'I' && b[2] == 'F' && b[3] == 'F')
|
||||
return true;
|
||||
if(b[0] == 'G' && b[1] == 'I' && b[2] == 'F')
|
||||
return true;
|
||||
if(b[0] == 'B' && b[1] == 'M')
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Options options_from_config(const GlobalConfig& conf) {
|
||||
Options o;
|
||||
o.compressOnInsert = conf.imageCompressOnInsert;
|
||||
o.compressFormat = conf.imageCompressFormat;
|
||||
o.webpQuality = conf.imageWebpQuality;
|
||||
o.freistellenOnInsert = conf.imageFreistellenOnInsert;
|
||||
o.freistellenMode = conf.imageFreistellenMode;
|
||||
o.whiteKeyThreshold = conf.imageWhiteKeyThreshold;
|
||||
return o;
|
||||
}
|
||||
|
||||
bool preprocess_resource(std::string& name, std::shared_ptr<std::string>& data, const Options& options) {
|
||||
if(!data || data->empty())
|
||||
return false;
|
||||
if(!looks_like_raster_image(name, *data))
|
||||
return false;
|
||||
|
||||
bool wantFreistellen = options.freistellenOnInsert && options.freistellenMode != FreistellenMode::OFF;
|
||||
bool wantCompress = options.compressOnInsert && options.compressFormat != CompressFormat::KEEP;
|
||||
if(!wantFreistellen && !wantCompress)
|
||||
return false;
|
||||
|
||||
std::string working = *data;
|
||||
bool changed = false;
|
||||
|
||||
if(wantFreistellen) {
|
||||
#ifndef __EMSCRIPTEN__
|
||||
if(options.freistellenMode == FreistellenMode::REMBG_SCHNELL || options.freistellenMode == FreistellenMode::REMBG_ALLGEMEIN) {
|
||||
const char* model = options.freistellenMode == FreistellenMode::REMBG_SCHNELL ? "silueta" : "bria-rmbg";
|
||||
// rembg wants an image file; feed current bytes (encode as PNG first if needed)
|
||||
SkBitmap bmp;
|
||||
std::string rembgInput = working;
|
||||
if(decode_first_frame(working, bmp)) {
|
||||
auto png = encode_bitmap(bmp, CompressFormat::PNG, 100);
|
||||
if(png)
|
||||
rembgInput = *png;
|
||||
}
|
||||
if(auto rembgOut = run_rembg(rembgInput, model)) {
|
||||
working = *rembgOut;
|
||||
changed = true;
|
||||
name = replace_extension(name, ".png");
|
||||
}
|
||||
else {
|
||||
// Fallback white-key
|
||||
if(decode_first_frame(working, bmp)) {
|
||||
apply_white_key(bmp, options.whiteKeyThreshold);
|
||||
if(auto enc = encode_bitmap(bmp, CompressFormat::PNG, 100)) {
|
||||
working = *enc;
|
||||
changed = true;
|
||||
name = replace_extension(name, ".png");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
#endif
|
||||
{
|
||||
SkBitmap bmp;
|
||||
if(decode_first_frame(working, bmp)) {
|
||||
apply_white_key(bmp, options.whiteKeyThreshold);
|
||||
if(auto enc = encode_bitmap(bmp, CompressFormat::PNG, 100)) {
|
||||
working = *enc;
|
||||
changed = true;
|
||||
name = replace_extension(name, ".png");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(wantCompress) {
|
||||
SkBitmap bmp;
|
||||
if(decode_first_frame(working, bmp)) {
|
||||
CompressFormat fmt = options.compressFormat;
|
||||
if(auto enc = encode_bitmap(bmp, fmt, options.webpQuality)) {
|
||||
// Keep result if smaller, or if freistellen required alpha-friendly format
|
||||
bool preferNew = enc->size() < working.size() || wantFreistellen || fmt == CompressFormat::WEBP;
|
||||
if(preferNew) {
|
||||
working = *enc;
|
||||
changed = true;
|
||||
name = replace_extension(name, fmt == CompressFormat::WEBP ? ".webp" : ".png");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(changed)
|
||||
data = std::make_shared<std::string>(std::move(working));
|
||||
return changed;
|
||||
}
|
||||
|
||||
}
|
||||
45
src/ImagePreprocess.hpp
Normal file
45
src/ImagePreprocess.hpp
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* InfiniPaint (fork)
|
||||
* Image insert preprocessing: freistellen + compression options.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
class GlobalConfig;
|
||||
|
||||
namespace ImagePreprocess {
|
||||
|
||||
enum class CompressFormat : uint8_t {
|
||||
KEEP = 0,
|
||||
WEBP,
|
||||
PNG
|
||||
};
|
||||
|
||||
enum class FreistellenMode : uint8_t {
|
||||
OFF = 0,
|
||||
WHITE_KEY, // Near-white → transparent (always available)
|
||||
REMBG_SCHNELL, // rembg -m silueta (Tower freistellen/schnell)
|
||||
REMBG_ALLGEMEIN // rembg -m bria-rmbg (Tower freistellen/allgemein)
|
||||
};
|
||||
|
||||
struct Options {
|
||||
bool compressOnInsert = true;
|
||||
CompressFormat compressFormat = CompressFormat::WEBP;
|
||||
int webpQuality = 80; // 0–100, lossy
|
||||
bool freistellenOnInsert = false;
|
||||
FreistellenMode freistellenMode = FreistellenMode::WHITE_KEY;
|
||||
int whiteKeyThreshold = 245; // 0–255, RGB channels all >= threshold → transparent
|
||||
};
|
||||
|
||||
// Mutates name + data in place when preprocessing applies. Returns true if changed.
|
||||
bool preprocess_resource(std::string& name, std::shared_ptr<std::string>& data, const Options& options);
|
||||
|
||||
Options options_from_config(const GlobalConfig& conf);
|
||||
|
||||
bool looks_like_raster_image(std::string_view name, std::string_view data);
|
||||
|
||||
}
|
||||
|
|
@ -346,6 +346,14 @@ void PhoneDrawingProgramScreen::top_toolbar_settings_popup() {
|
|||
.floating = {.offset = {.x = 0, .y = static_cast<float>(io.theme->padding1)}, .zIndex = gui.get_z_index(), .attachPoints = {.element = CLAY_ATTACH_POINT_RIGHT_TOP, .parent = CLAY_ATTACH_POINT_RIGHT_BOTTOM}, .attachTo = CLAY_ATTACH_TO_PARENT}
|
||||
}) {
|
||||
text_label_centered(gui, "Player List");
|
||||
if(main.world->netServer) {
|
||||
text_button(gui, "force all jump to host phone", "Bring Everyone Here", {
|
||||
.wide = true,
|
||||
.onClick = [&] {
|
||||
main.world->drawProg.force_clients_to_own_view();
|
||||
}
|
||||
});
|
||||
}
|
||||
left_to_right_line_layout(gui, [&]() {
|
||||
CLAY_AUTO_ID({
|
||||
.layout = {
|
||||
|
|
|
|||
|
|
@ -1211,6 +1211,14 @@ void Toolbar::player_list() {
|
|||
gui.new_id("client list", [&] {
|
||||
text_label_centered(gui, "Player List");
|
||||
if(!main.world->clientStillConnecting) {
|
||||
if(main.world->netServer) {
|
||||
text_button(gui, "force all jump to host", "Bring Everyone Here", {
|
||||
.wide = true,
|
||||
.onClick = [&] {
|
||||
main.world->drawProg.force_clients_to_own_view();
|
||||
}
|
||||
});
|
||||
}
|
||||
left_to_right_line_layout(gui, [&]() {
|
||||
CLAY_AUTO_ID({
|
||||
.layout = {
|
||||
|
|
@ -1463,6 +1471,7 @@ void Toolbar::general_settings_inner_gui() {
|
|||
});
|
||||
};
|
||||
category_button("Generalbutton", "General", GSETTINGS_GENERAL);
|
||||
category_button("Imagesbutton", "Images", GSETTINGS_IMAGES);
|
||||
category_button("Graphicsbutton", "Graphics", GSETTINGS_GRAPHICS);
|
||||
category_button("Tabletbutton", "Tablet", GSETTINGS_TABLET);
|
||||
category_button("Themebutton", "Theme", GSETTINGS_THEME);
|
||||
|
|
@ -1520,6 +1529,35 @@ void Toolbar::general_settings_inner_gui() {
|
|||
checkbox_boolean_field(gui, "force extension on path", "Force extension on path when saving files", &main.conf.forceExtensionOnPath);
|
||||
#ifdef ADD_PREFER_X11_OPTION
|
||||
checkbox_boolean_field(gui, "prefer x11", "Prefer X11 over Wayland (Requires restart)", &main.conf.preferX11);
|
||||
#endif
|
||||
});
|
||||
break;
|
||||
}
|
||||
case GSETTINGS_IMAGES: {
|
||||
general_scroll_area("image settings", [&] {
|
||||
text_label(gui, "On insert / paste:");
|
||||
checkbox_boolean_field(gui, "compress images on insert", "Compress images automatically", &main.conf.imageCompressOnInsert);
|
||||
text_label(gui, "Compression format:");
|
||||
radio_button_selector(gui, "image compress format", &main.conf.imageCompressFormat, {
|
||||
{"Keep original", ImagePreprocess::CompressFormat::KEEP},
|
||||
{"WebP", ImagePreprocess::CompressFormat::WEBP},
|
||||
{"PNG", ImagePreprocess::CompressFormat::PNG}
|
||||
});
|
||||
slider_scalar_field(gui, "webp quality", "WebP quality", &main.conf.imageWebpQuality, 1, 100);
|
||||
checkbox_boolean_field(gui, "freistellen on insert", "Freistellen / remove background on insert", &main.conf.imageFreistellenOnInsert);
|
||||
text_label(gui, "Freistellen mode:");
|
||||
radio_button_selector(gui, "freistellen mode", &main.conf.imageFreistellenMode, {
|
||||
{"Off", ImagePreprocess::FreistellenMode::OFF},
|
||||
{"White background", ImagePreprocess::FreistellenMode::WHITE_KEY},
|
||||
#ifndef __EMSCRIPTEN__
|
||||
{"Schnell (rembg silueta)", ImagePreprocess::FreistellenMode::REMBG_SCHNELL},
|
||||
{"Allgemein (rembg bria)", ImagePreprocess::FreistellenMode::REMBG_ALLGEMEIN},
|
||||
#endif
|
||||
});
|
||||
slider_scalar_field(gui, "white key threshold", "White-key threshold", &main.conf.imageWhiteKeyThreshold, 200, 255);
|
||||
text_label_light(gui, "Selection toolbar: Compress Images / Freistellen also work afterwards.");
|
||||
#ifndef __EMSCRIPTEN__
|
||||
text_label_light(gui, "rembg uses Tower venv (~/.local/share/tower-rembg) when present.");
|
||||
#endif
|
||||
});
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -169,6 +169,7 @@ class Toolbar {
|
|||
} optionsMenuType;
|
||||
enum GeneralSettingsOptions {
|
||||
GSETTINGS_GENERAL = 0,
|
||||
GSETTINGS_IMAGES,
|
||||
GSETTINGS_GRAPHICS,
|
||||
GSETTINGS_TABLET,
|
||||
GSETTINGS_THEME,
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ VersionNumber header_to_version_number(const std::string& header) {
|
|||
m["INFPNT000004"] = VersionNumber(0, 3, 0);
|
||||
m["INFPNT000005"] = VersionNumber(0, 4, 0);
|
||||
m["INFPNT000006"] = VersionNumber(0, 6, 0);
|
||||
m["INFPNT000007"] = VersionNumber(0, 7, 0);
|
||||
}
|
||||
auto it = m.find(header);
|
||||
if(it == m.end())
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ namespace VersionConstants {
|
|||
VersionNumber header_to_version_number(const std::string& header);
|
||||
|
||||
constexpr int SAVEFILE_HEADER_LEN = 12; // DO NOT CHANGE THIS HEADER LENGTH
|
||||
const std::string CURRENT_SAVEFILE_HEADER = "INFPNT000006"; // Change whenever the save file is incompatible with the previous version
|
||||
const std::string CURRENT_VERSION_STRING = "0.6.1";
|
||||
constexpr VersionNumber CURRENT_VERSION_NUMBER(0, 6, 1);
|
||||
const std::string CURRENT_SAVEFILE_HEADER = "INFPNT000007"; // Change whenever the save file is incompatible with the previous version
|
||||
const std::string CURRENT_VERSION_STRING = "0.7.0-fork";
|
||||
constexpr VersionNumber CURRENT_VERSION_NUMBER(0, 7, 0);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue