screenz 1.10.2__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.
Files changed (50) hide show
  1. screenz-1.10.2/PKG-INFO +74 -0
  2. screenz-1.10.2/README.md +57 -0
  3. screenz-1.10.2/pyproject.toml +52 -0
  4. screenz-1.10.2/scrz/__init__.py +0 -0
  5. screenz-1.10.2/scrz/__main__.py +506 -0
  6. screenz-1.10.2/scrz/extractor.py +472 -0
  7. screenz-1.10.2/scrz/font/10x18b.pil +0 -0
  8. screenz-1.10.2/scrz/font/10x18b.png +0 -0
  9. screenz-1.10.2/scrz/font/10x18n.pil +0 -0
  10. screenz-1.10.2/scrz/font/10x18n.png +0 -0
  11. screenz-1.10.2/scrz/font/10x20b.pil +0 -0
  12. screenz-1.10.2/scrz/font/10x20b.png +0 -0
  13. screenz-1.10.2/scrz/font/10x20n.pil +0 -0
  14. screenz-1.10.2/scrz/font/10x20n.png +0 -0
  15. screenz-1.10.2/scrz/font/11x22b.pil +0 -0
  16. screenz-1.10.2/scrz/font/11x22b.png +0 -0
  17. screenz-1.10.2/scrz/font/11x22n.pil +0 -0
  18. screenz-1.10.2/scrz/font/11x22n.png +0 -0
  19. screenz-1.10.2/scrz/font/12x24b.pil +0 -0
  20. screenz-1.10.2/scrz/font/12x24b.png +0 -0
  21. screenz-1.10.2/scrz/font/12x24n.pil +0 -0
  22. screenz-1.10.2/scrz/font/12x24n.png +0 -0
  23. screenz-1.10.2/scrz/font/14x28b.pil +0 -0
  24. screenz-1.10.2/scrz/font/14x28b.png +0 -0
  25. screenz-1.10.2/scrz/font/14x28n.pil +0 -0
  26. screenz-1.10.2/scrz/font/14x28n.png +0 -0
  27. screenz-1.10.2/scrz/font/16x32b.pil +0 -0
  28. screenz-1.10.2/scrz/font/16x32b.png +0 -0
  29. screenz-1.10.2/scrz/font/16x32n.pil +0 -0
  30. screenz-1.10.2/scrz/font/16x32n.png +0 -0
  31. screenz-1.10.2/scrz/font/6x12b.pil +0 -0
  32. screenz-1.10.2/scrz/font/6x12b.png +0 -0
  33. screenz-1.10.2/scrz/font/6x12n.pil +0 -0
  34. screenz-1.10.2/scrz/font/6x12n.png +0 -0
  35. screenz-1.10.2/scrz/font/8x14b.pil +0 -0
  36. screenz-1.10.2/scrz/font/8x14b.png +0 -0
  37. screenz-1.10.2/scrz/font/8x14n.pil +0 -0
  38. screenz-1.10.2/scrz/font/8x14n.png +0 -0
  39. screenz-1.10.2/scrz/font/8x16b.pil +0 -0
  40. screenz-1.10.2/scrz/font/8x16b.png +0 -0
  41. screenz-1.10.2/scrz/font/8x16n.pil +0 -0
  42. screenz-1.10.2/scrz/font/8x16n.png +0 -0
  43. screenz-1.10.2/scrz/font/__init__.py +44 -0
  44. screenz-1.10.2/scrz/generator.py +627 -0
  45. screenz-1.10.2/scrz/histogram.py +117 -0
  46. screenz-1.10.2/scrz/layout.py +516 -0
  47. screenz-1.10.2/scrz/py.typed +0 -0
  48. screenz-1.10.2/scrz/renderer.py +125 -0
  49. screenz-1.10.2/scrz/types.py +6 -0
  50. screenz-1.10.2/scrz/vignette.py +38 -0
@@ -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,57 @@
1
+ # ScreenZ
2
+
3
+ ## Installation
4
+
5
+ Requires Python 3.13 and [FFmpeg](https://ffmpeg.org/download.html).
6
+
7
+ Install using `pip install --user screenz`.
8
+
9
+ Make sure `PATH` is properly set. In particular, `ffmpeg` and `ffprobe` must be in the `PATH` for the tool to function.
10
+
11
+ ## Usage
12
+
13
+ Generate screens for a single video:
14
+
15
+ ```
16
+ screenz video.mp4 output.jpg
17
+ ```
18
+
19
+ Generate screens for all videos:
20
+
21
+ ```
22
+ screenz --type recursive Videos Screens
23
+ ```
24
+
25
+ Generate a collage of all videos:
26
+
27
+ ```
28
+ screenz --type videos Videos Collage
29
+ ```
30
+
31
+ Generate a collage from all images:
32
+
33
+ ```
34
+ screenz --type images Videos Collage
35
+ ```
36
+
37
+ Generate an histogram of video length distributions:
38
+
39
+ ```
40
+ screenz --type histogram Videos histogram.png
41
+ ```
42
+
43
+ ## Advanced
44
+
45
+ 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.
46
+
47
+ 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.
48
+
49
+ 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.
50
+
51
+ The built-in fonts are: `6x12b`, `6x12n`, `8x14b`, `8x14n`, `8x16b`, `8x16n`, `10x18b`, `10x18n`, `10x20b`, `10x20n`, `11x22b`, `11x22n`, `12x24b`, `12x24n`, `14x28b`, `14x28n`, `16x32b` and `16x32n`.
52
+
53
+ 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.
54
+
55
+ ## Reference
56
+
57
+ Use `screenz --help` to see the full documentation on options.
@@ -0,0 +1,52 @@
1
+ [tool.poetry]
2
+ name = "screenz"
3
+ version = "1.10.2"
4
+ description = "ScreenZ"
5
+ license = "MIT"
6
+ authors = ["deadmaster <deadmaster@noreply.codeberg.org>"]
7
+ readme = "README.md"
8
+ repository = "https://codeberg.org/deadmaster/screenz"
9
+ classifiers = ["Topic :: Multimedia :: Video"]
10
+ packages = [{ include = "scrz" }]
11
+ exclude = ["scrz/font/generate.sh"]
12
+
13
+ [tool.poetry.dependencies]
14
+ python = "^3.13"
15
+ Pillow = "^11.2.1"
16
+
17
+ [tool.poetry.group.dev.dependencies]
18
+ mypy = "^1.15.0"
19
+ ruff = "^0.11.8"
20
+ types-Pillow = "^10.2.0"
21
+
22
+ [tool.poetry.scripts]
23
+ screenz = "scrz.__main__:main"
24
+
25
+ [tool.ruff]
26
+ include = ["scrz/**/*.py"]
27
+ target-version = "py313"
28
+ line-length = 100
29
+ indent-width = 4
30
+
31
+ [tool.ruff.lint]
32
+ select = ["F", "I", "UP", "G"]
33
+ ignore = ["UP006", "UP007", "UP015", "UP032", "UP038", "UP040"]
34
+
35
+ [tool.ruff.lint.isort]
36
+ split-on-trailing-comma = false
37
+
38
+ [tool.ruff.format]
39
+ quote-style = "single"
40
+ indent-style = "tab"
41
+ skip-magic-trailing-comma = true
42
+
43
+ [tool.mypy]
44
+ python_version = "3.13"
45
+ show_error_codes = true
46
+ show_absolute_path = true
47
+ files = ["scrz"]
48
+ strict = true
49
+
50
+ [build-system]
51
+ requires = ["poetry-core"]
52
+ build-backend = "poetry.core.masonry.api"
File without changes
@@ -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()