peg-this 4.0.0__py3-none-any.whl → 4.1.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.
- peg_this/features/audio.py +63 -16
- peg_this/features/batch.py +105 -79
- peg_this/features/convert.py +179 -95
- peg_this/features/crop.py +63 -34
- peg_this/features/inspect.py +71 -21
- peg_this/features/join.py +85 -31
- peg_this/features/subtitle.py +397 -0
- peg_this/features/trim.py +40 -10
- peg_this/peg_this.py +4 -1
- peg_this/utils/validation.py +228 -0
- peg_this-4.1.0.dist-info/METADATA +283 -0
- peg_this-4.1.0.dist-info/RECORD +21 -0
- {peg_this-4.0.0.dist-info → peg_this-4.1.0.dist-info}/WHEEL +1 -1
- peg_this-4.0.0.dist-info/METADATA +0 -164
- peg_this-4.0.0.dist-info/RECORD +0 -19
- {peg_this-4.0.0.dist-info → peg_this-4.1.0.dist-info}/entry_points.txt +0 -0
- {peg_this-4.0.0.dist-info → peg_this-4.1.0.dist-info}/licenses/LICENSE +0 -0
- {peg_this-4.0.0.dist-info → peg_this-4.1.0.dist-info}/top_level.txt +0 -0
peg_this/features/convert.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
import os
|
|
3
2
|
from pathlib import Path
|
|
4
3
|
|
|
@@ -7,32 +6,52 @@ import questionary
|
|
|
7
6
|
from rich.console import Console
|
|
8
7
|
|
|
9
8
|
from peg_this.utils.ffmpeg_utils import run_command, has_audio_stream
|
|
9
|
+
from peg_this.utils.validation import (
|
|
10
|
+
validate_input_file, check_output_file, validate_positive_integer, press_continue
|
|
11
|
+
)
|
|
10
12
|
|
|
11
13
|
console = Console()
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
def convert_file(file_path):
|
|
15
|
-
|
|
17
|
+
if not validate_input_file(file_path):
|
|
18
|
+
press_continue()
|
|
19
|
+
return
|
|
20
|
+
|
|
16
21
|
is_gif = Path(file_path).suffix.lower() == '.gif'
|
|
17
22
|
has_audio = has_audio_stream(file_path)
|
|
18
23
|
|
|
19
|
-
output_format = questionary.select(
|
|
20
|
-
|
|
24
|
+
output_format = questionary.select(
|
|
25
|
+
"Select the output format:",
|
|
26
|
+
choices=["mp4", "mkv", "mov", "avi", "webm", "mp3", "flac", "wav", "gif"]
|
|
27
|
+
).ask()
|
|
28
|
+
if not output_format:
|
|
29
|
+
return
|
|
21
30
|
|
|
22
31
|
if (is_gif or not has_audio) and output_format in ["mp3", "flac", "wav"]:
|
|
23
32
|
console.print("[bold red]Error: Source has no audio to convert.[/bold red]")
|
|
24
|
-
|
|
33
|
+
press_continue()
|
|
25
34
|
return
|
|
26
35
|
|
|
27
36
|
output_file = f"{Path(file_path).stem}_converted.{output_format}"
|
|
28
|
-
|
|
37
|
+
action_result, final_output = check_output_file(output_file, "Output file")
|
|
38
|
+
|
|
39
|
+
if action_result == 'cancel':
|
|
40
|
+
console.print("[yellow]Operation cancelled.[/yellow]")
|
|
41
|
+
press_continue()
|
|
42
|
+
return
|
|
43
|
+
|
|
29
44
|
input_stream = ffmpeg.input(file_path)
|
|
30
45
|
output_stream = None
|
|
31
|
-
kwargs = {
|
|
46
|
+
kwargs = {}
|
|
32
47
|
|
|
33
48
|
if output_format in ["mp4", "mkv", "mov", "avi", "webm"]:
|
|
34
|
-
quality = questionary.select(
|
|
35
|
-
|
|
49
|
+
quality = questionary.select(
|
|
50
|
+
"Select quality preset:",
|
|
51
|
+
choices=["Same as source", "High (CRF 18)", "Medium (CRF 23)", "Low (CRF 28)"]
|
|
52
|
+
).ask()
|
|
53
|
+
if not quality:
|
|
54
|
+
return
|
|
36
55
|
|
|
37
56
|
if quality == "Same as source":
|
|
38
57
|
kwargs['c'] = 'copy'
|
|
@@ -46,73 +65,101 @@ def convert_file(file_path):
|
|
|
46
65
|
kwargs['b:a'] = '192k'
|
|
47
66
|
else:
|
|
48
67
|
kwargs['an'] = None
|
|
49
|
-
output_stream = input_stream.output(
|
|
68
|
+
output_stream = input_stream.output(final_output, **kwargs)
|
|
50
69
|
|
|
51
70
|
elif output_format in ["mp3", "flac", "wav"]:
|
|
52
71
|
kwargs['vn'] = None
|
|
53
72
|
if output_format == 'mp3':
|
|
54
|
-
bitrate = questionary.select(
|
|
55
|
-
|
|
73
|
+
bitrate = questionary.select(
|
|
74
|
+
"Select audio bitrate:",
|
|
75
|
+
choices=["128k", "192k", "256k", "320k"]
|
|
76
|
+
).ask()
|
|
77
|
+
if not bitrate:
|
|
78
|
+
return
|
|
56
79
|
kwargs['c:a'] = 'libmp3lame'
|
|
57
80
|
kwargs['b:a'] = bitrate
|
|
58
81
|
else:
|
|
59
82
|
kwargs['c:a'] = output_format
|
|
60
|
-
output_stream = input_stream.output(
|
|
83
|
+
output_stream = input_stream.output(final_output, **kwargs)
|
|
61
84
|
|
|
62
85
|
elif output_format == "gif":
|
|
63
86
|
fps = questionary.text("Enter frame rate (e.g., 15):", default="15").ask()
|
|
64
|
-
if not fps:
|
|
87
|
+
if not fps:
|
|
88
|
+
return
|
|
89
|
+
fps_val = validate_positive_integer(fps, "Frame rate")
|
|
90
|
+
if not fps_val:
|
|
91
|
+
press_continue()
|
|
92
|
+
return
|
|
93
|
+
|
|
65
94
|
scale = questionary.text("Enter width in pixels (e.g., 480):", default="480").ask()
|
|
66
|
-
if not scale:
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
palette_gen_stream = input_stream.video.filter('fps', fps=fps).filter('scale', w=scale, h=-1, flags='lanczos').filter('palettegen')
|
|
72
|
-
run_command(palette_gen_stream.output(palette_file, y=None), "Generating color palette...")
|
|
73
|
-
|
|
74
|
-
if not os.path.exists(palette_file):
|
|
75
|
-
console.print("[bold red]Failed to generate color palette for GIF.[/bold red]")
|
|
76
|
-
questionary.press_any_key_to_continue().ask()
|
|
95
|
+
if not scale:
|
|
96
|
+
return
|
|
97
|
+
scale_val = validate_positive_integer(scale, "Width")
|
|
98
|
+
if not scale_val:
|
|
99
|
+
press_continue()
|
|
77
100
|
return
|
|
78
101
|
|
|
79
|
-
|
|
80
|
-
video_stream = input_stream.video.filter('fps', fps=fps).filter('scale', w=scale, h=-1, flags='lanczos')
|
|
81
|
-
|
|
82
|
-
final_stream = ffmpeg.filter([video_stream, palette_input], 'paletteuse')
|
|
83
|
-
output_stream = final_stream.output(output_file, y=None)
|
|
102
|
+
palette_file = f"palette_{Path(file_path).stem}.png"
|
|
84
103
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
104
|
+
try:
|
|
105
|
+
palette_gen_stream = input_stream.video.filter('fps', fps=fps_val).filter('scale', w=scale_val, h=-1, flags='lanczos').filter('palettegen')
|
|
106
|
+
run_command(palette_gen_stream.output(palette_file).overwrite_output(), "Generating color palette...")
|
|
107
|
+
|
|
108
|
+
if not os.path.exists(palette_file):
|
|
109
|
+
console.print("[bold red]Failed to generate color palette for GIF.[/bold red]")
|
|
110
|
+
press_continue()
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
palette_input = ffmpeg.input(palette_file)
|
|
114
|
+
video_stream = input_stream.video.filter('fps', fps=fps_val).filter('scale', w=scale_val, h=-1, flags='lanczos')
|
|
115
|
+
final_stream = ffmpeg.filter([video_stream, palette_input], 'paletteuse')
|
|
116
|
+
output_stream = final_stream.output(final_output)
|
|
117
|
+
|
|
118
|
+
finally:
|
|
119
|
+
if os.path.exists(palette_file):
|
|
120
|
+
os.remove(palette_file)
|
|
121
|
+
|
|
122
|
+
if output_stream:
|
|
123
|
+
if action_result == 'overwrite':
|
|
124
|
+
output_stream = output_stream.overwrite_output()
|
|
125
|
+
|
|
126
|
+
if run_command(output_stream, f"Converting to {output_format}...", show_progress=True):
|
|
127
|
+
console.print(f"[bold green]Successfully converted to {final_output}[/bold green]")
|
|
128
|
+
else:
|
|
129
|
+
console.print("[bold red]Conversion failed.[/bold red]")
|
|
89
130
|
|
|
90
|
-
|
|
91
|
-
os.remove(f"palette_{Path(file_path).stem}.png")
|
|
92
|
-
|
|
93
|
-
questionary.press_any_key_to_continue().ask()
|
|
131
|
+
press_continue()
|
|
94
132
|
|
|
95
133
|
|
|
96
134
|
def convert_image(file_path):
|
|
97
|
-
|
|
135
|
+
if not validate_input_file(file_path):
|
|
136
|
+
press_continue()
|
|
137
|
+
return
|
|
138
|
+
|
|
98
139
|
output_format = questionary.select(
|
|
99
140
|
"Select the output format:",
|
|
100
|
-
choices=["jpg", "png", "webp", "bmp", "tiff"]
|
|
101
|
-
use_indicator=True
|
|
141
|
+
choices=["jpg", "png", "webp", "bmp", "tiff"]
|
|
102
142
|
).ask()
|
|
103
|
-
if not output_format:
|
|
143
|
+
if not output_format:
|
|
144
|
+
return
|
|
104
145
|
|
|
105
146
|
output_file = f"{Path(file_path).stem}_converted.{output_format}"
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
147
|
+
action_result, final_output = check_output_file(output_file, "Image file")
|
|
148
|
+
|
|
149
|
+
if action_result == 'cancel':
|
|
150
|
+
console.print("[yellow]Operation cancelled.[/yellow]")
|
|
151
|
+
press_continue()
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
kwargs = {}
|
|
155
|
+
|
|
109
156
|
if output_format in ['jpg', 'webp']:
|
|
110
157
|
quality_preset = questionary.select(
|
|
111
158
|
"Select quality preset:",
|
|
112
|
-
choices=["High (95%)", "Medium (80%)", "Low (60%)"]
|
|
113
|
-
use_indicator=True
|
|
159
|
+
choices=["High (95%)", "Medium (80%)", "Low (60%)"]
|
|
114
160
|
).ask()
|
|
115
|
-
if not quality_preset:
|
|
161
|
+
if not quality_preset:
|
|
162
|
+
return
|
|
116
163
|
|
|
117
164
|
quality_map = {"High (95%)": "95", "Medium (80%)": "80", "Low (60%)": "60"}
|
|
118
165
|
quality = quality_map[quality_preset]
|
|
@@ -123,103 +170,140 @@ def convert_image(file_path):
|
|
|
123
170
|
elif output_format == 'webp':
|
|
124
171
|
kwargs['quality'] = quality
|
|
125
172
|
|
|
126
|
-
stream = ffmpeg.input(file_path).output(
|
|
127
|
-
|
|
173
|
+
stream = ffmpeg.input(file_path).output(final_output, **kwargs)
|
|
174
|
+
|
|
175
|
+
if action_result == 'overwrite':
|
|
176
|
+
stream = stream.overwrite_output()
|
|
177
|
+
|
|
128
178
|
if run_command(stream, f"Converting to {output_format.upper()}..."):
|
|
129
|
-
console.print(f"[bold green]Successfully converted image to {
|
|
179
|
+
console.print(f"[bold green]Successfully converted image to {final_output}[/bold green]")
|
|
130
180
|
else:
|
|
131
181
|
console.print("[bold red]Image conversion failed.[/bold red]")
|
|
132
|
-
|
|
133
|
-
|
|
182
|
+
|
|
183
|
+
press_continue()
|
|
134
184
|
|
|
135
185
|
|
|
136
186
|
def resize_image(file_path):
|
|
137
|
-
|
|
187
|
+
if not validate_input_file(file_path):
|
|
188
|
+
press_continue()
|
|
189
|
+
return
|
|
190
|
+
|
|
138
191
|
console.print("Enter new dimensions. Use [bold]-1[/bold] for one dimension to preserve aspect ratio.")
|
|
139
192
|
width = questionary.text("Enter new width (e.g., 1280 or -1):").ask()
|
|
140
|
-
if not width:
|
|
193
|
+
if not width:
|
|
194
|
+
return
|
|
141
195
|
height = questionary.text("Enter new height (e.g., 720 or -1):").ask()
|
|
142
|
-
if not height:
|
|
196
|
+
if not height:
|
|
197
|
+
return
|
|
143
198
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
199
|
+
width_val = validate_positive_integer(width, "Width")
|
|
200
|
+
height_val = validate_positive_integer(height, "Height")
|
|
201
|
+
|
|
202
|
+
if width_val is None or height_val is None:
|
|
203
|
+
press_continue()
|
|
204
|
+
return
|
|
205
|
+
|
|
206
|
+
if width_val == -1 and height_val == -1:
|
|
207
|
+
console.print("[bold red]Error: Width and Height cannot both be -1.[/bold red]")
|
|
208
|
+
press_continue()
|
|
152
209
|
return
|
|
153
210
|
|
|
154
211
|
output_file = f"{Path(file_path).stem}_resized{Path(file_path).suffix}"
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
212
|
+
action_result, final_output = check_output_file(output_file, "Image file")
|
|
213
|
+
|
|
214
|
+
if action_result == 'cancel':
|
|
215
|
+
console.print("[yellow]Operation cancelled.[/yellow]")
|
|
216
|
+
press_continue()
|
|
217
|
+
return
|
|
218
|
+
|
|
219
|
+
stream = ffmpeg.input(file_path).filter('scale', w=width_val, h=height_val).output(final_output)
|
|
220
|
+
|
|
221
|
+
if action_result == 'overwrite':
|
|
222
|
+
stream = stream.overwrite_output()
|
|
223
|
+
|
|
158
224
|
if run_command(stream, "Resizing image..."):
|
|
159
|
-
console.print(f"[bold green]Successfully resized image to {
|
|
225
|
+
console.print(f"[bold green]Successfully resized image to {final_output}[/bold green]")
|
|
160
226
|
else:
|
|
161
227
|
console.print("[bold red]Image resizing failed.[/bold red]")
|
|
162
|
-
|
|
163
|
-
|
|
228
|
+
|
|
229
|
+
press_continue()
|
|
164
230
|
|
|
165
231
|
|
|
166
232
|
def rotate_image(file_path):
|
|
167
|
-
|
|
233
|
+
if not validate_input_file(file_path):
|
|
234
|
+
press_continue()
|
|
235
|
+
return
|
|
236
|
+
|
|
168
237
|
rotation = questionary.select(
|
|
169
238
|
"Select rotation:",
|
|
170
|
-
choices=[
|
|
171
|
-
"90 degrees clockwise",
|
|
172
|
-
"90 degrees counter-clockwise",
|
|
173
|
-
"180 degrees"
|
|
174
|
-
],
|
|
175
|
-
use_indicator=True
|
|
239
|
+
choices=["90 degrees clockwise", "90 degrees counter-clockwise", "180 degrees"]
|
|
176
240
|
).ask()
|
|
177
|
-
if not rotation:
|
|
241
|
+
if not rotation:
|
|
242
|
+
return
|
|
178
243
|
|
|
179
244
|
output_file = f"{Path(file_path).stem}_rotated{Path(file_path).suffix}"
|
|
180
|
-
|
|
245
|
+
action_result, final_output = check_output_file(output_file, "Image file")
|
|
246
|
+
|
|
247
|
+
if action_result == 'cancel':
|
|
248
|
+
console.print("[yellow]Operation cancelled.[/yellow]")
|
|
249
|
+
press_continue()
|
|
250
|
+
return
|
|
251
|
+
|
|
181
252
|
stream = ffmpeg.input(file_path)
|
|
182
253
|
if rotation == "90 degrees clockwise":
|
|
183
254
|
stream = stream.filter('transpose', 1)
|
|
184
255
|
elif rotation == "90 degrees counter-clockwise":
|
|
185
256
|
stream = stream.filter('transpose', 2)
|
|
186
257
|
elif rotation == "180 degrees":
|
|
187
|
-
# Apply 90-degree rotation twice for 180 degrees
|
|
188
258
|
stream = stream.filter('transpose', 2).filter('transpose', 2)
|
|
189
259
|
|
|
190
|
-
output_stream = stream.output(
|
|
191
|
-
|
|
260
|
+
output_stream = stream.output(final_output)
|
|
261
|
+
|
|
262
|
+
if action_result == 'overwrite':
|
|
263
|
+
output_stream = output_stream.overwrite_output()
|
|
264
|
+
|
|
192
265
|
if run_command(output_stream, "Rotating image..."):
|
|
193
|
-
console.print(f"[bold green]Successfully rotated image and saved to {
|
|
266
|
+
console.print(f"[bold green]Successfully rotated image and saved to {final_output}[/bold green]")
|
|
194
267
|
else:
|
|
195
268
|
console.print("[bold red]Image rotation failed.[/bold red]")
|
|
196
|
-
|
|
197
|
-
|
|
269
|
+
|
|
270
|
+
press_continue()
|
|
198
271
|
|
|
199
272
|
|
|
200
273
|
def flip_image(file_path):
|
|
201
|
-
|
|
274
|
+
if not validate_input_file(file_path):
|
|
275
|
+
press_continue()
|
|
276
|
+
return
|
|
277
|
+
|
|
202
278
|
flip_direction = questionary.select(
|
|
203
279
|
"Select flip direction:",
|
|
204
|
-
choices=["Horizontal", "Vertical"]
|
|
205
|
-
use_indicator=True
|
|
280
|
+
choices=["Horizontal", "Vertical"]
|
|
206
281
|
).ask()
|
|
207
|
-
if not flip_direction:
|
|
282
|
+
if not flip_direction:
|
|
283
|
+
return
|
|
208
284
|
|
|
209
285
|
output_file = f"{Path(file_path).stem}_flipped{Path(file_path).suffix}"
|
|
210
|
-
|
|
286
|
+
action_result, final_output = check_output_file(output_file, "Image file")
|
|
287
|
+
|
|
288
|
+
if action_result == 'cancel':
|
|
289
|
+
console.print("[yellow]Operation cancelled.[/yellow]")
|
|
290
|
+
press_continue()
|
|
291
|
+
return
|
|
292
|
+
|
|
211
293
|
stream = ffmpeg.input(file_path)
|
|
212
294
|
if flip_direction == "Horizontal":
|
|
213
295
|
stream = stream.filter('hflip')
|
|
214
296
|
else:
|
|
215
297
|
stream = stream.filter('vflip')
|
|
216
298
|
|
|
217
|
-
output_stream = stream.output(
|
|
218
|
-
|
|
299
|
+
output_stream = stream.output(final_output)
|
|
300
|
+
|
|
301
|
+
if action_result == 'overwrite':
|
|
302
|
+
output_stream = output_stream.overwrite_output()
|
|
303
|
+
|
|
219
304
|
if run_command(output_stream, "Flipping image..."):
|
|
220
|
-
console.print(f"[bold green]Successfully flipped image and saved to {
|
|
305
|
+
console.print(f"[bold green]Successfully flipped image and saved to {final_output}[/bold green]")
|
|
221
306
|
else:
|
|
222
307
|
console.print("[bold red]Image flipping failed.[/bold red]")
|
|
223
|
-
|
|
224
|
-
questionary.press_any_key_to_continue().ask()
|
|
225
308
|
|
|
309
|
+
press_continue()
|
peg_this/features/crop.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
import os
|
|
3
2
|
from pathlib import Path
|
|
4
3
|
|
|
@@ -7,6 +6,9 @@ import questionary
|
|
|
7
6
|
from rich.console import Console
|
|
8
7
|
|
|
9
8
|
from peg_this.utils.ffmpeg_utils import run_command, has_audio_stream
|
|
9
|
+
from peg_this.utils.validation import (
|
|
10
|
+
validate_input_file, check_output_file, warn_reencode, press_continue
|
|
11
|
+
)
|
|
10
12
|
|
|
11
13
|
try:
|
|
12
14
|
import tkinter as tk
|
|
@@ -19,29 +21,38 @@ console = Console()
|
|
|
19
21
|
|
|
20
22
|
|
|
21
23
|
def crop_video(file_path):
|
|
22
|
-
"""Visually crop a video by selecting an area."""
|
|
23
24
|
if not tk:
|
|
24
25
|
console.print("[bold red]Cannot perform visual cropping: tkinter & Pillow are not installed.[/bold red]")
|
|
26
|
+
console.print("[dim]Install them with: pip install tk Pillow[/dim]")
|
|
27
|
+
press_continue()
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
if not validate_input_file(file_path):
|
|
31
|
+
press_continue()
|
|
25
32
|
return
|
|
26
33
|
|
|
27
34
|
preview_frame = f"preview_{Path(file_path).stem}.jpg"
|
|
28
35
|
try:
|
|
29
|
-
# Extract a frame from the middle of the video for preview
|
|
30
36
|
probe = ffmpeg.probe(file_path)
|
|
31
|
-
duration = float(probe['format']
|
|
37
|
+
duration = float(probe['format'].get('duration', 0))
|
|
38
|
+
|
|
39
|
+
if duration <= 0:
|
|
40
|
+
console.print("[bold red]Error: Could not determine video duration.[/bold red]")
|
|
41
|
+
press_continue()
|
|
42
|
+
return
|
|
43
|
+
|
|
32
44
|
mid_point = duration / 2
|
|
33
|
-
|
|
34
|
-
# Corrected frame extraction command with `-q:v`
|
|
45
|
+
|
|
35
46
|
run_command(
|
|
36
|
-
ffmpeg.input(file_path, ss=mid_point).output(preview_frame, vframes=1, **{'q:v': 2}
|
|
47
|
+
ffmpeg.input(file_path, ss=mid_point).output(preview_frame, vframes=1, **{'q:v': 2}).overwrite_output(),
|
|
37
48
|
"Extracting a frame for preview..."
|
|
38
49
|
)
|
|
39
50
|
|
|
40
51
|
if not os.path.exists(preview_frame):
|
|
41
52
|
console.print("[bold red]Could not extract a frame from the video.[/bold red]")
|
|
53
|
+
press_continue()
|
|
42
54
|
return
|
|
43
55
|
|
|
44
|
-
# --- Tkinter GUI for Cropping ---
|
|
45
56
|
root = tk.Tk()
|
|
46
57
|
root.title("Crop Video - Drag to select area, close window to confirm")
|
|
47
58
|
root.attributes("-topmost", True)
|
|
@@ -67,65 +78,77 @@ def crop_video(file_path):
|
|
|
67
78
|
|
|
68
79
|
canvas.bind("<ButtonPress-1>", on_press)
|
|
69
80
|
canvas.bind("<B1-Motion>", on_drag)
|
|
70
|
-
|
|
81
|
+
|
|
71
82
|
messagebox.showinfo("Instructions", "Click and drag to draw a cropping rectangle.\nClose this window when you are done.", parent=root)
|
|
72
83
|
root.mainloop()
|
|
73
84
|
|
|
74
|
-
# --- Cropping Logic ---
|
|
75
85
|
crop_w = abs(rect_coords['x2'] - rect_coords['x1'])
|
|
76
86
|
crop_h = abs(rect_coords['y2'] - rect_coords['y1'])
|
|
77
87
|
crop_x = min(rect_coords['x1'], rect_coords['x2'])
|
|
78
88
|
crop_y = min(rect_coords['y1'], rect_coords['y2'])
|
|
79
89
|
|
|
80
|
-
if crop_w < 2 or crop_h < 2:
|
|
90
|
+
if crop_w < 2 or crop_h < 2:
|
|
81
91
|
console.print("[bold yellow]Cropping cancelled as no valid area was selected.[/bold yellow]")
|
|
82
92
|
return
|
|
83
93
|
|
|
84
94
|
console.print(f"Selected crop area: [bold]width={crop_w} height={crop_h} at (x={crop_x}, y={crop_y})[/bold]")
|
|
85
95
|
|
|
86
96
|
output_file = f"{Path(file_path).stem}_cropped{Path(file_path).suffix}"
|
|
87
|
-
|
|
97
|
+
action_result, final_output = check_output_file(output_file, "Video file")
|
|
98
|
+
|
|
99
|
+
if action_result == 'cancel':
|
|
100
|
+
console.print("[yellow]Operation cancelled.[/yellow]")
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
warn_reencode("Video cropping")
|
|
104
|
+
|
|
88
105
|
input_stream = ffmpeg.input(file_path)
|
|
89
106
|
video_stream = input_stream.video.filter('crop', w=crop_w, h=crop_h, x=crop_x, y=crop_y)
|
|
90
|
-
|
|
91
|
-
kwargs = {'y': None} # Overwrite output
|
|
92
|
-
# Check for audio and copy it if it exists
|
|
107
|
+
|
|
93
108
|
if has_audio_stream(file_path):
|
|
94
109
|
audio_stream = input_stream.audio
|
|
95
|
-
|
|
96
|
-
stream = ffmpeg.output(video_stream, audio_stream, output_file, **kwargs)
|
|
110
|
+
stream = ffmpeg.output(video_stream, audio_stream, final_output, **{'c:a': 'copy'})
|
|
97
111
|
else:
|
|
98
|
-
stream = ffmpeg.output(video_stream,
|
|
112
|
+
stream = ffmpeg.output(video_stream, final_output)
|
|
113
|
+
|
|
114
|
+
if action_result == 'overwrite':
|
|
115
|
+
stream = stream.overwrite_output()
|
|
99
116
|
|
|
100
|
-
run_command(stream, "Applying crop to video...", show_progress=True)
|
|
101
|
-
|
|
117
|
+
if run_command(stream, "Applying crop to video...", show_progress=True):
|
|
118
|
+
console.print(f"[bold green]Successfully cropped video and saved to {final_output}[/bold green]")
|
|
119
|
+
else:
|
|
120
|
+
console.print("[bold red]Video cropping failed.[/bold red]")
|
|
102
121
|
|
|
122
|
+
except Exception as e:
|
|
123
|
+
console.print(f"[bold red]An error occurred: {e}[/bold red]")
|
|
103
124
|
finally:
|
|
104
125
|
if os.path.exists(preview_frame):
|
|
105
126
|
os.remove(preview_frame)
|
|
106
|
-
|
|
127
|
+
press_continue()
|
|
107
128
|
|
|
108
129
|
|
|
109
130
|
def crop_image(file_path):
|
|
110
|
-
"""Visually crop an image by selecting an area."""
|
|
111
131
|
if not tk:
|
|
112
132
|
console.print("[bold red]Cannot perform visual cropping: tkinter & Pillow are not installed.[/bold red]")
|
|
113
|
-
console.print("
|
|
114
|
-
|
|
133
|
+
console.print("[dim]Install them with: pip install tk Pillow[/dim]")
|
|
134
|
+
press_continue()
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
if not validate_input_file(file_path):
|
|
138
|
+
press_continue()
|
|
115
139
|
return
|
|
116
140
|
|
|
117
141
|
try:
|
|
118
|
-
# --- Tkinter GUI for Cropping ---
|
|
119
142
|
root = tk.Tk()
|
|
120
143
|
root.title("Crop Image - Drag to select area, close window to confirm")
|
|
121
144
|
root.attributes("-topmost", True)
|
|
122
145
|
|
|
123
146
|
img = Image.open(file_path)
|
|
124
|
-
|
|
147
|
+
|
|
125
148
|
max_width = root.winfo_screenwidth() - 100
|
|
126
149
|
max_height = root.winfo_screenheight() - 100
|
|
127
150
|
img.thumbnail((max_width, max_height), Image.Resampling.LANCZOS)
|
|
128
|
-
|
|
151
|
+
|
|
129
152
|
img_tk = ImageTk.PhotoImage(img)
|
|
130
153
|
|
|
131
154
|
canvas = tk.Canvas(root, width=img.width, height=img.height, cursor="cross")
|
|
@@ -146,11 +169,10 @@ def crop_image(file_path):
|
|
|
146
169
|
|
|
147
170
|
canvas.bind("<ButtonPress-1>", on_press)
|
|
148
171
|
canvas.bind("<B1-Motion>", on_drag)
|
|
149
|
-
|
|
172
|
+
|
|
150
173
|
messagebox.showinfo("Instructions", "Click and drag to draw a cropping rectangle.\nClose this window when you are done.", parent=root)
|
|
151
174
|
root.mainloop()
|
|
152
175
|
|
|
153
|
-
# --- Cropping Logic ---
|
|
154
176
|
crop_w = abs(rect_coords['x2'] - rect_coords['x1'])
|
|
155
177
|
crop_h = abs(rect_coords['y2'] - rect_coords['y1'])
|
|
156
178
|
crop_x = min(rect_coords['x1'], rect_coords['x2'])
|
|
@@ -158,21 +180,28 @@ def crop_image(file_path):
|
|
|
158
180
|
|
|
159
181
|
if crop_w < 2 or crop_h < 2:
|
|
160
182
|
console.print("[bold yellow]Cropping cancelled as no valid area was selected.[/bold yellow]")
|
|
161
|
-
questionary.press_any_key_to_continue().ask()
|
|
162
183
|
return
|
|
163
184
|
|
|
164
185
|
console.print(f"Selected crop area: [bold]width={crop_w} height={crop_h} at (x={crop_x}, y={crop_y})[/bold]")
|
|
165
186
|
|
|
166
187
|
output_file = f"{Path(file_path).stem}_cropped{Path(file_path).suffix}"
|
|
167
|
-
|
|
168
|
-
|
|
188
|
+
action_result, final_output = check_output_file(output_file, "Image file")
|
|
189
|
+
|
|
190
|
+
if action_result == 'cancel':
|
|
191
|
+
console.print("[yellow]Operation cancelled.[/yellow]")
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
stream = ffmpeg.input(file_path).filter('crop', w=crop_w, h=crop_h, x=crop_x, y=crop_y).output(final_output)
|
|
195
|
+
|
|
196
|
+
if action_result == 'overwrite':
|
|
197
|
+
stream = stream.overwrite_output()
|
|
169
198
|
|
|
170
199
|
if run_command(stream, "Applying crop to image..."):
|
|
171
|
-
console.print(f"[bold green]Successfully cropped image and saved to {
|
|
200
|
+
console.print(f"[bold green]Successfully cropped image and saved to {final_output}[/bold green]")
|
|
172
201
|
else:
|
|
173
202
|
console.print("[bold red]Image cropping failed.[/bold red]")
|
|
174
203
|
|
|
175
204
|
except Exception as e:
|
|
176
205
|
console.print(f"[bold red]An error occurred during cropping: {e}[/bold red]")
|
|
177
206
|
finally:
|
|
178
|
-
|
|
207
|
+
press_continue()
|