Skip to content

Commit 57dd11e

Browse files
author
tpeulen
committedFeb 21, 2025·
Added PTU file splitter
1 parent 40c231e commit 57dd11e

File tree

5 files changed

+454
-338
lines changed

5 files changed

+454
-338
lines changed
 

‎chisurf/plugins/file_ptu_header_edit/__init__.py

-1
This file was deleted.

‎chisurf/plugins/file_ptu_header_edit/wizard.py

-337
This file was deleted.
+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
name = "PTU Splitter"
+305
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
from pathlib import Path
2+
from qtpy import QtWidgets, QtCore, QtGui
3+
4+
import tttrlib
5+
6+
import chisurf.gui.decorators
7+
import chisurf.settings
8+
9+
VERBOSE = False
10+
11+
def enable_file_drop_for_open(line_edit: QtWidgets.QLineEdit, open_func):
12+
"""
13+
Enable dropping a *single file* onto a QLineEdit.
14+
Calls `open_func(path_str)` after dropping the file.
15+
"""
16+
line_edit.setAcceptDrops(True)
17+
18+
def dragEnterEvent(event: QtGui.QDragEnterEvent):
19+
if event.mimeData().hasUrls():
20+
urls = event.mimeData().urls()
21+
# Accept only if exactly one URL and it is an existing file
22+
if len(urls) == 1:
23+
if Path(urls[0].toLocalFile()).is_file():
24+
event.acceptProposedAction()
25+
return
26+
event.ignore()
27+
28+
def dropEvent(event: QtGui.QDropEvent):
29+
if event.mimeData().hasUrls():
30+
path_str = event.mimeData().urls()[0].toLocalFile()
31+
if Path(path_str).is_file():
32+
event.acceptProposedAction()
33+
open_func(path_str) # Call the function to actually open the file
34+
return
35+
event.ignore()
36+
37+
# Monkey-patch the lineEdit's events:
38+
line_edit.dragEnterEvent = dragEnterEvent
39+
line_edit.dropEvent = dropEvent
40+
41+
42+
def enable_folder_drop(line_edit: QtWidgets.QLineEdit):
43+
"""
44+
Enable dropping a *single folder* onto a QLineEdit.
45+
Sets the lineEdit text to the dropped folder path.
46+
"""
47+
line_edit.setAcceptDrops(True)
48+
49+
def dragEnterEvent(event: QtGui.QDragEnterEvent):
50+
if event.mimeData().hasUrls():
51+
urls = event.mimeData().urls()
52+
# Accept only if exactly one URL and it is an existing directory
53+
if len(urls) == 1:
54+
if Path(urls[0].toLocalFile()).is_dir():
55+
event.acceptProposedAction()
56+
return
57+
event.ignore()
58+
59+
def dropEvent(event: QtGui.QDropEvent):
60+
if event.mimeData().hasUrls():
61+
folder_str = event.mimeData().urls()[0].toLocalFile()
62+
if Path(folder_str).is_dir():
63+
event.acceptProposedAction()
64+
line_edit.setText(folder_str)
65+
return
66+
event.ignore()
67+
68+
# Monkey-patch the lineEdit's events:
69+
line_edit.dragEnterEvent = dragEnterEvent
70+
line_edit.dropEvent = dropEvent
71+
72+
73+
74+
class PTUSplitter(QtWidgets.QWidget):
75+
76+
@chisurf.gui.decorators.init_with_ui("ptu_splitter/wizard.ui",
77+
path=chisurf.settings.plugin_path)
78+
def __init__(self, *args, **kwargs):
79+
# NO super() call here (the decorator handles it).
80+
self._tttr = None
81+
82+
# Set up drag & drop on lineedits
83+
enable_file_drop_for_open(self.lineEdit, self._open_input_file)
84+
enable_folder_drop(self.lineEdit_2)
85+
86+
# Fill combo box with supported types + "Auto"
87+
self.populate_supported_types()
88+
89+
# Connect your UI elements (change names if different in .ui)
90+
self.toolButton.clicked.connect(self.browse_and_open_input_file)
91+
self.toolButton_2.clicked.connect(self.browse_output_folder)
92+
self.pushButton.clicked.connect(self.split_file)
93+
94+
# Initialize progress bar to 0
95+
self.progressBar.setValue(0)
96+
97+
# --------------------------------------------------------------------------
98+
# Private helper to open a file (browse or drag & drop)
99+
# --------------------------------------------------------------------------
100+
def _open_input_file(self, file_path: str):
101+
"""
102+
Loads the specified file into the TTTR object.
103+
Also updates lineEdit (input path) and lineEdit_2 (default output folder).
104+
"""
105+
p = Path(file_path)
106+
if not p.is_file():
107+
QtWidgets.QMessageBox.warning(self, "Invalid File",
108+
f"'{file_path}' is not a valid file.")
109+
return
110+
111+
# Update the lineEdit to reflect the chosen file
112+
self.lineEdit.setText(str(p))
113+
114+
# Default output folder is file's parent
115+
self.lineEdit_2.setText(str(p.parent))
116+
117+
# Create the TTTR object
118+
if self.tttr_type is None:
119+
self._tttr = tttrlib.TTTR(str(p))
120+
else:
121+
self._tttr = tttrlib.TTTR(str(p), self.tttr_type)
122+
123+
if VERBOSE:
124+
QtWidgets.QMessageBox.information(
125+
self, "File Loaded",
126+
f"Successfully opened {p.name}."
127+
)
128+
129+
def populate_supported_types(self):
130+
"""Populates the comboBox with supported container types plus an 'Auto' option."""
131+
self.comboBox.clear()
132+
self.comboBox.insertItem(0, "Auto")
133+
self.comboBox.insertItems(1, list(tttrlib.TTTR.get_supported_container_names()))
134+
135+
def browse_and_open_input_file(self):
136+
"""File dialog for selecting a PTU file, then open it."""
137+
dialog = QtWidgets.QFileDialog(self, "Select PTU File")
138+
dialog.setFileMode(QtWidgets.QFileDialog.ExistingFile)
139+
# Optionally: dialog.setNameFilter("PTU Files (*.ptu)")
140+
141+
if dialog.exec_():
142+
selected_files = dialog.selectedFiles()
143+
if selected_files:
144+
self._open_input_file(selected_files[0])
145+
146+
147+
def browse_output_folder(self):
148+
"""Open a File Dialog to select an output folder."""
149+
dialog = QtWidgets.QFileDialog(self, "Select Output Folder")
150+
dialog.setFileMode(QtWidgets.QFileDialog.Directory)
151+
if dialog.exec_():
152+
selected_dirs = dialog.selectedFiles()
153+
if selected_dirs:
154+
self.lineEdit_2.setText(selected_dirs[0])
155+
156+
def split_file(self):
157+
"""
158+
Split the loaded TTTR file into multiple .ptu files.
159+
Each file will contain `photons_per_file` photons.
160+
Updates self.progressBar and disables user input during splitting.
161+
"""
162+
# Ensure we have TTTR data:
163+
if self._tttr is None:
164+
QtWidgets.QMessageBox.warning(
165+
self, "No Data Loaded",
166+
"Please choose and load a PTU file before splitting."
167+
)
168+
return
169+
170+
# Ensure valid output folder
171+
out_folder = self.output_folder
172+
if out_folder is None:
173+
QtWidgets.QMessageBox.warning(
174+
self, "Invalid Output Folder",
175+
"Please specify a valid output folder."
176+
)
177+
return
178+
179+
# Disable UI elements while splitting
180+
self._set_user_input_enabled(False)
181+
182+
t = self._tttr
183+
total_photons = len(t)
184+
chunk_size = self.photons_per_file
185+
186+
if total_photons == 0:
187+
QtWidgets.QMessageBox.warning(self, "Empty File", "No photons to split!")
188+
self._set_user_input_enabled(True)
189+
return
190+
191+
n_full_chunks = total_photons // chunk_size
192+
remainder = total_photons % chunk_size
193+
total_files = n_full_chunks + (1 if remainder else 0)
194+
195+
# Create sub-folder "filename_stem_chunkSize"
196+
input_file = self.tttr_input_filename
197+
output_subfolder = out_folder / f"{input_file.stem}_{chunk_size // 1000}k"
198+
output_subfolder.mkdir(parents=True, exist_ok=True)
199+
200+
# Retrieve header
201+
header = t.header
202+
203+
# Loop over each chunk (including remainder if present)
204+
for i in range(total_files):
205+
# progress 0..100
206+
progress = int((i / total_files) * 100)
207+
self.progressBar.setValue(progress)
208+
QtWidgets.QApplication.processEvents()
209+
210+
start = i * chunk_size
211+
stop = start + chunk_size
212+
if stop > total_photons:
213+
stop = total_photons # leftover chunk
214+
215+
c = t[start:stop]
216+
out_name = f"{input_file.stem}_{i:05d}.ptu"
217+
fn = output_subfolder / out_name
218+
c.write(fn.as_posix(), header)
219+
220+
# Finalize progress
221+
self.progressBar.setValue(100)
222+
223+
if VERBOSE:
224+
QtWidgets.QMessageBox.information(
225+
self,
226+
"Splitting Complete",
227+
f"Created {total_files} files in:\n{output_subfolder}"
228+
)
229+
230+
# Re-enable UI elements
231+
self._set_user_input_enabled(True)
232+
233+
def _set_user_input_enabled(self, enabled: bool):
234+
"""
235+
Enable/Disable the user input widgets to prevent interaction during splitting.
236+
"""
237+
pass
238+
# Adjust to your specific widget names:
239+
self.lineEdit.setEnabled(enabled)
240+
self.lineEdit_2.setEnabled(enabled)
241+
self.comboBox.setEnabled(enabled)
242+
self.spinBox.setEnabled(enabled)
243+
self.toolButton.setEnabled(enabled)
244+
self.toolButton_2.setEnabled(enabled)
245+
self.pushButton.setEnabled(enabled)
246+
247+
# --------------------------------------------------------------------------
248+
# Properties
249+
# --------------------------------------------------------------------------
250+
@property
251+
def photons_per_file(self) -> int:
252+
"""Returns the current value from the spinBox as the chunk size."""
253+
return int(self.spinBox.value()) * 1000
254+
255+
@property
256+
def tttr_type(self) -> str | None:
257+
"""
258+
Returns the type from the comboBox or None if 'Auto' is selected.
259+
"""
260+
tp = self.comboBox.currentText()
261+
return None if tp == "Auto" else tp
262+
263+
@property
264+
def tttr_input_filename(self) -> Path | None:
265+
"""
266+
Returns a Path object for the input file if it exists, otherwise None.
267+
"""
268+
fn = self.lineEdit.text().strip()
269+
path = Path(fn)
270+
return path if path.is_file() else None
271+
272+
@property
273+
def output_folder(self) -> Path | None:
274+
"""
275+
Returns a Path object for the desired output folder, or None if invalid.
276+
Creates the folder if needed.
277+
"""
278+
folder_str = self.lineEdit_2.text().strip()
279+
if not folder_str:
280+
return None
281+
p = Path(folder_str)
282+
# If the user typed a new folder path, we can decide to create it:
283+
if not p.exists():
284+
try:
285+
p.mkdir(parents=True, exist_ok=True)
286+
except Exception as e:
287+
print(f"Failed to create folder '{p}': {e}")
288+
return None
289+
return p
290+
291+
292+
293+
if __name__ == "plugin":
294+
brick_mic_wiz = PTUSplitter()
295+
brick_mic_wiz.show()
296+
297+
298+
if __name__ == '__main__':
299+
import sys
300+
app = QtWidgets.QApplication(sys.argv)
301+
app.aboutToQuit.connect(app.deleteLater)
302+
brick_mic_wiz = PTUSplitter()
303+
brick_mic_wiz.setWindowTitle('PTU-Splitter')
304+
brick_mic_wiz.show()
305+
sys.exit(app.exec_())
+148
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<ui version="4.0">
3+
<class>PTUSplitter</class>
4+
<widget class="QWidget" name="PTUSplitter">
5+
<property name="geometry">
6+
<rect>
7+
<x>0</x>
8+
<y>0</y>
9+
<width>374</width>
10+
<height>98</height>
11+
</rect>
12+
</property>
13+
<property name="windowTitle">
14+
<string>PTU-SPlitter</string>
15+
</property>
16+
<layout class="QGridLayout" name="gridLayout">
17+
<property name="leftMargin">
18+
<number>6</number>
19+
</property>
20+
<property name="topMargin">
21+
<number>6</number>
22+
</property>
23+
<property name="rightMargin">
24+
<number>6</number>
25+
</property>
26+
<property name="bottomMargin">
27+
<number>6</number>
28+
</property>
29+
<property name="spacing">
30+
<number>0</number>
31+
</property>
32+
<item row="3" column="0">
33+
<widget class="QLabel" name="label_5">
34+
<property name="text">
35+
<string>Progress</string>
36+
</property>
37+
</widget>
38+
</item>
39+
<item row="1" column="0">
40+
<widget class="QLabel" name="label_2">
41+
<property name="text">
42+
<string>Output folder</string>
43+
</property>
44+
</widget>
45+
</item>
46+
<item row="2" column="1" colspan="5">
47+
<layout class="QHBoxLayout" name="horizontalLayout">
48+
<property name="spacing">
49+
<number>0</number>
50+
</property>
51+
<item>
52+
<widget class="QSpinBox" name="spinBox">
53+
<property name="suffix">
54+
<string> k</string>
55+
</property>
56+
<property name="minimum">
57+
<number>100</number>
58+
</property>
59+
<property name="maximum">
60+
<number>999999999</number>
61+
</property>
62+
<property name="singleStep">
63+
<number>100</number>
64+
</property>
65+
<property name="value">
66+
<number>300</number>
67+
</property>
68+
</widget>
69+
</item>
70+
<item>
71+
<widget class="QPushButton" name="pushButton">
72+
<property name="sizePolicy">
73+
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
74+
<horstretch>0</horstretch>
75+
<verstretch>0</verstretch>
76+
</sizepolicy>
77+
</property>
78+
<property name="text">
79+
<string>Split</string>
80+
</property>
81+
</widget>
82+
</item>
83+
</layout>
84+
</item>
85+
<item row="2" column="0">
86+
<widget class="QLabel" name="label_3">
87+
<property name="text">
88+
<string>Photons per file</string>
89+
</property>
90+
</widget>
91+
</item>
92+
<item row="0" column="3">
93+
<widget class="QLabel" name="label_4">
94+
<property name="text">
95+
<string>Type</string>
96+
</property>
97+
</widget>
98+
</item>
99+
<item row="3" column="1" colspan="5">
100+
<widget class="QProgressBar" name="progressBar">
101+
<property name="value">
102+
<number>0</number>
103+
</property>
104+
</widget>
105+
</item>
106+
<item row="0" column="0">
107+
<widget class="QLabel" name="label">
108+
<property name="text">
109+
<string>File</string>
110+
</property>
111+
</widget>
112+
</item>
113+
<item row="0" column="5">
114+
<widget class="QToolButton" name="toolButton">
115+
<property name="text">
116+
<string>...</string>
117+
</property>
118+
</widget>
119+
</item>
120+
<item row="1" column="1" colspan="4">
121+
<widget class="QLineEdit" name="lineEdit_2">
122+
<property name="placeholderText">
123+
<string>Output folder name</string>
124+
</property>
125+
</widget>
126+
</item>
127+
<item row="0" column="1">
128+
<widget class="QLineEdit" name="lineEdit">
129+
<property name="placeholderText">
130+
<string>Drop file here</string>
131+
</property>
132+
</widget>
133+
</item>
134+
<item row="0" column="4">
135+
<widget class="QComboBox" name="comboBox"/>
136+
</item>
137+
<item row="1" column="5">
138+
<widget class="QToolButton" name="toolButton_2">
139+
<property name="text">
140+
<string>...</string>
141+
</property>
142+
</widget>
143+
</item>
144+
</layout>
145+
</widget>
146+
<resources/>
147+
<connections/>
148+
</ui>

0 commit comments

Comments
 (0)
Please sign in to comment.