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.
- screenz-1.10.2/PKG-INFO +74 -0
- screenz-1.10.2/README.md +57 -0
- screenz-1.10.2/pyproject.toml +52 -0
- screenz-1.10.2/scrz/__init__.py +0 -0
- screenz-1.10.2/scrz/__main__.py +506 -0
- screenz-1.10.2/scrz/extractor.py +472 -0
- screenz-1.10.2/scrz/font/10x18b.pil +0 -0
- screenz-1.10.2/scrz/font/10x18b.png +0 -0
- screenz-1.10.2/scrz/font/10x18n.pil +0 -0
- screenz-1.10.2/scrz/font/10x18n.png +0 -0
- screenz-1.10.2/scrz/font/10x20b.pil +0 -0
- screenz-1.10.2/scrz/font/10x20b.png +0 -0
- screenz-1.10.2/scrz/font/10x20n.pil +0 -0
- screenz-1.10.2/scrz/font/10x20n.png +0 -0
- screenz-1.10.2/scrz/font/11x22b.pil +0 -0
- screenz-1.10.2/scrz/font/11x22b.png +0 -0
- screenz-1.10.2/scrz/font/11x22n.pil +0 -0
- screenz-1.10.2/scrz/font/11x22n.png +0 -0
- screenz-1.10.2/scrz/font/12x24b.pil +0 -0
- screenz-1.10.2/scrz/font/12x24b.png +0 -0
- screenz-1.10.2/scrz/font/12x24n.pil +0 -0
- screenz-1.10.2/scrz/font/12x24n.png +0 -0
- screenz-1.10.2/scrz/font/14x28b.pil +0 -0
- screenz-1.10.2/scrz/font/14x28b.png +0 -0
- screenz-1.10.2/scrz/font/14x28n.pil +0 -0
- screenz-1.10.2/scrz/font/14x28n.png +0 -0
- screenz-1.10.2/scrz/font/16x32b.pil +0 -0
- screenz-1.10.2/scrz/font/16x32b.png +0 -0
- screenz-1.10.2/scrz/font/16x32n.pil +0 -0
- screenz-1.10.2/scrz/font/16x32n.png +0 -0
- screenz-1.10.2/scrz/font/6x12b.pil +0 -0
- screenz-1.10.2/scrz/font/6x12b.png +0 -0
- screenz-1.10.2/scrz/font/6x12n.pil +0 -0
- screenz-1.10.2/scrz/font/6x12n.png +0 -0
- screenz-1.10.2/scrz/font/8x14b.pil +0 -0
- screenz-1.10.2/scrz/font/8x14b.png +0 -0
- screenz-1.10.2/scrz/font/8x14n.pil +0 -0
- screenz-1.10.2/scrz/font/8x14n.png +0 -0
- screenz-1.10.2/scrz/font/8x16b.pil +0 -0
- screenz-1.10.2/scrz/font/8x16b.png +0 -0
- screenz-1.10.2/scrz/font/8x16n.pil +0 -0
- screenz-1.10.2/scrz/font/8x16n.png +0 -0
- screenz-1.10.2/scrz/font/__init__.py +44 -0
- screenz-1.10.2/scrz/generator.py +627 -0
- screenz-1.10.2/scrz/histogram.py +117 -0
- screenz-1.10.2/scrz/layout.py +516 -0
- screenz-1.10.2/scrz/py.typed +0 -0
- screenz-1.10.2/scrz/renderer.py +125 -0
- screenz-1.10.2/scrz/types.py +6 -0
- screenz-1.10.2/scrz/vignette.py +38 -0
screenz-1.10.2/PKG-INFO
ADDED
|
@@ -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
|
+
|
screenz-1.10.2/README.md
ADDED
|
@@ -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()
|