2026-02-23 14:12:45 +00:00
|
|
|
|
import os
|
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
|
|
from PySide6.QtCore import Qt
|
|
|
|
|
|
from PySide6.QtGui import QColor, QFont, QFontInfo, QIcon, QPalette
|
|
|
|
|
|
from PySide6.QtWidgets import (
|
|
|
|
|
|
QApplication,
|
|
|
|
|
|
QGridLayout,
|
|
|
|
|
|
QHBoxLayout,
|
|
|
|
|
|
QLabel,
|
|
|
|
|
|
QMainWindow,
|
|
|
|
|
|
QPushButton,
|
|
|
|
|
|
QWidget,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
from .autoslicer import Autoslicer, AutoslicerParams
|
|
|
|
|
|
from .controller import ScanSlicerControllerMixin
|
|
|
|
|
|
from .ui_builders import ScanSlicerUiBuilderMixin
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-02-23 14:29:20 +00:00
|
|
|
|
def _unwrap_qvariant(value):
|
|
|
|
|
|
unwrapped = value
|
|
|
|
|
|
for _ in range(4):
|
|
|
|
|
|
if hasattr(unwrapped, "variant"):
|
|
|
|
|
|
try:
|
|
|
|
|
|
candidate = unwrapped.variant()
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
candidate = unwrapped
|
|
|
|
|
|
if candidate is not unwrapped:
|
|
|
|
|
|
unwrapped = candidate
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
if hasattr(unwrapped, "value"):
|
|
|
|
|
|
try:
|
|
|
|
|
|
candidate = unwrapped.value()
|
|
|
|
|
|
except TypeError:
|
|
|
|
|
|
candidate = unwrapped.value
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
candidate = unwrapped
|
|
|
|
|
|
if candidate is not unwrapped:
|
|
|
|
|
|
unwrapped = candidate
|
|
|
|
|
|
continue
|
|
|
|
|
|
break
|
|
|
|
|
|
return unwrapped
|
2026-02-23 14:34:20 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _read_portal_dark_preference():
|
|
|
|
|
|
try:
|
|
|
|
|
|
from PySide6.QtDBus import QDBusConnection, QDBusInterface, QDBusMessage
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
bus = QDBusConnection.sessionBus()
|
|
|
|
|
|
if not bus.isConnected():
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
iface = QDBusInterface(
|
|
|
|
|
|
"org.freedesktop.portal.Desktop",
|
|
|
|
|
|
"/org/freedesktop/portal/desktop",
|
|
|
|
|
|
"org.freedesktop.portal.Settings",
|
|
|
|
|
|
bus,
|
|
|
|
|
|
)
|
|
|
|
|
|
if not iface.isValid():
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
for method in ("ReadOne", "Read"):
|
|
|
|
|
|
reply = iface.call(method, "org.freedesktop.appearance", "color-scheme")
|
|
|
|
|
|
if reply.type() == QDBusMessage.MessageType.ErrorMessage:
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
args = reply.arguments()
|
|
|
|
|
|
if not args:
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
raw_value = _unwrap_qvariant(args[0])
|
|
|
|
|
|
if isinstance(raw_value, dict) and "color-scheme" in raw_value:
|
|
|
|
|
|
raw_value = _unwrap_qvariant(raw_value["color-scheme"])
|
|
|
|
|
|
if isinstance(raw_value, (list, tuple)) and raw_value:
|
|
|
|
|
|
raw_value = _unwrap_qvariant(raw_value[0])
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
color_scheme = int(raw_value)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
# XDG portal color-scheme values:
|
|
|
|
|
|
# 0 = no preference, 1 = prefer dark, 2 = prefer light
|
|
|
|
|
|
if color_scheme == 1:
|
|
|
|
|
|
return True
|
|
|
|
|
|
if color_scheme == 2:
|
|
|
|
|
|
return False
|
|
|
|
|
|
return None
|
2026-02-23 14:39:20 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def apply_dark_theme_if_requested(app):
|
|
|
|
|
|
theme_value = os.environ.get("GTK_THEME", "").lower()
|
|
|
|
|
|
dark_theme_requested = "dark" in theme_value
|
|
|
|
|
|
if not dark_theme_requested:
|
|
|
|
|
|
portal_pref = _read_portal_dark_preference()
|
|
|
|
|
|
if portal_pref is not None:
|
|
|
|
|
|
dark_theme_requested = portal_pref
|
|
|
|
|
|
|
|
|
|
|
|
if not dark_theme_requested:
|
|
|
|
|
|
try:
|
|
|
|
|
|
dark_theme_requested = app.styleHints().colorScheme() == Qt.ColorScheme.Dark
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
dark_theme_requested = False
|
|
|
|
|
|
|
|
|
|
|
|
if not dark_theme_requested:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# Some container/desktop combinations do not propagate GTK dark theme
|
|
|
|
|
|
# correctly to Qt widgets; enforce a dark palette when dark mode is requested.
|
|
|
|
|
|
app.setStyle("Fusion")
|
|
|
|
|
|
palette = QPalette()
|
|
|
|
|
|
palette.setColor(QPalette.Window, QColor(53, 53, 53))
|
|
|
|
|
|
palette.setColor(QPalette.WindowText, QColor(255, 255, 255))
|
|
|
|
|
|
palette.setColor(QPalette.Base, QColor(35, 35, 35))
|
|
|
|
|
|
palette.setColor(QPalette.AlternateBase, QColor(53, 53, 53))
|
|
|
|
|
|
palette.setColor(QPalette.ToolTipBase, QColor(35, 35, 35))
|
|
|
|
|
|
palette.setColor(QPalette.ToolTipText, QColor(255, 255, 255))
|
|
|
|
|
|
palette.setColor(QPalette.Text, QColor(255, 255, 255))
|
|
|
|
|
|
palette.setColor(QPalette.Button, QColor(53, 53, 53))
|
|
|
|
|
|
palette.setColor(QPalette.ButtonText, QColor(255, 255, 255))
|
|
|
|
|
|
palette.setColor(QPalette.BrightText, QColor(255, 0, 0))
|
|
|
|
|
|
palette.setColor(QPalette.Highlight, QColor(42, 130, 218))
|
|
|
|
|
|
palette.setColor(QPalette.HighlightedText, QColor(255, 255, 255))
|
|
|
|
|
|
app.setPalette(palette)
|
2026-02-23 14:44:20 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def apply_global_text_size_offset(app, pixel_offset=2):
|
|
|
|
|
|
font = app.font()
|
|
|
|
|
|
info = QFontInfo(font)
|
|
|
|
|
|
pixel_size = info.pixelSize()
|
|
|
|
|
|
|
|
|
|
|
|
if pixel_size > 0:
|
|
|
|
|
|
font.setPixelSize(pixel_size + pixel_offset)
|
|
|
|
|
|
else:
|
|
|
|
|
|
point_size = info.pointSizeF()
|
|
|
|
|
|
if point_size <= 0:
|
|
|
|
|
|
point_size = 10.0
|
|
|
|
|
|
font.setPointSizeF(point_size + (pixel_offset * 0.75))
|
|
|
|
|
|
|
|
|
|
|
|
app.setFont(font)
|
2026-02-23 14:49:20 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def resolve_app_icon():
|
|
|
|
|
|
icon = QIcon.fromTheme("scanslicer")
|
|
|
|
|
|
if not icon.isNull():
|
|
|
|
|
|
return icon
|
|
|
|
|
|
|
|
|
|
|
|
appdir = os.environ.get("APPDIR", "").strip()
|
|
|
|
|
|
if not appdir:
|
|
|
|
|
|
return QIcon()
|
|
|
|
|
|
|
|
|
|
|
|
candidates = (
|
|
|
|
|
|
os.path.join(appdir, "usr", "share", "icons", "scanslicer.png"),
|
|
|
|
|
|
os.path.join(appdir, "usr", "share", "icons", "scanslicer.svg"),
|
|
|
|
|
|
os.path.join(appdir, "usr", "share", "pixmaps", "scanslicer.png"),
|
|
|
|
|
|
os.path.join(appdir, "usr", "share", "pixmaps", "scanslicer.svg"),
|
|
|
|
|
|
os.path.join(appdir, ".DirIcon"),
|
|
|
|
|
|
)
|
|
|
|
|
|
for candidate_path in candidates:
|
|
|
|
|
|
if not os.path.exists(candidate_path):
|
|
|
|
|
|
continue
|
|
|
|
|
|
candidate_icon = QIcon(candidate_path)
|
|
|
|
|
|
if not candidate_icon.isNull():
|
|
|
|
|
|
return candidate_icon
|
|
|
|
|
|
|
|
|
|
|
|
return QIcon()
|
2026-02-24 12:23:36 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class QtScanSlicer(QMainWindow, ScanSlicerUiBuilderMixin, ScanSlicerControllerMixin):
|
|
|
|
|
|
CONTROL_PANEL_WIDTH = 370
|
|
|
|
|
|
STEPPER_WIDTH = 75
|
|
|
|
|
|
PANEL_CONTENT_MARGIN = 10
|
|
|
|
|
|
VERTICAL_SPACER_HEIGHT = 8
|
|
|
|
|
|
SECTION_TITLE_FONT_PX = 18
|
|
|
|
|
|
SECTION_TITLE_VPAD = 5
|
|
|
|
|
|
CHOICE_PARAM_OPTIONS = {
|
|
|
|
|
|
"detect_pipeline": [("Threshold", 0), ("Canny", 1)],
|
|
|
|
|
|
"bw_method": [("Simple", 0), ("Gauss", 1), ("Otsu", 2)],
|
|
|
|
|
|
"invert_mode": [("Auto", 0), ("Off", 1), ("On", 2)],
|
|
|
|
|
|
"morph_op": [("None", 0), ("Dilate", 1), ("Erode", 2), ("Open", 3), ("Close", 4)],
|
|
|
|
|
|
}
|
|
|
|
|
|
ADVANCED_PARAMS = {
|
|
|
|
|
|
"detect_pipeline",
|
|
|
|
|
|
"clahe_clip",
|
|
|
|
|
|
"gaussian",
|
|
|
|
|
|
"bw_adaptive_c",
|
|
|
|
|
|
"invert_mode",
|
|
|
|
|
|
"contour_approx_pct",
|
|
|
|
|
|
"bbox_max_size",
|
|
|
|
|
|
"aspect_ratio_min",
|
|
|
|
|
|
"aspect_ratio_max",
|
|
|
|
|
|
"border_ignore",
|
|
|
|
|
|
"iou_merge_thresh",
|
|
|
|
|
|
}
|
|
|
|
|
|
LOCK_ICON_DATA_URL = (
|
|
|
|
|
|
"data:image/svg+xml;base64,"
|
|
|
|
|
|
"PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIg"
|
|
|
|
|
|
"dmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdp"
|
|
|
|
|
|
"ZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdW"
|
|
|
|
|
|
"NpZGUgbHVjaWRlLWxvY2staWNvbiBsdWNpZGUtbG9jayI+PHJlY3Qgd2lkdGg9IjE4IiBoZWlnaHQ9IjExIiB4"
|
|
|
|
|
|
"PSIzIiB5PSIxMSIgcng9IjIiIHJ5PSIyIi8+PHBhdGggZD0iTTcgMTFWN2E1IDUgMCAwIDEgMTAgMHY0Ii8+PC"
|
|
|
|
|
|
"9zdmc+"
|
|
|
|
|
|
)
|
|
|
|
|
|
SQUARIZE_ICON_DATA_URL = (
|
|
|
|
|
|
"data:image/svg+xml;base64,"
|
|
|
|
|
|
"PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIg"
|
|
|
|
|
|
"dmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdp"
|
|
|
|
|
|
"ZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdW"
|
|
|
|
|
|
"NpZGUgbHVjaWRlLXNxdWFyZS1pY29uIGx1Y2lkZS1zcXVhcmUiPjxyZWN0IHdpZHRoPSIxOCIgaGVpZ2h0PSIx"
|
|
|
|
|
|
"OCIgeD0iMyIgeT0iMyIgcng9IjIiLz48L3N2Zz4="
|
|
|
|
|
|
)
|
|
|
|
|
|
ROTATE_ICON_DATA_URL = (
|
|
|
|
|
|
"data:image/svg+xml;base64,"
|
|
|
|
|
|
"PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIg"
|
|
|
|
|
|
"dmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdp"
|
|
|
|
|
|
"ZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdW"
|
|
|
|
|
|
"NpZGUgbHVjaWRlLXJlZnJlc2gtY2N3LWRvdC1pY29uIGx1Y2lkZS1yZWZyZXNoLWNjdy1kb3QiPjxwYXRoIGQ9"
|
|
|
|
|
|
"Ik0yMSAxMmE5IDkgMCAwIDAtOS05IDkuNzUgOS43NSAwIDAgMC02Ljc0IDIuNzRMMyA4Ii8+PHBhdGggZD0i"
|
|
|
|
|
|
"TTMgM3Y1aDUiLz48cGF0aCBkPSJNMyAxMmE5IDkgMCAwIDAgOSA5IDkuNzUgOS43NSAwIDAgMCA2Ljc0LTIu"
|
|
|
|
|
|
"NzRMMjEgMTYiLz48cGF0aCBkPSJNMTYgMTZoNXY1Ii8+PGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iMSIv"
|
|
|
|
|
|
"Pjwvc3ZnPg=="
|
|
|
|
|
|
)
|
2026-09-11 21:33:59 +02:00
|
|
|
|
SWAP_LR_ICON_DATA_URL = (
|
|
|
|
|
|
"data:image/svg+xml;base64,"
|
|
|
|
|
|
"PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIg"
|
|
|
|
|
|
"dmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdp"
|
|
|
|
|
|
"ZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9"
|
|
|
|
|
|
"Ik04IDMgNCA3bDQgNCIvPjxwYXRoIGQ9Ik00IDdoMTYiLz48cGF0aCBkPSJtMTYgMjEgNC00LTQtNCIvPjxw"
|
|
|
|
|
|
"YXRoIGQ9Ik0yMCAxN0g0Ii8+PC9zdmc+"
|
|
|
|
|
|
)
|
2026-02-24 12:23:36 +00:00
|
|
|
|
RESET_ROTATION_ICON_DATA_URL = (
|
|
|
|
|
|
"data:image/svg+xml;base64,"
|
|
|
|
|
|
"PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIg"
|
|
|
|
|
|
"dmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdp"
|
|
|
|
|
|
"ZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdW"
|
|
|
|
|
|
"NpZGUgbHVjaWRlLXJvdGF0ZS1jdy1zcXVhcmUtaWNvbiBsdWNpZGUtcm90YXRlLWN3LXNxdWFyZSI+PHBhdGgg"
|
|
|
|
|
|
"ZD0iTTEyIDVINmEyIDIgMCAwIDAtMiAydjMiLz48cGF0aCBkPSJtOSA4IDMtMy0zLTMiLz48cGF0aCBkPSJNNC"
|
|
|
|
|
|
"AxNHY0YTIgMiAwIDAgMCAyIDJoMTJhMiAyIDAgMCAwIDItMlY3YTIgMiAwIDAgMC0yLTJoLTIiLz48L3N2Zz4="
|
|
|
|
|
|
)
|
|
|
|
|
|
REMOVE_ICON_DATA_URL = (
|
|
|
|
|
|
"data:image/svg+xml;base64,"
|
|
|
|
|
|
"PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIg"
|
|
|
|
|
|
"dmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdp"
|
|
|
|
|
|
"ZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdW"
|
|
|
|
|
|
"NpZGUgbHVjaWRlLXNxdWFyZS14LWljb24gbHVjaWRlLXNxdWFyZS14Ij48cmVjdCB3aWR0aD0iMTgiIGhlaWdo"
|
|
|
|
|
|
"dD0iMTgiIHg9IjMiIHk9IjMiIHJ4PSIyIiByeT0iMiIvPjxwYXRoIGQ9Im0xNSA5LTYgNiIvPjxwYXRoIGQ9"
|
|
|
|
|
|
"Im05IDkgNiA2Ii8+PC9zdmc+"
|
|
|
|
|
|
)
|
|
|
|
|
|
LOCK_ALL_ICON_DATA_URL = (
|
|
|
|
|
|
"data:image/svg+xml;base64,"
|
|
|
|
|
|
"PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIg"
|
|
|
|
|
|
"dmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdp"
|
|
|
|
|
|
"ZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdW"
|
|
|
|
|
|
"NpZGUgbHVjaWRlLWxvY2sta2V5aG9sZS1pY29uIGx1Y2lkZS1sb2NrLWtleWhvbGUiPjxjaXJjbGUgY3g9IjEy"
|
|
|
|
|
|
"IiBjeT0iMTYiIHI9IjEiLz48cmVjdCB4PSIzIiB5PSIxMCIgd2lkdGg9IjE4IiBoZWlnaHQ9IjEyIiByeD0i"
|
|
|
|
|
|
"MiIvPjxwYXRoIGQ9Ik03IDEwVjdhNSA1IDAgMCAxIDEwIDB2MyIvPjwvc3ZnPg=="
|
|
|
|
|
|
)
|
|
|
|
|
|
DETECT_ICON_DATA_URL = (
|
|
|
|
|
|
"data:image/svg+xml;base64,"
|
|
|
|
|
|
"PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIg"
|
|
|
|
|
|
"dmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdp"
|
|
|
|
|
|
"ZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdW"
|
|
|
|
|
|
"NpZGUgbHVjaWRlLWV5ZS1pY29uIGx1Y2lkZS1leWUiPjxwYXRoIGQ9Ik0yLjA2MiAxMi4zNDhhMSAxIDAgMCAx"
|
|
|
|
|
|
"IDAtLjY5NiAxMC43NSAxMC43NSAwIDAgMSAxOS44NzYgMCAxIDEgMCAwIDEgMCAuNjk2IDEwLjc1IDEwLjc1ID"
|
|
|
|
|
|
"AgMCAxLTE5Ljg3NiAwIi8+PGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iMyIvPjwvc3ZnPg=="
|
|
|
|
|
|
)
|
|
|
|
|
|
PREVIEW_FILTER_ICON_DATA_URL = (
|
|
|
|
|
|
"data:image/svg+xml;base64,"
|
|
|
|
|
|
"PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIg"
|
|
|
|
|
|
"dmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdp"
|
|
|
|
|
|
"ZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdW"
|
|
|
|
|
|
"NpZGUgbHVjaWRlLWV5ZS1vZmYtaWNvbiBsdWNpZGUtZXllLW9mZiI+PHBhdGggZD0iTTEwLjczMyA1LjA3NmEx"
|
|
|
|
|
|
"MC43NDQgMTAuNzQ0IDAgMCAxIDExLjIwNSA2LjU3NSAxIDEgMCAwIDEgMCAuNjk2IDEwLjc0NyAxMC43NDcgMC"
|
|
|
|
|
|
"AwIDEtMS40NDQgMi40OSIvPjxwYXRoIGQ9Ik0xNC4wODQgMTQuMTU4YTMgMyAwIDAgMS00LjI0Mi00LjI0MiIv"
|
|
|
|
|
|
"PjxwYXRoIGQ9Ik0xNy40NzkgMTcuNDk5YTEwLjc1IDEwLjc1IDAgMCAxLTE1LjQxNy01LjE1MSAxIDEgMCAwID"
|
|
|
|
|
|
"EgMC0uNjk2IDEwLjc1IDEwLjc1IDAgMCAxIDQuNDQ2LTUuMTQzIi8+PHBhdGggZD0ibTIgMiAyMCAyMCIvPjwv"
|
|
|
|
|
|
"c3ZnPg=="
|
|
|
|
|
|
)
|
|
|
|
|
|
ADD_BOX_ICON_DATA_URL = (
|
|
|
|
|
|
"data:image/svg+xml;base64,"
|
|
|
|
|
|
"PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIg"
|
|
|
|
|
|
"dmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdp"
|
|
|
|
|
|
"ZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdW"
|
|
|
|
|
|
"NpZGUgbHVjaWRlLXNxdWFyZS1wbHVzLWljb24gbHVjaWRlLXNxdWFyZS1wbHVzIj48cmVjdCB3aWR0aD0iMTgi"
|
|
|
|
|
|
"IGhlaWdodD0iMTgiIHg9IjMiIHk9IjMiIHJ4PSIyIi8+PHBhdGggZD0iTTggMTJoOCIvPjxwYXRoIGQ9Ik0x"
|
|
|
|
|
|
"MiA4djgiLz48L3N2Zz4="
|
|
|
|
|
|
)
|
|
|
|
|
|
RESET_VIEW_ICON_DATA_URL = (
|
|
|
|
|
|
"data:image/svg+xml;base64,"
|
|
|
|
|
|
"PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIg"
|
|
|
|
|
|
"dmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdp"
|
|
|
|
|
|
"ZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdW"
|
|
|
|
|
|
"NpZGUgbHVjaWRlLXZpZXctaWNvbiBsdWNpZGUtdmlldyI+PHBhdGggZD0iTTIxIDE3djJhMiAyIDAgMCAxLTIg"
|
|
|
|
|
|
"Mkg1YTIgMiAwIDAgMS0yLTJ2LTIiLz48cGF0aCBkPSJNMjEgN1Y1YTIgMiAwIDAgMC0yLTJINWEyIDIgMCAwID"
|
|
|
|
|
|
"AtMiAydjIiLz48Y2lyY2xlIGN4PSIxMiIgY3k9IjEyIiByPSIxIi8+PHBhdGggZD0iTTE4Ljk0NCAxMi4zM2Ex"
|
|
|
|
|
|
"IDEgMCAwIDAgMC0uNjYgNy41IDcuNSAwIDAgMC0xMy44ODggMCAxIDEgMCAwIDAgMCAuNjYgNy41IDcuNSAwID"
|
|
|
|
|
|
"AgMCAxMy44ODggMCIvPjwvc3ZnPg=="
|
|
|
|
|
|
)
|
|
|
|
|
|
PREV_IMAGE_ICON_DATA_URL = (
|
|
|
|
|
|
"data:image/svg+xml;base64,"
|
|
|
|
|
|
"PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIg"
|
|
|
|
|
|
"dmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdp"
|
|
|
|
|
|
"ZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdW"
|
|
|
|
|
|
"NpZGUgbHVjaWRlLWNpcmNsZS1hcnJvdy1sZWZ0LWljb24gbHVjaWRlLWNpcmNsZS1hcnJvdy1sZWZ0Ij48Y2ly"
|
|
|
|
|
|
"Y2xlIGN4PSIxMiIgY3k9IjEyIiByPSIxMCIvPjxwYXRoIGQ9Im0xMiA4LTQgNCA0IDQiLz48cGF0aCBkPSJN"
|
|
|
|
|
|
"MTYgMTJIOCIvPjwvc3ZnPg=="
|
|
|
|
|
|
)
|
|
|
|
|
|
NEXT_IMAGE_ICON_DATA_URL = (
|
|
|
|
|
|
"data:image/svg+xml;base64,"
|
|
|
|
|
|
"PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIg"
|
|
|
|
|
|
"dmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdp"
|
|
|
|
|
|
"ZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdW"
|
|
|
|
|
|
"NpZGUgbHVjaWRlLWNpcmNsZS1hcnJvdy1yaWdodC1pY29uIGx1Y2lkZS1jaXJjbGUtYXJyb3ctcmlnaHQiPjxj"
|
|
|
|
|
|
"aXJjbGUgY3g9IjEyIiBjeT0iMTIiIHI9IjEwIi8+PHBhdGggZD0ibTEyIDE2IDQtNC00LTQiLz48cGF0aCBk"
|
|
|
|
|
|
"PSJNOCAxMmg4Ii8+PC9zdmc+"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, startup_dir=None):
|
|
|
|
|
|
super().__init__()
|
|
|
|
|
|
|
|
|
|
|
|
self.setWindowTitle("ScanSlicer")
|
|
|
|
|
|
app_icon = resolve_app_icon()
|
|
|
|
|
|
if not app_icon.isNull():
|
|
|
|
|
|
self.setWindowIcon(app_icon)
|
|
|
|
|
|
self.setStyleSheet(
|
|
|
|
|
|
"QPushButton { padding: 5px; }"
|
|
|
|
|
|
"QSpinBox { padding: 3px; }"
|
|
|
|
|
|
)
|
|
|
|
|
|
self.source_images = []
|
|
|
|
|
|
self.source_index = None
|
|
|
|
|
|
self.input_dir = None
|
|
|
|
|
|
self.output_dir = None
|
|
|
|
|
|
self.save_format = "png"
|
|
|
|
|
|
self.jpg_quality = 95
|
|
|
|
|
|
self.png_compression = 3
|
|
|
|
|
|
self.sort_mode = "name"
|
|
|
|
|
|
self.preview_filter_enabled = False
|
|
|
|
|
|
self.last_detected_boxes = None
|
|
|
|
|
|
self.last_preview_normal_image = None
|
|
|
|
|
|
self.last_preview_filter_image = None
|
|
|
|
|
|
|
|
|
|
|
|
self.params = AutoslicerParams()
|
|
|
|
|
|
self.autoslicer = Autoslicer(self.params)
|
|
|
|
|
|
self.autotune_profile = self.autoslicer.default_autotune_profile_name()
|
|
|
|
|
|
self.param_controls = {}
|
|
|
|
|
|
self.param_blocks = {}
|
|
|
|
|
|
self.show_advanced_params = False
|
|
|
|
|
|
self.updating_param_controls = False
|
|
|
|
|
|
self.processing_busy = False
|
|
|
|
|
|
self.controls_scroll = None
|
|
|
|
|
|
self.preview_toolbar = None
|
|
|
|
|
|
self.status_log_path = None
|
|
|
|
|
|
self.status_logging_enabled = True
|
|
|
|
|
|
self._status_last_event_pump_ts = 0.0
|
|
|
|
|
|
self._is_closing = False
|
|
|
|
|
|
self._pending_detection_new_image = False
|
|
|
|
|
|
self._worker_pool = None
|
|
|
|
|
|
self._detection_task = None
|
|
|
|
|
|
self._autotune_task = None
|
|
|
|
|
|
self._detection_running = False
|
|
|
|
|
|
self._autotune_running = False
|
|
|
|
|
|
self._detection_task_id = 0
|
|
|
|
|
|
self._autotune_task_id = 0
|
|
|
|
|
|
self._active_detection_task_id = None
|
|
|
|
|
|
self._active_autotune_task_id = None
|
|
|
|
|
|
|
|
|
|
|
|
root = QWidget(self)
|
|
|
|
|
|
self.setCentralWidget(root)
|
|
|
|
|
|
layout = QGridLayout(root)
|
|
|
|
|
|
|
|
|
|
|
|
header = self._build_header_controls(root)
|
|
|
|
|
|
layout.addWidget(header, 0, 0, 1, 2)
|
|
|
|
|
|
|
|
|
|
|
|
self.controls_scroll = self._build_controls_panel(root)
|
|
|
|
|
|
preview_panel = self._build_preview_panel(root)
|
|
|
|
|
|
|
|
|
|
|
|
layout.addWidget(self.controls_scroll, 1, 0)
|
|
|
|
|
|
layout.addWidget(preview_panel, 1, 1)
|
|
|
|
|
|
layout.setColumnStretch(1, 1)
|
|
|
|
|
|
layout.setRowStretch(1, 1)
|
|
|
|
|
|
|
|
|
|
|
|
self._build_status_bar()
|
|
|
|
|
|
|
|
|
|
|
|
self.initialize_status_log()
|
|
|
|
|
|
self.update_statusbar("Ready.")
|
|
|
|
|
|
self.update_quality_controls_state()
|
|
|
|
|
|
self.update_parameter_visibility()
|
|
|
|
|
|
self.update_output_dir_label()
|
|
|
|
|
|
self.refresh_active_box_toolbar()
|
|
|
|
|
|
|
|
|
|
|
|
if startup_dir:
|
|
|
|
|
|
self.open_directory(startup_dir)
|
|
|
|
|
|
|
|
|
|
|
|
def _build_header_controls(self, parent):
|
|
|
|
|
|
header = QWidget(parent)
|
|
|
|
|
|
header_layout = QHBoxLayout(header)
|
|
|
|
|
|
header_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
|
|
header_layout.setSpacing(6)
|
|
|
|
|
|
|
|
|
|
|
|
title_label = QLabel("ScanSlicer")
|
|
|
|
|
|
title_font = QFont()
|
|
|
|
|
|
title_font.setPointSize(16)
|
|
|
|
|
|
title_font.setBold(True)
|
|
|
|
|
|
title_label.setFont(title_font)
|
|
|
|
|
|
title_label.setAlignment(Qt.AlignCenter)
|
|
|
|
|
|
|
|
|
|
|
|
header_layout.addStretch(1)
|
|
|
|
|
|
header_layout.addWidget(title_label)
|
|
|
|
|
|
header_layout.addStretch(1)
|
|
|
|
|
|
|
|
|
|
|
|
self.button_minimize = QPushButton("−", header)
|
|
|
|
|
|
self.button_minimize.setToolTip("Minimize")
|
|
|
|
|
|
self.button_minimize.clicked.connect(self.showMinimized)
|
|
|
|
|
|
header_layout.addWidget(self.button_minimize)
|
|
|
|
|
|
|
|
|
|
|
|
self.button_maximize = QPushButton("□", header)
|
|
|
|
|
|
self.button_maximize.setToolTip("Maximize/Restore")
|
|
|
|
|
|
self.button_maximize.clicked.connect(self.toggle_maximize_restore)
|
|
|
|
|
|
header_layout.addWidget(self.button_maximize)
|
|
|
|
|
|
|
|
|
|
|
|
self.button_quit = QPushButton("✕", header)
|
|
|
|
|
|
self.button_quit.setToolTip("Quit")
|
|
|
|
|
|
self.button_quit.clicked.connect(self.close)
|
|
|
|
|
|
header_layout.addWidget(self.button_quit)
|
|
|
|
|
|
return header
|
|
|
|
|
|
|
|
|
|
|
|
def toggle_maximize_restore(self):
|
|
|
|
|
|
if self.isMaximized():
|
|
|
|
|
|
self.showNormal()
|
|
|
|
|
|
return
|
|
|
|
|
|
self.showMaximized()
|
|
|
|
|
|
|
|
|
|
|
|
def closeEvent(self, event):
|
|
|
|
|
|
self._is_closing = True
|
|
|
|
|
|
if hasattr(self, "autoslicer"):
|
|
|
|
|
|
self.autoslicer.abort_operation()
|
|
|
|
|
|
if hasattr(self, "_close_status_log"):
|
|
|
|
|
|
self._close_status_log()
|
|
|
|
|
|
super().closeEvent(event)
|
2026-02-24 12:46:56 +00:00
|
|
|
|
|
|
|
|
|
|
def run_app(startup_dir=None):
|
|
|
|
|
|
app = QApplication.instance()
|
|
|
|
|
|
if app is None:
|
|
|
|
|
|
app = QApplication(sys.argv)
|
|
|
|
|
|
apply_dark_theme_if_requested(app)
|
|
|
|
|
|
apply_global_text_size_offset(app, 2)
|
|
|
|
|
|
app.setApplicationName("ScanSlicer")
|
|
|
|
|
|
app.setApplicationDisplayName("ScanSlicer")
|
|
|
|
|
|
app.setDesktopFileName("scanslicer")
|
|
|
|
|
|
app_icon = resolve_app_icon()
|
|
|
|
|
|
if not app_icon.isNull():
|
|
|
|
|
|
app.setWindowIcon(app_icon)
|
|
|
|
|
|
|
|
|
|
|
|
scan_slicer_window = QtScanSlicer(startup_dir)
|
|
|
|
|
|
scan_slicer_window.showMaximized()
|
|
|
|
|
|
return app.exec()
|