Quickbend 1.0.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ Metadata-Version: 2.4
2
+ Name: Quickbend
3
+ Version: 1.0.0
4
+ Summary: an app to create glitchart using pyside6
5
+ Author-email: Daniel Crutti <dancrutti@gmail.com>
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
8
+ Classifier: Operating System :: OS Independent
9
+ Requires-Python: >=3.9
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: audioop-lts==0.2.2
12
+ Requires-Dist: numpy==2.4.6
13
+ Requires-Dist: pillow==12.2.0
14
+ Requires-Dist: PySide6==6.11.1
15
+ Requires-Dist: PySide6_Addons==6.11.1
16
+ Requires-Dist: PySide6_Essentials==6.11.1
17
+ Requires-Dist: shiboken6==6.11.1
18
+
19
+ # Quickbend
20
+
21
+ **Quickbend** is a tool for creating art via databending with images. It uses interpreting bmp files as logarithmic audio encoding schemes (a-law/u-law/adpcm) and then reconverting those back to bmp to create warped and glitched images.
@@ -0,0 +1,3 @@
1
+ # Quickbend
2
+
3
+ **Quickbend** is a tool for creating art via databending with images. It uses interpreting bmp files as logarithmic audio encoding schemes (a-law/u-law/adpcm) and then reconverting those back to bmp to create warped and glitched images.
@@ -0,0 +1,30 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "Quickbend"
7
+ version = "1.0.0"
8
+ description = "an app to create glitchart using pyside6"
9
+ readme = "README.md"
10
+ authors = [
11
+ { name = "Daniel Crutti", email = "dancrutti@gmail.com" }
12
+ ]
13
+ classifiers = [
14
+ "Programming Language :: Python :: 3",
15
+ "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
16
+ "Operating System :: OS Independent",
17
+ ]
18
+ requires-python = ">=3.9"
19
+ dependencies = [
20
+ "audioop-lts==0.2.2",
21
+ "numpy==2.4.6",
22
+ "pillow==12.2.0",
23
+ "PySide6==6.11.1",
24
+ "PySide6_Addons==6.11.1",
25
+ "PySide6_Essentials==6.11.1",
26
+ "shiboken6==6.11.1"
27
+ ]
28
+
29
+ [project.scripts]
30
+ Quickbend = "Quickbend.app:main"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,152 @@
1
+ import os
2
+ import shutil
3
+
4
+ from PySide6.QtCore import Qt
5
+ from PySide6.QtGui import QPixmap
6
+ from PySide6.QtWidgets import (
7
+ QApplication,
8
+ QComboBox,
9
+ QHBoxLayout,
10
+ QLabel,
11
+ QLineEdit,
12
+ QMainWindow,
13
+ QPushButton,
14
+ QVBoxLayout,
15
+ QWidget,
16
+ )
17
+
18
+ from .databend import databend
19
+ from .files import browse_file, save_file
20
+
21
+ cleanup_paths = []
22
+
23
+
24
+ # main window
25
+ class MainWindow(QMainWindow):
26
+ def __init__(self):
27
+ # main class
28
+ super().__init__()
29
+
30
+ self.setWindowTitle("My App")
31
+ layout = QVBoxLayout()
32
+
33
+ # button that opens file selection dialog
34
+ self.file_button = QPushButton("Get Files:")
35
+ self.file_button.clicked.connect(self.LoadNewImage)
36
+ layout.addWidget(self.file_button)
37
+
38
+ self.encoding_box = QComboBox()
39
+ self.encoding_box.addItems(["alaw", "ulaw", "adpcm"])
40
+ self.encoding_box_label = QLabel("Select Encoding:")
41
+ encoding_layout = QHBoxLayout()
42
+
43
+ encoding_layout.addWidget(self.encoding_box_label)
44
+ encoding_layout.addWidget(self.encoding_box, stretch=1)
45
+ layout.addLayout(encoding_layout)
46
+
47
+ self.xor_box = QComboBox()
48
+ self.xor_box.addItems(["Off", "On"])
49
+ self.xor_box_label = QLabel("Xor Mixing:")
50
+ xor_layout = QHBoxLayout()
51
+
52
+ xor_layout.addWidget(self.xor_box_label)
53
+ xor_layout.addWidget(self.xor_box, stretch=1)
54
+ layout.addLayout(xor_layout)
55
+
56
+ self.echo_time = QLineEdit()
57
+ self.echo_time.setPlaceholderText("Enter a Number (Decimals Accepted)")
58
+ self.echo_time.setInputMask("00.00;_")
59
+ self.echo_time_label = QLabel("Echo Time (Leave blank for no echo):")
60
+ echo_layout = QHBoxLayout()
61
+
62
+ echo_layout.addWidget(self.echo_time_label)
63
+ echo_layout.addWidget(self.echo_time, stretch=1)
64
+
65
+ layout.addLayout(echo_layout)
66
+
67
+ # image from file selection
68
+ self.image_preview = QLabel()
69
+ self.original_pixmap = QPixmap("placeholder.jpg")
70
+ layout.addWidget(self.image_preview)
71
+
72
+ save_button = QPushButton("export image")
73
+ save_button.clicked.connect(self.export_handler)
74
+ layout.addWidget(save_button)
75
+
76
+ # container to hold all widgets
77
+ container = QWidget()
78
+ container.setLayout(layout)
79
+
80
+ self.setCentralWidget(container)
81
+
82
+ # handles window resizes and keeps aspect ratio
83
+ def resizeEvent(self, event):
84
+ super().resizeEvent(event)
85
+ scaled_pixmap = self.original_pixmap.scaled(
86
+ self.image_preview.size(),
87
+ Qt.AspectRatioMode.KeepAspectRatio,
88
+ Qt.TransformationMode.SmoothTransformation,
89
+ )
90
+ self.image_preview.setPixmap(scaled_pixmap)
91
+ self.image_preview.setAlignment(Qt.AlignmentFlag.AlignCenter)
92
+
93
+ # loads image from file dialog into pixmap of QLabel
94
+ def LoadNewImage(self):
95
+ path = browse_file()
96
+ # gets paths for program cleanup later.
97
+ global cleanup_paths
98
+ cleanup_paths = path
99
+ if self.xor_box.currentText() == "On":
100
+ xor_flag = True
101
+ else:
102
+ xor_flag = False
103
+ if path:
104
+ try:
105
+ delay_time = float(self.echo_time.text())
106
+ except ValueError:
107
+ delay_time = 0
108
+ synced_files = databend(
109
+ path,
110
+ "/tmp/mash.bmp",
111
+ encoding=self.encoding_box.currentText(),
112
+ use_xor=xor_flag,
113
+ delay_time=delay_time,
114
+ )
115
+ if synced_files:
116
+ cleanup_paths.extend(synced_files)
117
+ self.original_pixmap = QPixmap("/tmp/mash.bmp")
118
+ scaled_pixmap = self.original_pixmap.scaled(
119
+ self.image_preview.size(),
120
+ Qt.AspectRatioMode.KeepAspectRatio,
121
+ Qt.TransformationMode.SmoothTransformation,
122
+ )
123
+ self.image_preview.setPixmap(scaled_pixmap)
124
+
125
+ def export_handler(self):
126
+ (save_file_path) = save_file()
127
+ if save_file_path:
128
+ shutil.move("/tmp/mash.bmp", save_file_path)
129
+
130
+
131
+ def cleanup():
132
+ if os.path.exists("/tmp/mash.bmp"):
133
+ os.remove("/tmp/mash.bmp")
134
+ for file in cleanup_paths:
135
+ if os.path.exists(file):
136
+ os.remove(file)
137
+
138
+
139
+ # open and show window
140
+ def main():
141
+ global app, window
142
+ app = QApplication()
143
+
144
+ app.aboutToQuit.connect(cleanup)
145
+
146
+ window = MainWindow()
147
+ window.show()
148
+
149
+ app.exec()
150
+
151
+ if __name__ == "__main__":
152
+ main()
@@ -0,0 +1,214 @@
1
+ import argparse
2
+ import audioop
3
+ import os
4
+ import struct
5
+
6
+ import numpy as np
7
+ from PIL import Image
8
+
9
+
10
+ def databend(input_files, output_file, encoding="alaw", use_xor=False, delay_time=0.0):
11
+ if not input_files:
12
+ print("No input files provided.")
13
+ return []
14
+
15
+ # filter out non-existent files
16
+ valid_files = [f for f in input_files if os.path.exists(f)]
17
+ if not valid_files:
18
+ print("No valid input files found.")
19
+ return []
20
+
21
+ # get dimensions and areas to find the lowest resolution
22
+ file_dims = {}
23
+ min_area = float("inf")
24
+ target_w, target_h = None, None
25
+ target_file = None
26
+
27
+ for f in valid_files:
28
+ try:
29
+ with Image.open(f) as img:
30
+ w, h = img.size
31
+ area = w * h
32
+ file_dims[f] = (w, h, area)
33
+ if area < min_area:
34
+ min_area = area
35
+ target_w, target_h = w, h
36
+ target_file = f
37
+ except Exception as e:
38
+ print(f"Error reading dimensions for {f}: {e}")
39
+ return []
40
+
41
+ # downscale higher resolution files to match the lowest resolution file
42
+ synced_files = []
43
+ created_synced_files = [] # array to track newly generated downscaled files
44
+
45
+ for f in valid_files:
46
+ w, h, area = file_dims[f]
47
+ if area > min_area:
48
+ synced_name = f"{os.path.splitext(f)[0]}_synced.bmp"
49
+ print(
50
+ f"Downscaling {f} ({w}x{h}) to match {target_file} ({target_w}x{target_h})..."
51
+ )
52
+ try:
53
+ with Image.open(f) as img:
54
+ resized_img = img.resize(
55
+ (target_w, target_h), Image.Resampling.LANCZOS
56
+ )
57
+ resized_img.save(synced_name, format="BMP")
58
+ synced_files.append(synced_name)
59
+ created_synced_files.append(synced_name) # log the file path
60
+ except Exception as e:
61
+ print(f"Failed to resize {f}: {e}")
62
+ return []
63
+ else:
64
+ synced_files.append(f)
65
+
66
+ print(
67
+ f"Bending {len(synced_files)} BMP file(s) | Enc: {encoding.upper()} | XOR: {use_xor} | Echo: {delay_time}s"
68
+ )
69
+
70
+ # extract header from the first synced file
71
+ with open(synced_files[0], "rb") as f:
72
+ first_file_data = f.read()
73
+
74
+ pixel_offset = struct.unpack_from("<I", first_file_data, 10)[0]
75
+ header = bytearray(first_file_data[:pixel_offset])
76
+
77
+ target_byte_length = len(first_file_data) - pixel_offset
78
+
79
+ if encoding == "adpcm":
80
+ sample_multiplier = 2
81
+ else:
82
+ sample_multiplier = 1
83
+
84
+ target_sample_length = target_byte_length * sample_multiplier
85
+
86
+ # buffer for math
87
+ if use_xor:
88
+ mix_buffer = np.zeros(target_sample_length, dtype=np.int16)
89
+ else:
90
+ mix_buffer = np.zeros(target_sample_length, dtype=np.float32)
91
+
92
+ # where the real shit happens
93
+ for file in synced_files:
94
+ with open(file, "rb") as f:
95
+ data = f.read()
96
+ offset = struct.unpack_from("<I", data, 10)[0]
97
+ pixel_data = data[offset:]
98
+
99
+ if encoding == "ulaw":
100
+ pcm16_bytes = audioop.ulaw2lin(pixel_data, 2)
101
+ elif encoding == "adpcm":
102
+ pcm16_bytes, _ = audioop.adpcm2lin(pixel_data, 2, None)
103
+ else:
104
+ pcm16_bytes = audioop.alaw2lin(pixel_data, 2)
105
+
106
+ if use_xor:
107
+ audio_track = np.frombuffer(pcm16_bytes, dtype=np.int16)
108
+ else:
109
+ audio_track = np.frombuffer(pcm16_bytes, dtype=np.int16).astype(
110
+ np.float32
111
+ )
112
+
113
+ if len(audio_track) > target_sample_length:
114
+ audio_track = audio_track[:target_sample_length]
115
+ elif len(audio_track) < target_sample_length:
116
+ if use_xor:
117
+ padded = np.zeros(target_sample_length, dtype=np.int16)
118
+ else:
119
+ padded = np.zeros(target_sample_length, dtype=np.float32)
120
+ padded[: len(audio_track)] = audio_track
121
+ audio_track = padded
122
+
123
+ if use_xor:
124
+ mix_buffer ^= audio_track
125
+ else:
126
+ mix_buffer += audio_track
127
+
128
+ # apply delay / echo
129
+ if delay_time > 0.0:
130
+ delay_samples = int(delay_time * 44100)
131
+ echo_track = np.roll(mix_buffer, delay_samples)
132
+
133
+ if use_xor:
134
+ mix_buffer ^= echo_track
135
+ else:
136
+ mix_buffer += echo_track * 0.5
137
+
138
+ # Prepare for export
139
+ if not use_xor:
140
+ mix_buffer = np.clip(mix_buffer, -32768.0, 32767.0)
141
+ mixed_pcm16_bytes = mix_buffer.astype(np.int16).tobytes()
142
+ else:
143
+ mixed_pcm16_bytes = mix_buffer.tobytes()
144
+
145
+ if encoding == "ulaw":
146
+ final_pixels = audioop.lin2ulaw(mixed_pcm16_bytes, 2)
147
+ elif encoding == "adpcm":
148
+ final_pixels, _ = audioop.lin2adpcm(mixed_pcm16_bytes, 2, None)
149
+ else:
150
+ final_pixels = audioop.lin2alaw(mixed_pcm16_bytes, 2)
151
+
152
+ # export
153
+ with open(output_file, "wb") as f:
154
+ f.write(header)
155
+ f.write(final_pixels[:target_byte_length])
156
+
157
+ print(f"Successfully bended: {output_file}")
158
+
159
+ # returns array of downscaled images to the caller app for deletion later
160
+ return created_synced_files
161
+
162
+
163
+ # exec stuff
164
+ if __name__ == "__main__":
165
+ parser = argparse.ArgumentParser(
166
+ description="Databend BMP files using audio algorithms."
167
+ )
168
+ parser.add_argument("images", nargs="+", help="Input BMP files")
169
+ parser.add_argument(
170
+ "-o",
171
+ "--output",
172
+ default="out/mash.bmp",
173
+ help="Output BMP file (default: out/mash.bmp)",
174
+ )
175
+ parser.add_argument("-u", "--ulaw", action="store_true", help="Use u-law encoding")
176
+ parser.add_argument("-a", "--adpcm", action="store_true", help="Use ADPCM encoding")
177
+ parser.add_argument(
178
+ "-x",
179
+ "--xor",
180
+ action="store_true",
181
+ help="Use Bitwise XOR mixing instead of additive",
182
+ )
183
+ parser.add_argument(
184
+ "-e", "--echo", action="store_true", help="Enable Delay/Echo effect"
185
+ )
186
+ parser.add_argument(
187
+ "-n",
188
+ "--delay_time",
189
+ type=float,
190
+ default=0.1,
191
+ help="Delay time in seconds for echo (default: 0.1)",
192
+ )
193
+
194
+ args = parser.parse_args()
195
+
196
+ encoding = "alaw"
197
+ if args.adpcm:
198
+ encoding = "adpcm"
199
+ elif args.ulaw:
200
+ encoding = "ulaw"
201
+
202
+ delay_to_pass = args.delay_time if args.echo else 0.0
203
+
204
+ output_dir = os.path.dirname(args.output)
205
+ if output_dir:
206
+ os.makedirs(output_dir, exist_ok=True)
207
+
208
+ databend(
209
+ args.images,
210
+ args.output,
211
+ encoding=encoding,
212
+ use_xor=args.xor,
213
+ delay_time=delay_to_pass,
214
+ )
@@ -0,0 +1,33 @@
1
+ import os
2
+
3
+ from PIL import Image
4
+ from PySide6.QtWidgets import QFileDialog
5
+
6
+
7
+ def browse_file():
8
+ input_file_path, _ = QFileDialog.getOpenFileNames(
9
+ None, caption="Select bmp files", filter="Images (*.png *.jpg *.jpeg *.bmp)"
10
+ )
11
+
12
+ file_path = []
13
+ if input_file_path:
14
+ for paths in input_file_path:
15
+ imgbeforergb = Image.open(paths)
16
+ img = imgbeforergb.convert("RGB")
17
+ file_basename = os.path.basename(paths)
18
+ name, extension = os.path.splitext(file_basename)
19
+ bmp_filename = name + ".bmp"
20
+
21
+ output_path = os.path.join("/tmp", bmp_filename)
22
+
23
+ img.save(output_path)
24
+ file_path.append(output_path)
25
+ return file_path
26
+
27
+
28
+ def save_file():
29
+ save_path, _ = QFileDialog.getSaveFileName(
30
+ parent=None, caption="Save file:", filter="Bitmap Files (*.bmp)"
31
+ )
32
+ if save_path:
33
+ return save_path
@@ -0,0 +1,21 @@
1
+ Metadata-Version: 2.4
2
+ Name: Quickbend
3
+ Version: 1.0.0
4
+ Summary: an app to create glitchart using pyside6
5
+ Author-email: Daniel Crutti <dancrutti@gmail.com>
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
8
+ Classifier: Operating System :: OS Independent
9
+ Requires-Python: >=3.9
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: audioop-lts==0.2.2
12
+ Requires-Dist: numpy==2.4.6
13
+ Requires-Dist: pillow==12.2.0
14
+ Requires-Dist: PySide6==6.11.1
15
+ Requires-Dist: PySide6_Addons==6.11.1
16
+ Requires-Dist: PySide6_Essentials==6.11.1
17
+ Requires-Dist: shiboken6==6.11.1
18
+
19
+ # Quickbend
20
+
21
+ **Quickbend** is a tool for creating art via databending with images. It uses interpreting bmp files as logarithmic audio encoding schemes (a-law/u-law/adpcm) and then reconverting those back to bmp to create warped and glitched images.
@@ -0,0 +1,12 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/Quickbend/__init__.py
4
+ src/Quickbend/app.py
5
+ src/Quickbend/databend.py
6
+ src/Quickbend/files.py
7
+ src/Quickbend.egg-info/PKG-INFO
8
+ src/Quickbend.egg-info/SOURCES.txt
9
+ src/Quickbend.egg-info/dependency_links.txt
10
+ src/Quickbend.egg-info/entry_points.txt
11
+ src/Quickbend.egg-info/requires.txt
12
+ src/Quickbend.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ Quickbend = Quickbend.app:main
@@ -0,0 +1,7 @@
1
+ audioop-lts==0.2.2
2
+ numpy==2.4.6
3
+ pillow==12.2.0
4
+ PySide6==6.11.1
5
+ PySide6_Addons==6.11.1
6
+ PySide6_Essentials==6.11.1
7
+ shiboken6==6.11.1
@@ -0,0 +1 @@
1
+ Quickbend