setiastrosuitepro 1.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of setiastrosuitepro might be problematic. Click here for more details.
- setiastro/__init__.py +2 -0
- setiastro/saspro/__init__.py +20 -0
- setiastro/saspro/__main__.py +784 -0
- setiastro/saspro/_generated/__init__.py +7 -0
- setiastro/saspro/_generated/build_info.py +2 -0
- setiastro/saspro/abe.py +1295 -0
- setiastro/saspro/abe_preset.py +196 -0
- setiastro/saspro/aberration_ai.py +694 -0
- setiastro/saspro/aberration_ai_preset.py +224 -0
- setiastro/saspro/accel_installer.py +218 -0
- setiastro/saspro/accel_workers.py +30 -0
- setiastro/saspro/add_stars.py +621 -0
- setiastro/saspro/astrobin_exporter.py +1007 -0
- setiastro/saspro/astrospike.py +153 -0
- setiastro/saspro/astrospike_python.py +1839 -0
- setiastro/saspro/autostretch.py +196 -0
- setiastro/saspro/backgroundneutral.py +560 -0
- setiastro/saspro/batch_convert.py +325 -0
- setiastro/saspro/batch_renamer.py +519 -0
- setiastro/saspro/blemish_blaster.py +488 -0
- setiastro/saspro/blink_comparator_pro.py +2923 -0
- setiastro/saspro/bundles.py +61 -0
- setiastro/saspro/bundles_dock.py +114 -0
- setiastro/saspro/cheat_sheet.py +168 -0
- setiastro/saspro/clahe.py +342 -0
- setiastro/saspro/comet_stacking.py +1377 -0
- setiastro/saspro/config.py +38 -0
- setiastro/saspro/config_bootstrap.py +40 -0
- setiastro/saspro/config_manager.py +316 -0
- setiastro/saspro/continuum_subtract.py +1617 -0
- setiastro/saspro/convo.py +1397 -0
- setiastro/saspro/convo_preset.py +414 -0
- setiastro/saspro/copyastro.py +187 -0
- setiastro/saspro/cosmicclarity.py +1564 -0
- setiastro/saspro/cosmicclarity_preset.py +407 -0
- setiastro/saspro/crop_dialog_pro.py +948 -0
- setiastro/saspro/crop_preset.py +189 -0
- setiastro/saspro/curve_editor_pro.py +2544 -0
- setiastro/saspro/curves_preset.py +375 -0
- setiastro/saspro/debayer.py +670 -0
- setiastro/saspro/debug_utils.py +29 -0
- setiastro/saspro/dnd_mime.py +35 -0
- setiastro/saspro/doc_manager.py +2634 -0
- setiastro/saspro/exoplanet_detector.py +2166 -0
- setiastro/saspro/file_utils.py +284 -0
- setiastro/saspro/fitsmodifier.py +744 -0
- setiastro/saspro/free_torch_memory.py +48 -0
- setiastro/saspro/frequency_separation.py +1343 -0
- setiastro/saspro/function_bundle.py +1594 -0
- setiastro/saspro/ghs_dialog_pro.py +660 -0
- setiastro/saspro/ghs_preset.py +284 -0
- setiastro/saspro/graxpert.py +634 -0
- setiastro/saspro/graxpert_preset.py +287 -0
- setiastro/saspro/gui/__init__.py +0 -0
- setiastro/saspro/gui/main_window.py +8494 -0
- setiastro/saspro/gui/mixins/__init__.py +33 -0
- setiastro/saspro/gui/mixins/dock_mixin.py +263 -0
- setiastro/saspro/gui/mixins/file_mixin.py +445 -0
- setiastro/saspro/gui/mixins/geometry_mixin.py +403 -0
- setiastro/saspro/gui/mixins/header_mixin.py +441 -0
- setiastro/saspro/gui/mixins/mask_mixin.py +421 -0
- setiastro/saspro/gui/mixins/menu_mixin.py +361 -0
- setiastro/saspro/gui/mixins/theme_mixin.py +367 -0
- setiastro/saspro/gui/mixins/toolbar_mixin.py +1324 -0
- setiastro/saspro/gui/mixins/update_mixin.py +309 -0
- setiastro/saspro/gui/mixins/view_mixin.py +435 -0
- setiastro/saspro/halobgon.py +462 -0
- setiastro/saspro/header_viewer.py +445 -0
- setiastro/saspro/headless_utils.py +88 -0
- setiastro/saspro/histogram.py +753 -0
- setiastro/saspro/history_explorer.py +939 -0
- setiastro/saspro/image_combine.py +414 -0
- setiastro/saspro/image_peeker_pro.py +1596 -0
- setiastro/saspro/imageops/__init__.py +37 -0
- setiastro/saspro/imageops/mdi_snap.py +292 -0
- setiastro/saspro/imageops/scnr.py +36 -0
- setiastro/saspro/imageops/starbasedwhitebalance.py +210 -0
- setiastro/saspro/imageops/stretch.py +244 -0
- setiastro/saspro/isophote.py +1179 -0
- setiastro/saspro/layers.py +208 -0
- setiastro/saspro/layers_dock.py +714 -0
- setiastro/saspro/lazy_imports.py +193 -0
- setiastro/saspro/legacy/__init__.py +2 -0
- setiastro/saspro/legacy/image_manager.py +2226 -0
- setiastro/saspro/legacy/numba_utils.py +3659 -0
- setiastro/saspro/legacy/xisf.py +1071 -0
- setiastro/saspro/linear_fit.py +534 -0
- setiastro/saspro/live_stacking.py +1830 -0
- setiastro/saspro/log_bus.py +5 -0
- setiastro/saspro/logging_config.py +460 -0
- setiastro/saspro/luminancerecombine.py +309 -0
- setiastro/saspro/main_helpers.py +201 -0
- setiastro/saspro/mask_creation.py +928 -0
- setiastro/saspro/masks_core.py +56 -0
- setiastro/saspro/mdi_widgets.py +353 -0
- setiastro/saspro/memory_utils.py +666 -0
- setiastro/saspro/metadata_patcher.py +75 -0
- setiastro/saspro/mfdeconv.py +3826 -0
- setiastro/saspro/mfdeconv_earlystop.py +71 -0
- setiastro/saspro/mfdeconvcudnn.py +3263 -0
- setiastro/saspro/mfdeconvsport.py +2382 -0
- setiastro/saspro/minorbodycatalog.py +567 -0
- setiastro/saspro/morphology.py +382 -0
- setiastro/saspro/multiscale_decomp.py +1290 -0
- setiastro/saspro/nbtorgb_stars.py +531 -0
- setiastro/saspro/numba_utils.py +3044 -0
- setiastro/saspro/numba_warmup.py +141 -0
- setiastro/saspro/ops/__init__.py +9 -0
- setiastro/saspro/ops/command_help_dialog.py +623 -0
- setiastro/saspro/ops/command_runner.py +217 -0
- setiastro/saspro/ops/commands.py +1594 -0
- setiastro/saspro/ops/script_editor.py +1102 -0
- setiastro/saspro/ops/scripts.py +1413 -0
- setiastro/saspro/ops/settings.py +560 -0
- setiastro/saspro/parallel_utils.py +554 -0
- setiastro/saspro/pedestal.py +121 -0
- setiastro/saspro/perfect_palette_picker.py +1053 -0
- setiastro/saspro/pipeline.py +110 -0
- setiastro/saspro/pixelmath.py +1600 -0
- setiastro/saspro/plate_solver.py +2435 -0
- setiastro/saspro/project_io.py +797 -0
- setiastro/saspro/psf_utils.py +136 -0
- setiastro/saspro/psf_viewer.py +549 -0
- setiastro/saspro/pyi_rthook_astroquery.py +95 -0
- setiastro/saspro/remove_green.py +314 -0
- setiastro/saspro/remove_stars.py +1625 -0
- setiastro/saspro/remove_stars_preset.py +404 -0
- setiastro/saspro/resources.py +472 -0
- setiastro/saspro/rgb_combination.py +207 -0
- setiastro/saspro/rgb_extract.py +19 -0
- setiastro/saspro/rgbalign.py +723 -0
- setiastro/saspro/runtime_imports.py +7 -0
- setiastro/saspro/runtime_torch.py +754 -0
- setiastro/saspro/save_options.py +72 -0
- setiastro/saspro/selective_color.py +1552 -0
- setiastro/saspro/sfcc.py +1425 -0
- setiastro/saspro/shortcuts.py +2807 -0
- setiastro/saspro/signature_insert.py +1099 -0
- setiastro/saspro/stacking_suite.py +17712 -0
- setiastro/saspro/star_alignment.py +7420 -0
- setiastro/saspro/star_alignment_preset.py +329 -0
- setiastro/saspro/star_metrics.py +49 -0
- setiastro/saspro/star_spikes.py +681 -0
- setiastro/saspro/star_stretch.py +470 -0
- setiastro/saspro/stat_stretch.py +502 -0
- setiastro/saspro/status_log_dock.py +78 -0
- setiastro/saspro/subwindow.py +3267 -0
- setiastro/saspro/supernovaasteroidhunter.py +1712 -0
- setiastro/saspro/swap_manager.py +99 -0
- setiastro/saspro/torch_backend.py +89 -0
- setiastro/saspro/torch_rejection.py +434 -0
- setiastro/saspro/view_bundle.py +1555 -0
- setiastro/saspro/wavescale_hdr.py +624 -0
- setiastro/saspro/wavescale_hdr_preset.py +100 -0
- setiastro/saspro/wavescalede.py +657 -0
- setiastro/saspro/wavescalede_preset.py +228 -0
- setiastro/saspro/wcs_update.py +374 -0
- setiastro/saspro/whitebalance.py +456 -0
- setiastro/saspro/widgets/__init__.py +48 -0
- setiastro/saspro/widgets/common_utilities.py +305 -0
- setiastro/saspro/widgets/graphics_views.py +122 -0
- setiastro/saspro/widgets/image_utils.py +518 -0
- setiastro/saspro/widgets/preview_dialogs.py +280 -0
- setiastro/saspro/widgets/spinboxes.py +275 -0
- setiastro/saspro/widgets/themed_buttons.py +13 -0
- setiastro/saspro/widgets/wavelet_utils.py +299 -0
- setiastro/saspro/window_shelf.py +185 -0
- setiastro/saspro/xisf.py +1123 -0
- setiastrosuitepro-1.6.0.dist-info/METADATA +266 -0
- setiastrosuitepro-1.6.0.dist-info/RECORD +174 -0
- setiastrosuitepro-1.6.0.dist-info/WHEEL +4 -0
- setiastrosuitepro-1.6.0.dist-info/entry_points.txt +6 -0
- setiastrosuitepro-1.6.0.dist-info/licenses/LICENSE +674 -0
- setiastrosuitepro-1.6.0.dist-info/licenses/license.txt +2580 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import os
|
|
3
|
+
import glob
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from PyQt6.QtCore import Qt, QThread, pyqtSignal
|
|
7
|
+
from PyQt6.QtWidgets import (
|
|
8
|
+
QDialog, QVBoxLayout, QLabel, QLineEdit, QPushButton, QComboBox, QCheckBox,
|
|
9
|
+
QFileDialog, QHBoxLayout, QProgressBar, QMessageBox
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
from setiastro.saspro.legacy.image_manager import load_image as legacy_load_image, save_image as legacy_save_image
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# --- helpers ---------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
_ALL_INPUT_PATTERNS = [
|
|
18
|
+
# science formats
|
|
19
|
+
"*.fit", "*.fits", "*.fts", "*.fz", "*.xisf",
|
|
20
|
+
# tiff/png/jpeg
|
|
21
|
+
"*.tif", "*.tiff", "*.png", "*.jpg", "*.jpeg",
|
|
22
|
+
# common RAWs
|
|
23
|
+
"*.cr2", "*.nef", "*.arw", "*.dng", "*.orf", "*.rw2", "*.pef",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
# Allowed bit-depths per output format (labels match your legacy.save_image)
|
|
27
|
+
_ALLOWED_DEPTHS = {
|
|
28
|
+
"png": {"8-bit"},
|
|
29
|
+
"jpg": {"8-bit"},
|
|
30
|
+
"jpeg": {"8-bit"},
|
|
31
|
+
"fits": {"32-bit floating point"},
|
|
32
|
+
"fit": {"32-bit floating point"},
|
|
33
|
+
"tiff": {"8-bit", "16-bit", "32-bit unsigned", "32-bit floating point"},
|
|
34
|
+
"tif": {"8-bit", "16-bit", "32-bit unsigned", "32-bit floating point"},
|
|
35
|
+
"xisf": {"16-bit", "32-bit unsigned", "32-bit floating point"},
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
from setiastro.saspro.file_utils import _normalize_ext
|
|
39
|
+
|
|
40
|
+
def _format_token_for_save(ext: str) -> str:
|
|
41
|
+
"""
|
|
42
|
+
Map UI extension to save_image's `original_format` token.
|
|
43
|
+
"""
|
|
44
|
+
e = _normalize_ext(ext)
|
|
45
|
+
if e == "tif": return "tiff"
|
|
46
|
+
if e == "jpg": return "jpg"
|
|
47
|
+
if e == "png": return "png"
|
|
48
|
+
if e in ("fit", "fits"): return e
|
|
49
|
+
if e == "xisf": return "xisf"
|
|
50
|
+
# default to fits if somehow unknown
|
|
51
|
+
return "fits"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# --- worker ----------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
class _BatchWorker(QThread):
|
|
57
|
+
progress = pyqtSignal(int, str) # (percent, message)
|
|
58
|
+
finished = pyqtSignal()
|
|
59
|
+
failed = pyqtSignal(str)
|
|
60
|
+
|
|
61
|
+
def __init__(self, in_dir: str, out_dir: str, out_ext: str,
|
|
62
|
+
recurse: bool, skip_existing: bool, bit_depth_choice: str):
|
|
63
|
+
super().__init__()
|
|
64
|
+
self.in_dir = in_dir
|
|
65
|
+
self.out_dir = out_dir
|
|
66
|
+
self.out_ext = out_ext # like ".fits"
|
|
67
|
+
self.recurse = recurse
|
|
68
|
+
self.skip_existing = skip_existing
|
|
69
|
+
self.bit_depth_choice = bit_depth_choice # "Auto" or one of _ALLOWED_DEPTHS per format
|
|
70
|
+
self._cancel = False
|
|
71
|
+
|
|
72
|
+
def cancel(self):
|
|
73
|
+
self._cancel = True
|
|
74
|
+
|
|
75
|
+
def _collect_files(self) -> list[str]:
|
|
76
|
+
files = []
|
|
77
|
+
pats = _ALL_INPUT_PATTERNS
|
|
78
|
+
if self.recurse:
|
|
79
|
+
for pat in pats:
|
|
80
|
+
files.extend(glob.glob(os.path.join(self.in_dir, "**", pat), recursive=True))
|
|
81
|
+
else:
|
|
82
|
+
for pat in pats:
|
|
83
|
+
files.extend(glob.glob(os.path.join(self.in_dir, pat)))
|
|
84
|
+
# unique + sorted
|
|
85
|
+
files = sorted(set(files))
|
|
86
|
+
return files
|
|
87
|
+
|
|
88
|
+
def run(self):
|
|
89
|
+
try:
|
|
90
|
+
files = self._collect_files()
|
|
91
|
+
n = len(files)
|
|
92
|
+
if not n:
|
|
93
|
+
self.failed.emit("No matching files in input directory.")
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
Path(self.out_dir).mkdir(parents=True, exist_ok=True)
|
|
97
|
+
out_token = _format_token_for_save(self.out_ext)
|
|
98
|
+
allowed = _ALLOWED_DEPTHS.get(_normalize_ext(self.out_ext), set())
|
|
99
|
+
|
|
100
|
+
for i, src in enumerate(files, start=1):
|
|
101
|
+
if self._cancel:
|
|
102
|
+
break
|
|
103
|
+
|
|
104
|
+
base = Path(src).stem
|
|
105
|
+
dst = os.path.join(self.out_dir, f"{base}{self.out_ext}")
|
|
106
|
+
|
|
107
|
+
if self.skip_existing and os.path.exists(dst):
|
|
108
|
+
self.progress.emit(int(i * 100 / n), f"Skipping (exists): {os.path.basename(dst)}")
|
|
109
|
+
continue
|
|
110
|
+
|
|
111
|
+
self.progress.emit(int((i - 1) * 100 / n), f"Loading {os.path.basename(src)}")
|
|
112
|
+
try:
|
|
113
|
+
img, header, src_bit_depth, is_mono = legacy_load_image(src)
|
|
114
|
+
except Exception as e:
|
|
115
|
+
self.progress.emit(int(i * 100 / n), f"Skipping (load failed): {os.path.basename(src)}")
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
if img is None:
|
|
119
|
+
self.progress.emit(int(i * 100 / n), f"Skipping (unreadable): {os.path.basename(src)}")
|
|
120
|
+
continue
|
|
121
|
+
|
|
122
|
+
# Decide bit depth to use
|
|
123
|
+
if self.bit_depth_choice == "Auto":
|
|
124
|
+
# Prefer source bit depth if valid for target, else pick a sane default from allowed
|
|
125
|
+
if src_bit_depth in allowed:
|
|
126
|
+
bit_depth = src_bit_depth
|
|
127
|
+
else:
|
|
128
|
+
# fallbacks by format
|
|
129
|
+
if out_token in ("png", "jpg"):
|
|
130
|
+
bit_depth = "8-bit"
|
|
131
|
+
elif out_token in ("tiff", "xisf"):
|
|
132
|
+
# prefer float if available, else first allowed
|
|
133
|
+
bit_depth = "32-bit floating point" if "32-bit floating point" in allowed else next(iter(allowed)) if allowed else None
|
|
134
|
+
else: # fits/fit
|
|
135
|
+
bit_depth = "32-bit floating point"
|
|
136
|
+
else:
|
|
137
|
+
bit_depth = self.bit_depth_choice
|
|
138
|
+
if allowed and bit_depth not in allowed:
|
|
139
|
+
# shouldn't happen because UI filters, but guard anyway
|
|
140
|
+
bit_depth = next(iter(allowed))
|
|
141
|
+
|
|
142
|
+
# Write
|
|
143
|
+
self.progress.emit(int((i - 1) * 100 / n), f"Saving {os.path.basename(dst)}")
|
|
144
|
+
try:
|
|
145
|
+
legacy_save_image(
|
|
146
|
+
img_array=img,
|
|
147
|
+
filename=dst,
|
|
148
|
+
original_format=out_token,
|
|
149
|
+
bit_depth=bit_depth,
|
|
150
|
+
original_header=header, # preserves FITS keywords when saving to FITS
|
|
151
|
+
is_mono=is_mono,
|
|
152
|
+
image_meta=None,
|
|
153
|
+
file_meta=None,
|
|
154
|
+
)
|
|
155
|
+
except Exception as e:
|
|
156
|
+
self.progress.emit(int(i * 100 / n), f"ERROR: {os.path.basename(base)} → {e}")
|
|
157
|
+
continue
|
|
158
|
+
|
|
159
|
+
self.progress.emit(int(i * 100 / n), f"Saved {os.path.basename(dst)}")
|
|
160
|
+
|
|
161
|
+
self.finished.emit()
|
|
162
|
+
except Exception as e:
|
|
163
|
+
self.failed.emit(str(e))
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# --- dialog ----------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
class BatchConvertDialog(QDialog):
|
|
169
|
+
def __init__(self, parent=None):
|
|
170
|
+
super().__init__(parent)
|
|
171
|
+
self.setWindowTitle("Batch Convert")
|
|
172
|
+
self.setMinimumWidth(560)
|
|
173
|
+
self.worker: _BatchWorker | None = None
|
|
174
|
+
|
|
175
|
+
lay = QVBoxLayout(self)
|
|
176
|
+
|
|
177
|
+
# in dir
|
|
178
|
+
self.in_edit = QLineEdit()
|
|
179
|
+
in_row = self._row("Input folder:", self.in_edit, self._browse_in)
|
|
180
|
+
lay.addLayout(in_row)
|
|
181
|
+
|
|
182
|
+
# out dir
|
|
183
|
+
self.out_edit = QLineEdit()
|
|
184
|
+
out_row = self._row("Output folder:", self.out_edit, self._browse_out)
|
|
185
|
+
lay.addLayout(out_row)
|
|
186
|
+
|
|
187
|
+
# options row
|
|
188
|
+
opt_row = QHBoxLayout()
|
|
189
|
+
|
|
190
|
+
self.recurse_cb = QCheckBox("Recurse subfolders")
|
|
191
|
+
self.recurse_cb.setChecked(True)
|
|
192
|
+
opt_row.addWidget(self.recurse_cb)
|
|
193
|
+
|
|
194
|
+
self.skip_cb = QCheckBox("Skip existing")
|
|
195
|
+
self.skip_cb.setChecked(True)
|
|
196
|
+
opt_row.addWidget(self.skip_cb)
|
|
197
|
+
|
|
198
|
+
opt_row.addStretch(1)
|
|
199
|
+
lay.addLayout(opt_row)
|
|
200
|
+
|
|
201
|
+
# output format + bit depth
|
|
202
|
+
fmt_row = QHBoxLayout()
|
|
203
|
+
self.fmt = QComboBox()
|
|
204
|
+
self.fmt.addItems([".fits", ".fit", ".tif", ".tiff", ".png", ".jpg", ".jpeg", ".xisf"])
|
|
205
|
+
self.fmt.currentIndexChanged.connect(self._refresh_depth_choices)
|
|
206
|
+
|
|
207
|
+
self.depth = QComboBox()
|
|
208
|
+
self.depth.addItem("Auto") # always first
|
|
209
|
+
# choices will be populated based on fmt
|
|
210
|
+
|
|
211
|
+
fmt_row.addWidget(QLabel("Output format:"))
|
|
212
|
+
fmt_row.addWidget(self.fmt)
|
|
213
|
+
fmt_row.addSpacing(16)
|
|
214
|
+
fmt_row.addWidget(QLabel("Bit depth:"))
|
|
215
|
+
fmt_row.addWidget(self.depth)
|
|
216
|
+
fmt_row.addStretch(1)
|
|
217
|
+
lay.addLayout(fmt_row)
|
|
218
|
+
|
|
219
|
+
# status/progress
|
|
220
|
+
self.status = QLabel("")
|
|
221
|
+
self.bar = QProgressBar()
|
|
222
|
+
self.bar.setRange(0, 100)
|
|
223
|
+
|
|
224
|
+
# buttons
|
|
225
|
+
self.start_btn = QPushButton("Start")
|
|
226
|
+
self.cancel_btn = QPushButton("Cancel")
|
|
227
|
+
self.cancel_btn.setEnabled(False)
|
|
228
|
+
btns = QHBoxLayout()
|
|
229
|
+
btns.addStretch(1)
|
|
230
|
+
btns.addWidget(self.start_btn)
|
|
231
|
+
btns.addWidget(self.cancel_btn)
|
|
232
|
+
|
|
233
|
+
lay.addSpacing(8)
|
|
234
|
+
lay.addWidget(self.status)
|
|
235
|
+
lay.addWidget(self.bar)
|
|
236
|
+
lay.addLayout(btns)
|
|
237
|
+
|
|
238
|
+
self.start_btn.clicked.connect(self._start)
|
|
239
|
+
self.cancel_btn.clicked.connect(self._cancel)
|
|
240
|
+
|
|
241
|
+
# initialize bit-depth choices
|
|
242
|
+
self._refresh_depth_choices()
|
|
243
|
+
|
|
244
|
+
def _row(self, label: str, line: QLineEdit, browse_fn):
|
|
245
|
+
hb = QHBoxLayout()
|
|
246
|
+
hb.addWidget(QLabel(label))
|
|
247
|
+
hb.addWidget(line, 1)
|
|
248
|
+
b = QPushButton("Browse…")
|
|
249
|
+
b.clicked.connect(browse_fn)
|
|
250
|
+
hb.addWidget(b)
|
|
251
|
+
return hb
|
|
252
|
+
|
|
253
|
+
def _browse_in(self):
|
|
254
|
+
d = QFileDialog.getExistingDirectory(self, "Choose Input Folder", self.in_edit.text().strip() or "")
|
|
255
|
+
if d:
|
|
256
|
+
self.in_edit.setText(d)
|
|
257
|
+
|
|
258
|
+
def _browse_out(self):
|
|
259
|
+
d = QFileDialog.getExistingDirectory(self, "Choose Output Folder", self.out_edit.text().strip() or "")
|
|
260
|
+
if d:
|
|
261
|
+
self.out_edit.setText(d)
|
|
262
|
+
|
|
263
|
+
def _refresh_depth_choices(self):
|
|
264
|
+
self.depth.blockSignals(True)
|
|
265
|
+
cur_fmt = self.fmt.currentText().lstrip(".").lower()
|
|
266
|
+
self.depth.clear()
|
|
267
|
+
self.depth.addItem("Auto")
|
|
268
|
+
# Map .tif/.tiff etc.
|
|
269
|
+
key = _normalize_ext(cur_fmt)
|
|
270
|
+
allowed = _ALLOWED_DEPTHS.get(key if key != "tif" else "tiff", set())
|
|
271
|
+
for d in sorted(allowed):
|
|
272
|
+
self.depth.addItem(d)
|
|
273
|
+
# pick a sensible default
|
|
274
|
+
self.depth.setCurrentIndex(0)
|
|
275
|
+
self.depth.blockSignals(False)
|
|
276
|
+
|
|
277
|
+
def _start(self):
|
|
278
|
+
in_dir = self.in_edit.text().strip()
|
|
279
|
+
out_dir = self.out_edit.text().strip()
|
|
280
|
+
if not in_dir or not out_dir:
|
|
281
|
+
QMessageBox.warning(self, "Batch Convert", "Pick input and output folders.")
|
|
282
|
+
return
|
|
283
|
+
|
|
284
|
+
if os.path.abspath(in_dir) == os.path.abspath(out_dir):
|
|
285
|
+
# you *can* convert in place, but warn if target ext might overwrite sources
|
|
286
|
+
QMessageBox.information(
|
|
287
|
+
self, "In-place Notice",
|
|
288
|
+
"Input and output folders are the same. Existing files with the same base name and extension may be overwritten."
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
out_ext = self.fmt.currentText()
|
|
292
|
+
recurse = self.recurse_cb.isChecked()
|
|
293
|
+
skip_existing = self.skip_cb.isChecked()
|
|
294
|
+
bit_depth_choice = self.depth.currentText() # "Auto" or explicit
|
|
295
|
+
|
|
296
|
+
self.worker = _BatchWorker(in_dir, out_dir, out_ext, recurse, skip_existing, bit_depth_choice)
|
|
297
|
+
self.worker.progress.connect(self._on_progress)
|
|
298
|
+
self.worker.finished.connect(self._on_finished)
|
|
299
|
+
self.worker.failed.connect(self._on_failed)
|
|
300
|
+
|
|
301
|
+
self.start_btn.setEnabled(False)
|
|
302
|
+
self.cancel_btn.setEnabled(True)
|
|
303
|
+
self.status.setText("Starting…")
|
|
304
|
+
self.bar.setValue(0)
|
|
305
|
+
self.worker.start()
|
|
306
|
+
|
|
307
|
+
def _cancel(self):
|
|
308
|
+
if self.worker:
|
|
309
|
+
self.worker.cancel()
|
|
310
|
+
self.cancel_btn.setEnabled(False)
|
|
311
|
+
|
|
312
|
+
def _on_progress(self, pct: int, msg: str):
|
|
313
|
+
self.bar.setValue(pct)
|
|
314
|
+
self.status.setText(msg)
|
|
315
|
+
|
|
316
|
+
def _on_finished(self):
|
|
317
|
+
self.start_btn.setEnabled(True)
|
|
318
|
+
self.cancel_btn.setEnabled(False)
|
|
319
|
+
self.status.setText("Done.")
|
|
320
|
+
|
|
321
|
+
def _on_failed(self, err: str):
|
|
322
|
+
self.start_btn.setEnabled(True)
|
|
323
|
+
self.cancel_btn.setEnabled(False)
|
|
324
|
+
self.status.setText("Failed.")
|
|
325
|
+
QMessageBox.critical(self, "Batch Convert", err)
|