screenz 1.10.2__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.
@@ -0,0 +1,74 @@
1
+ Metadata-Version: 2.3
2
+ Name: screenz
3
+ Version: 1.10.2
4
+ Summary: ScreenZ
5
+ License: MIT
6
+ Author: deadmaster
7
+ Author-email: deadmaster@noreply.codeberg.org
8
+ Requires-Python: >=3.13,<4.0
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Topic :: Multimedia :: Video
13
+ Requires-Dist: Pillow (>=11.2.1,<12.0.0)
14
+ Project-URL: Repository, https://codeberg.org/deadmaster/screenz
15
+ Description-Content-Type: text/markdown
16
+
17
+ # ScreenZ
18
+
19
+ ## Installation
20
+
21
+ Requires Python 3.13 and [FFmpeg](https://ffmpeg.org/download.html).
22
+
23
+ Install using `pip install --user screenz`.
24
+
25
+ Make sure `PATH` is properly set. In particular, `ffmpeg` and `ffprobe` must be in the `PATH` for the tool to function.
26
+
27
+ ## Usage
28
+
29
+ Generate screens for a single video:
30
+
31
+ ```
32
+ screenz video.mp4 output.jpg
33
+ ```
34
+
35
+ Generate screens for all videos:
36
+
37
+ ```
38
+ screenz --type recursive Videos Screens
39
+ ```
40
+
41
+ Generate a collage of all videos:
42
+
43
+ ```
44
+ screenz --type videos Videos Collage
45
+ ```
46
+
47
+ Generate a collage from all images:
48
+
49
+ ```
50
+ screenz --type images Videos Collage
51
+ ```
52
+
53
+ Generate an histogram of video length distributions:
54
+
55
+ ```
56
+ screenz --type histogram Videos histogram.png
57
+ ```
58
+
59
+ ## Advanced
60
+
61
+ Colors can be configured using the `--background`, `--foreground` and `--accent` options. They take colors in the `#rrggbb` format, or any [color name](https://pillow.readthedocs.io/en/stable/reference/ImageColor.html#color-names) supported by Pillow.
62
+
63
+ The background can also be a tiled image instead of a solid color by by prefixing the image path with `image:` as a value to the `--background` option.
64
+
65
+ Font can be changed using the `--font` option. It takes the name of a built-in font, or the path to a `.ttf` or `.otf` file. In this case, the `--font-size` is used to specify the pixel size.
66
+
67
+ The built-in fonts are: `6x12b`, `6x12n`, `8x14b`, `8x14n`, `8x16b`, `8x16n`, `10x18b`, `10x18n`, `10x20b`, `10x20n`, `11x22b`, `11x22n`, `12x24b`, `12x24n`, `14x28b`, `14x28n`, `16x32b` and `16x32n`.
68
+
69
+ The number of cells is controlled with `--count`, and their size with `--size`. Cells may be bigger or smaller than the requested size to account for aspect ratio, depending on the value of the `--scale` option. The defaults are adjusted to be compliant with the upload rules on a specific site.
70
+
71
+ ## Reference
72
+
73
+ Use `screenz --help` to see the full documentation on options.
74
+
@@ -0,0 +1,51 @@
1
+ scrz/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ scrz/__main__.py,sha256=nH5UTK904sFU7-B0a8JZ0mpuubP3Xoed6rcdIm848ZU,13357
3
+ scrz/extractor.py,sha256=bYAk0CAm51B8MDErL9xkLXi70IfmLBTucP1zTyTQIPE,11591
4
+ scrz/font/10x18b.pil,sha256=AORnVS81PqgfyiewLAuzOGSuRxcudjzBPlcBmhk4pM8,5143
5
+ scrz/font/10x18b.png,sha256=_h6kWSjkA5o6F3J00fP1C_uyvi0myns8sHVyyhjNqHY,2070
6
+ scrz/font/10x18n.pil,sha256=AORnVS81PqgfyiewLAuzOGSuRxcudjzBPlcBmhk4pM8,5143
7
+ scrz/font/10x18n.png,sha256=g48W8elDOk40PLtyZD3R3IIoVxS3XwbBWilB89GJEos,1926
8
+ scrz/font/10x20b.pil,sha256=vw7cvKMBpSkpUwU3GxbhoN4sIwAYqz-p3whx3GcgijM,5143
9
+ scrz/font/10x20b.png,sha256=p5U9J5wHHhFpZRe00Cn0EzCoIhx5UUDeUBuZCrj7cQg,2121
10
+ scrz/font/10x20n.pil,sha256=vw7cvKMBpSkpUwU3GxbhoN4sIwAYqz-p3whx3GcgijM,5143
11
+ scrz/font/10x20n.png,sha256=nN5AeXiKNnkQFjwnYBHxlPk7unxaM_GZb4Tq-hTAH0E,1943
12
+ scrz/font/11x22b.pil,sha256=rkYFbkyVT1RJl3lRVgi5YF-lmni0gPpGMGyKlT-1P3w,5143
13
+ scrz/font/11x22b.png,sha256=Je4M622dGtS49NbUxRsVao8ZI3glmommYkTs4D8ePKc,2585
14
+ scrz/font/11x22n.pil,sha256=rkYFbkyVT1RJl3lRVgi5YF-lmni0gPpGMGyKlT-1P3w,5143
15
+ scrz/font/11x22n.png,sha256=kBiN4ewD4GyaUqHe-24N9DcZPtAAdzfbLJJC7UsqDaY,2343
16
+ scrz/font/12x24b.pil,sha256=3jB5Y36Kpvrr3Me7dcEJryQFpRgYRi1A9nn_gu_FxD8,5143
17
+ scrz/font/12x24b.png,sha256=feXVq-x21Ooe4HMSwizGRN526a6SowhCbb6k3Hhc99Y,2528
18
+ scrz/font/12x24n.pil,sha256=3jB5Y36Kpvrr3Me7dcEJryQFpRgYRi1A9nn_gu_FxD8,5143
19
+ scrz/font/12x24n.png,sha256=S6cWVRlvMoNxkQvSevsCyK-PbiqJPt54JXPk2rgRXLo,2345
20
+ scrz/font/14x28b.pil,sha256=F4NSXvcup5MlJy-hJ3Xw3fp7jyi7gfttHUpVB6T8mdY,5143
21
+ scrz/font/14x28b.png,sha256=EvF9qhbwzZGLYfOm37u6Ni87BfJSumYnT0XDnBhKmi0,3293
22
+ scrz/font/14x28n.pil,sha256=F4NSXvcup5MlJy-hJ3Xw3fp7jyi7gfttHUpVB6T8mdY,5143
23
+ scrz/font/14x28n.png,sha256=rtuQTOzhc2R3S1zqxm6z8Y4aUUlBjcWu7fmm2-JI_zg,3032
24
+ scrz/font/16x32b.pil,sha256=9Ay2hGi50Iro8d7V0TtW1h7AWR-R0sb8DrHO7BbwKUc,5143
25
+ scrz/font/16x32b.png,sha256=4RXoZCVQA-0EUGQx5wy8JYOP7G0aRoOqRd4Q-gyka9k,3315
26
+ scrz/font/16x32n.pil,sha256=9Ay2hGi50Iro8d7V0TtW1h7AWR-R0sb8DrHO7BbwKUc,5143
27
+ scrz/font/16x32n.png,sha256=qTlT-0PhHeT3LMBSTqK-ggGDgqUZ8yuxF6gDgUI5npM,3372
28
+ scrz/font/6x12b.pil,sha256=u-DJHEX569KePxH3cqkuYGTksm-D_8rP8th2X5ih7_E,5143
29
+ scrz/font/6x12b.png,sha256=AdqC56Alrnos47yc8X1uYsBPk5Iaf9Rumupj3aO9hUc,1306
30
+ scrz/font/6x12n.pil,sha256=u-DJHEX569KePxH3cqkuYGTksm-D_8rP8th2X5ih7_E,5143
31
+ scrz/font/6x12n.png,sha256=AdqC56Alrnos47yc8X1uYsBPk5Iaf9Rumupj3aO9hUc,1306
32
+ scrz/font/8x14b.pil,sha256=lFJOVFd9foETxRXqQ2wcB7QHczLk9fR9b4Sbbg3onlQ,5143
33
+ scrz/font/8x14b.png,sha256=3MBYQLoP2W_f8m3Oxow_utNquCx102AZ18ZiEO8oqmU,1437
34
+ scrz/font/8x14n.pil,sha256=lFJOVFd9foETxRXqQ2wcB7QHczLk9fR9b4Sbbg3onlQ,5143
35
+ scrz/font/8x14n.png,sha256=fvhfvY1BchHWNwWk4U9f2l5U2CJk_ODlf0d2YBgOC94,1406
36
+ scrz/font/8x16b.pil,sha256=dsW6vPeFE7XcKPnS_Bh9e7-4DyiB7PGSZ8ZZBBM8ka4,5143
37
+ scrz/font/8x16b.png,sha256=V4Csy6H0EEcxJSvYi_WkR3zs4Tcw3UVjE9_Uyknt5I8,1389
38
+ scrz/font/8x16n.pil,sha256=dsW6vPeFE7XcKPnS_Bh9e7-4DyiB7PGSZ8ZZBBM8ka4,5143
39
+ scrz/font/8x16n.png,sha256=5hwO0eFhBOhxTdrOv-iqorRt128LeTc0qbFtj1QygXE,1367
40
+ scrz/font/__init__.py,sha256=MvVPFQnfIYCWrx5W6LBa2Q_gXxXs--T30K3UGGaZDTM,1163
41
+ scrz/generator.py,sha256=PDbCrQWVJkJjOSOonMQoTPp1h_U3Ld1t7wzU5TrViHM,14951
42
+ scrz/histogram.py,sha256=UIm4ECu_VSUYYOtTLlkjSUcAYdydY-Z_KJfhtH_wdn8,2732
43
+ scrz/layout.py,sha256=PZz0crzxspOO0Vi_vCnvpdQ06LGl4q0ORdc6M1ngiyo,12427
44
+ scrz/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
+ scrz/renderer.py,sha256=XtCAluiB-JvLb-yqXEGW8tFb_CDogCvsFltexm99ydc,3525
46
+ scrz/types.py,sha256=v3JDbmy26_53lFcZS4Euvh9h_4In-KKkJ8b3bqMJNJQ,226
47
+ scrz/vignette.py,sha256=RZlJ_XfirVbhad6PAYavGWmPm40Z_escexc0VIFV4qs,941
48
+ screenz-1.10.2.dist-info/METADATA,sha256=NI7xYfGSEG_xUlThoZmPrh7XlPmWH3Kh_gGnOsYlWDg,2409
49
+ screenz-1.10.2.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
50
+ screenz-1.10.2.dist-info/entry_points.txt,sha256=KgpIHu4U2AGBXr8-GyI3qmS8N0VmMauC24ps5_pKtVM,46
51
+ screenz-1.10.2.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.1.2
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ screenz=scrz.__main__:main
3
+
scrz/__init__.py ADDED
File without changes
scrz/__main__.py ADDED
@@ -0,0 +1,506 @@
1
+ import argparse
2
+ import concurrent.futures
3
+ import contextlib
4
+ import dataclasses
5
+ import enum
6
+ import importlib.metadata
7
+ import os
8
+ import pathlib
9
+ import random
10
+ import sys
11
+ import types
12
+ import typing
13
+
14
+ import PIL.Image
15
+
16
+ from .extractor import (
17
+ VIDEOS,
18
+ Order,
19
+ Scale,
20
+ Verbosity,
21
+ extract_directory_paths,
22
+ extract_directory_videos,
23
+ is_extended_order,
24
+ )
25
+ from .font import load as load_font
26
+ from .generator import (
27
+ Configuration,
28
+ Header,
29
+ compose,
30
+ configure_histogram,
31
+ configure_image_collage,
32
+ configure_video,
33
+ configure_video_collage,
34
+ generate_histogram,
35
+ generate_image_collage,
36
+ generate_video,
37
+ generate_video_collage,
38
+ )
39
+ from .layout import Layout
40
+
41
+
42
+ class Type(enum.Enum):
43
+ SINGLE = enum.auto()
44
+ RECURSIVE = enum.auto()
45
+ VIDEOS = enum.auto()
46
+ IMAGES = enum.auto()
47
+ HISTOGRAM = enum.auto()
48
+
49
+
50
+ @dataclasses.dataclass(eq=False, kw_only=True)
51
+ class Context(contextlib.AbstractContextManager['Context']):
52
+ executor: typing.Optional[concurrent.futures.Executor]
53
+ randomizer: random.Random
54
+ type: Type
55
+ input: pathlib.Path
56
+ output: pathlib.Path
57
+ follow_symlinks: bool
58
+ ignore_patterns: list[str]
59
+ verbosity: typing.Optional[Verbosity]
60
+ order: Order
61
+ window: typing.Optional[slice]
62
+ filename_format: str
63
+ configuration: Configuration
64
+ debug: bool
65
+
66
+ def __enter__(self) -> 'Context':
67
+ return self
68
+
69
+ def __exit__(
70
+ self,
71
+ exc_type: typing.Optional[typing.Type[BaseException]],
72
+ exc_value: typing.Optional[BaseException],
73
+ traceback: typing.Optional[types.TracebackType],
74
+ ) -> typing.Optional[bool]:
75
+ if self.executor is not None:
76
+ self.executor.shutdown()
77
+ return None
78
+
79
+
80
+ def parse_slice(input: str) -> typing.Optional[slice]:
81
+ return slice(*(int(part) if part else None for part in input.split(':')))
82
+
83
+
84
+ def create_parser() -> argparse.ArgumentParser:
85
+ parser = argparse.ArgumentParser(description='Generates screenshots from videos and images')
86
+ parser.add_argument('input')
87
+ parser.add_argument('output')
88
+
89
+ parser.add_argument(
90
+ '-V', '--version', action='version', version=importlib.metadata.version('screenz')
91
+ )
92
+
93
+ parser.add_argument(
94
+ '-t',
95
+ '--type',
96
+ type=str,
97
+ choices=list(map(str.lower, Type.__members__.keys())),
98
+ help='generation type',
99
+ )
100
+ parser.add_argument(
101
+ '-L',
102
+ '--follow',
103
+ default=False,
104
+ action=argparse.BooleanOptionalAction,
105
+ help='follow symbolic links when recursing',
106
+ )
107
+ parser.add_argument(
108
+ '-i',
109
+ '--ignore',
110
+ type=str,
111
+ nargs='*',
112
+ help='ignore directory or file names matching this pattern when recursing',
113
+ )
114
+ parser.add_argument(
115
+ '-v',
116
+ '--verbosity',
117
+ type=str,
118
+ choices=list(map(str.lower, Verbosity.__members__.keys())),
119
+ help='ffmpeg output verbosity',
120
+ )
121
+ parser.add_argument(
122
+ '-b',
123
+ '--background',
124
+ type=str,
125
+ help='color or image of the background (use image:PATH to specify image)',
126
+ )
127
+ parser.add_argument('-f', '--foreground', type=str, help='color of the foreground and borders')
128
+ parser.add_argument(
129
+ '-a', '--accent', type=str, help='color of the timestamps and histogram bars'
130
+ )
131
+ parser.add_argument('-F', '--font', type=str, help='font name')
132
+ parser.add_argument('-z', '--font-size', type=int, help='font height')
133
+ parser.add_argument(
134
+ '-H',
135
+ '--header',
136
+ type=str,
137
+ choices=list(map(str.lower, Header.__members__.keys())),
138
+ help='level of information in the header',
139
+ )
140
+ parser.add_argument('-s', '--size', type=int, help='size of the cells')
141
+ parser.add_argument(
142
+ '-S',
143
+ '--scale',
144
+ type=str,
145
+ choices=list(map(str.lower, Scale.__members__.keys())),
146
+ help='how to compute the size of the cells',
147
+ )
148
+ parser.add_argument(
149
+ '-l',
150
+ '--layout',
151
+ type=str,
152
+ choices=list(map(str.lower, Layout.__members__.keys())),
153
+ help='cell layout mode',
154
+ )
155
+ parser.add_argument('-C', '--columns', type=int, help='number of columns')
156
+ parser.add_argument('-c', '--count', type=int, help='number of cells')
157
+ parser.add_argument('-p', '--padding', type=int, help='padding between cells')
158
+ parser.add_argument(
159
+ '-o',
160
+ '--order',
161
+ type=str,
162
+ choices=list(map(str.lower, Order.__members__.keys())),
163
+ help='order of files when recursing',
164
+ )
165
+ parser.add_argument('-Z', '--seed', type=int, help='seed for random order')
166
+ parser.add_argument('-w', '--window', type=parse_slice, help='create a window over the results')
167
+ parser.add_argument('-n', '--filename', type=str, help='default image filename format')
168
+ parser.add_argument('-T', '--threads', type=int, help='number of threads')
169
+ parser.add_argument(
170
+ '-d',
171
+ '--debug',
172
+ default=False,
173
+ action=argparse.BooleanOptionalAction,
174
+ help='show layout item bounds',
175
+ )
176
+ return parser
177
+
178
+
179
+ def configure() -> Context:
180
+ parser = create_parser()
181
+ options = parser.parse_args()
182
+
183
+ type = Type.SINGLE if options.type is None else Type.__members__[options.type.upper()]
184
+ input = os.path.normpath(os.path.abspath(options.input))
185
+ output = os.path.normpath(os.path.abspath(options.output))
186
+
187
+ if type == Type.SINGLE or type == Type.RECURSIVE:
188
+ configuration = configure_video()
189
+ elif type == Type.VIDEOS:
190
+ configuration = configure_video_collage()
191
+ elif type == Type.IMAGES:
192
+ configuration = configure_image_collage()
193
+ elif type == Type.HISTOGRAM:
194
+ configuration = configure_histogram()
195
+ else:
196
+ raise ValueError('unknown type')
197
+
198
+ follow_symlinks: bool = options.follow
199
+ ignore_patterns: list[str] = [] if options.ignore is None else options.ignore
200
+
201
+ verbosity: typing.Optional[Verbosity] = None
202
+ if options.verbosity is not None:
203
+ verbosity = Verbosity.__members__[options.verbosity.upper()]
204
+
205
+ if options.background is not None:
206
+ if options.background.startswith('image:'):
207
+ configuration.background = PIL.Image.open(options.background[6:])
208
+ configuration.background.load()
209
+ else:
210
+ configuration.background = options.background
211
+
212
+ if options.foreground is not None:
213
+ configuration.foreground = options.foreground
214
+
215
+ if options.accent is not None:
216
+ configuration.accent = options.accent
217
+
218
+ if options.font is not None:
219
+ configuration.font2 = configuration.font1 = configuration.font0 = (
220
+ load_font(options.font)
221
+ if options.font_size is None
222
+ else load_font(options.font, options.font_size)
223
+ )
224
+
225
+ if options.header is not None:
226
+ configuration.header = Header.__members__[options.header.upper()]
227
+
228
+ if options.size is not None:
229
+ configuration.size = options.size
230
+
231
+ if options.scale is not None:
232
+ configuration.scale = Scale.__members__[options.scale.upper()]
233
+
234
+ if options.layout is not None:
235
+ configuration.layout = Layout.__members__[options.layout.upper()]
236
+
237
+ if options.columns is not None:
238
+ configuration.columns = options.columns
239
+
240
+ if options.count is not None:
241
+ configuration.count = options.count
242
+
243
+ if options.padding is not None:
244
+ configuration.padding = (options.padding, options.padding)
245
+
246
+ order = Order.DEFAULT
247
+ if options.order is not None:
248
+ order = Order.__members__[options.order.upper()]
249
+
250
+ randomizer: random.Random
251
+ if options.seed is not None:
252
+ randomizer = random.Random(options.seed)
253
+ else:
254
+ randomizer = random.SystemRandom()
255
+
256
+ window = options.window
257
+
258
+ if type == Type.SINGLE:
259
+ filename_format = '{filename}.jpg'
260
+ elif type == Type.RECURSIVE:
261
+ filename_format = os.path.join('{directory}', '{filename}.jpg')
262
+ elif type == Type.VIDEOS:
263
+ filename_format = '{name}_{index:02d}.jpg'
264
+ elif type == Type.IMAGES:
265
+ filename_format = '{name}_{index:02d}.jpg'
266
+ elif type == Type.HISTOGRAM:
267
+ filename_format = '{filename}.jpg'
268
+
269
+ if options.filename is not None:
270
+ filename_format = options.filename
271
+
272
+ threads: typing.Optional[int] = options.threads
273
+ if threads is None:
274
+ if sys.platform == 'linux':
275
+ threads = len(os.sched_getaffinity(0))
276
+ else:
277
+ threads = os.cpu_count() or 0
278
+
279
+ threads = min(32, threads)
280
+
281
+ debug: bool = options.debug
282
+
283
+ return Context(
284
+ executor=concurrent.futures.ThreadPoolExecutor(threads) if threads > 1 else None,
285
+ randomizer=randomizer,
286
+ type=type,
287
+ input=pathlib.Path(input),
288
+ output=pathlib.Path(output),
289
+ follow_symlinks=follow_symlinks,
290
+ ignore_patterns=ignore_patterns,
291
+ verbosity=verbosity,
292
+ order=order,
293
+ window=window,
294
+ filename_format=filename_format,
295
+ configuration=configuration,
296
+ debug=debug,
297
+ )
298
+
299
+
300
+ def make_output(outputs: set[pathlib.Path], directory: pathlib.Path, filename: str) -> pathlib.Path:
301
+ index = 0
302
+ split: typing.Optional[tuple[str, str]] = None
303
+ output = directory / filename
304
+
305
+ while output in outputs:
306
+ index += 1
307
+ if split is None:
308
+ split = os.path.splitext(filename)
309
+ output = directory / '{}[{}]{}'.format(split[0], index, split[1])
310
+
311
+ outputs.add(output)
312
+ return output
313
+
314
+
315
+ def execute_video(context: Context) -> None:
316
+ vignette = generate_video(
317
+ context.configuration, context.input, verbosity=context.verbosity, executor=context.executor
318
+ )
319
+
320
+ image = compose(context.configuration, vignette, debug=context.debug)
321
+ image.save(context.output)
322
+
323
+
324
+ def execute_recursive(context: Context) -> None:
325
+ if is_extended_order(context.order):
326
+ paths = [
327
+ path
328
+ for path, _, _ in extract_directory_videos(
329
+ context.input,
330
+ follow_symlinks=context.follow_symlinks,
331
+ ignore_patterns=context.ignore_patterns,
332
+ recurse=True,
333
+ order=context.order,
334
+ window=context.window,
335
+ offset=None,
336
+ verbosity=context.verbosity,
337
+ executor=context.executor,
338
+ randomizer=context.randomizer,
339
+ )
340
+ ]
341
+
342
+ else:
343
+ paths = extract_directory_paths(
344
+ context.input,
345
+ VIDEOS,
346
+ follow_symlinks=context.follow_symlinks,
347
+ ignore_patterns=context.ignore_patterns,
348
+ recurse=True,
349
+ order=context.order,
350
+ window=context.window,
351
+ randomizer=context.randomizer,
352
+ )
353
+
354
+ outputs: set[pathlib.Path] = set()
355
+ for index, input in enumerate(paths):
356
+ relative_input = input.relative_to(context.input)
357
+ print(os.fspath(relative_input))
358
+
359
+ values: dict[str, object] = {
360
+ 'directory': os.fspath(relative_input.parent),
361
+ 'filename': input.stem,
362
+ 'name': input.name,
363
+ 'index': index,
364
+ 'ordinal': index + 1,
365
+ 'count': len(paths),
366
+ }
367
+
368
+ output = make_output(outputs, context.output, context.filename_format.format(**values))
369
+ os.makedirs(output.parent, exist_ok=True)
370
+
371
+ sub_context = Context(
372
+ executor=context.executor,
373
+ randomizer=context.randomizer,
374
+ type=Type.SINGLE,
375
+ input=input,
376
+ output=output,
377
+ follow_symlinks=context.follow_symlinks,
378
+ ignore_patterns=context.ignore_patterns,
379
+ verbosity=context.verbosity,
380
+ order=context.order,
381
+ window=None,
382
+ filename_format=os.path.basename(context.filename_format),
383
+ configuration=context.configuration,
384
+ debug=context.debug,
385
+ )
386
+
387
+ execute_video(sub_context)
388
+
389
+
390
+ def execute_video_collage(context: Context) -> None:
391
+ width: typing.Optional[int] = None
392
+
393
+ def receiver(value: typing.Optional[int]) -> None:
394
+ nonlocal width
395
+ width = value
396
+
397
+ vignettes = list(
398
+ generate_video_collage(
399
+ context.configuration,
400
+ context.input,
401
+ follow_symlinks=context.follow_symlinks,
402
+ ignore_patterns=context.ignore_patterns,
403
+ order=context.order,
404
+ window=context.window,
405
+ receiver=receiver,
406
+ verbosity=context.verbosity,
407
+ executor=context.executor,
408
+ randomizer=context.randomizer,
409
+ )
410
+ )
411
+
412
+ outputs: set[pathlib.Path] = set()
413
+ for index, vignette in enumerate(vignettes):
414
+ values: dict[str, object] = {
415
+ 'directory': '',
416
+ 'filename': '',
417
+ 'name': context.input.name,
418
+ 'index': index,
419
+ 'ordinal': index + 1,
420
+ 'count': len(vignettes),
421
+ }
422
+
423
+ output = make_output(outputs, context.output, context.filename_format.format(**values))
424
+ os.makedirs(output.parent, exist_ok=True)
425
+
426
+ image = compose(context.configuration, vignette, width=width, debug=context.debug)
427
+ image.save(output)
428
+
429
+ del image
430
+
431
+
432
+ def execute_image_collage(context: Context) -> None:
433
+ width: typing.Optional[int] = None
434
+
435
+ def receiver(value: typing.Optional[int]) -> None:
436
+ nonlocal width
437
+ width = value
438
+
439
+ vignettes = list(
440
+ generate_image_collage(
441
+ context.configuration,
442
+ context.input,
443
+ follow_symlinks=context.follow_symlinks,
444
+ ignore_patterns=context.ignore_patterns,
445
+ order=context.order,
446
+ window=context.window,
447
+ receiver=receiver,
448
+ verbosity=context.verbosity,
449
+ executor=context.executor,
450
+ randomizer=context.randomizer,
451
+ )
452
+ )
453
+
454
+ outputs: set[pathlib.Path] = set()
455
+ for index, vignette in enumerate(vignettes):
456
+ values: dict[str, object] = {
457
+ 'directory': '',
458
+ 'filename': '',
459
+ 'name': context.input.name,
460
+ 'index': index,
461
+ 'ordinal': index + 1,
462
+ 'count': len(vignettes),
463
+ }
464
+
465
+ output = make_output(outputs, context.output, context.filename_format.format(**values))
466
+ os.makedirs(output.parent, exist_ok=True)
467
+
468
+ image = compose(context.configuration, vignette, width=width, debug=context.debug)
469
+ image.save(output)
470
+
471
+ del image
472
+
473
+
474
+ def execute_histogram(context: Context) -> None:
475
+ vignette = generate_histogram(
476
+ context.configuration,
477
+ context.input,
478
+ follow_symlinks=context.follow_symlinks,
479
+ ignore_patterns=context.ignore_patterns,
480
+ order=context.order,
481
+ window=context.window,
482
+ verbosity=context.verbosity,
483
+ executor=context.executor,
484
+ randomizer=context.randomizer,
485
+ )
486
+
487
+ image = compose(context.configuration, vignette, debug=context.debug)
488
+ image.save(context.output)
489
+
490
+
491
+ def main() -> None:
492
+ with configure() as context:
493
+ if context.type == Type.SINGLE:
494
+ execute_video(context)
495
+ elif context.type == Type.RECURSIVE:
496
+ execute_recursive(context)
497
+ elif context.type == Type.VIDEOS:
498
+ execute_video_collage(context)
499
+ elif context.type == Type.IMAGES:
500
+ execute_image_collage(context)
501
+ elif context.type == Type.HISTOGRAM:
502
+ execute_histogram(context)
503
+
504
+
505
+ if __name__ == '__main__':
506
+ main()