fotolab 0.22.0__tar.gz → 0.25.1__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.
- {fotolab-0.22.0 → fotolab-0.25.1}/.pre-commit-config.yaml +1 -1
- fotolab-0.25.1/.python-version +5 -0
- {fotolab-0.22.0 → fotolab-0.25.1}/CHANGELOG.md +33 -0
- {fotolab-0.22.0 → fotolab-0.25.1}/PKG-INFO +17 -11
- {fotolab-0.22.0 → fotolab-0.25.1}/Pipfile.lock +72 -76
- {fotolab-0.22.0 → fotolab-0.25.1}/README.md +16 -10
- {fotolab-0.22.0 → fotolab-0.25.1}/fotolab/__init__.py +2 -2
- {fotolab-0.22.0 → fotolab-0.25.1}/fotolab/cli.py +27 -43
- fotolab-0.25.1/fotolab/subcommands/__init__.py +32 -0
- {fotolab-0.22.0/fotolab → fotolab-0.25.1/fotolab/subcommands}/animate.py +1 -1
- {fotolab-0.22.0/fotolab → fotolab-0.25.1/fotolab/subcommands}/auto.py +10 -9
- {fotolab-0.22.0/fotolab → fotolab-0.25.1/fotolab/subcommands}/border.py +20 -17
- {fotolab-0.22.0/fotolab → fotolab-0.25.1/fotolab/subcommands}/contrast.py +1 -1
- {fotolab-0.22.0/fotolab → fotolab-0.25.1/fotolab/subcommands}/env.py +4 -4
- {fotolab-0.22.0/fotolab → fotolab-0.25.1/fotolab/subcommands}/info.py +17 -10
- {fotolab-0.22.0/fotolab → fotolab-0.25.1/fotolab/subcommands}/montage.py +4 -4
- {fotolab-0.22.0/fotolab → fotolab-0.25.1/fotolab/subcommands}/resize.py +1 -1
- {fotolab-0.22.0/fotolab → fotolab-0.25.1/fotolab/subcommands}/rotate.py +21 -2
- {fotolab-0.22.0/fotolab → fotolab-0.25.1/fotolab/subcommands}/sharpen.py +40 -3
- {fotolab-0.22.0/fotolab → fotolab-0.25.1/fotolab/subcommands}/watermark.py +79 -25
- fotolab-0.22.0/.python-version +0 -5
- {fotolab-0.22.0 → fotolab-0.25.1}/.coveragerc +0 -0
- {fotolab-0.22.0 → fotolab-0.25.1}/.gitignore +0 -0
- {fotolab-0.22.0 → fotolab-0.25.1}/CONTRIBUTING.md +0 -0
- {fotolab-0.22.0 → fotolab-0.25.1}/LICENSE.md +0 -0
- {fotolab-0.22.0 → fotolab-0.25.1}/Pipfile +0 -0
- {fotolab-0.22.0 → fotolab-0.25.1}/docs/Makefile +0 -0
- {fotolab-0.22.0 → fotolab-0.25.1}/docs/make.bat +0 -0
- {fotolab-0.22.0 → fotolab-0.25.1}/docs/source/CHANGELOG.md +0 -0
- {fotolab-0.22.0 → fotolab-0.25.1}/docs/source/CONTRIBUTING.md +0 -0
- {fotolab-0.22.0 → fotolab-0.25.1}/docs/source/LICENSE.md +0 -0
- {fotolab-0.22.0 → fotolab-0.25.1}/docs/source/README.md +0 -0
- {fotolab-0.22.0 → fotolab-0.25.1}/docs/source/_static/logo.jpg +0 -0
- {fotolab-0.22.0 → fotolab-0.25.1}/docs/source/conf.py +0 -0
- {fotolab-0.22.0 → fotolab-0.25.1}/docs/source/index.rst +0 -0
- {fotolab-0.22.0 → fotolab-0.25.1}/fotolab/__main__.py +0 -0
- {fotolab-0.22.0 → fotolab-0.25.1}/noxfile.py +0 -0
- {fotolab-0.22.0 → fotolab-0.25.1}/pyproject.toml +0 -0
- {fotolab-0.22.0 → fotolab-0.25.1}/tests/__init__.py +0 -0
- {fotolab-0.22.0 → fotolab-0.25.1}/tests/conftest.py +0 -0
- {fotolab-0.22.0 → fotolab-0.25.1}/tests/test_env.py +0 -0
- {fotolab-0.22.0 → fotolab-0.25.1}/tests/test_help_flag.py +0 -0
- {fotolab-0.22.0 → fotolab-0.25.1}/tests/test_quiet_flag.py +0 -0
@@ -7,6 +7,39 @@ and this project adheres to [0-based versioning](https://0ver.org/).
|
|
7
7
|
|
8
8
|
## [Unreleased]
|
9
9
|
|
10
|
+
## v0.25.1 (2025-01-13)
|
11
|
+
|
12
|
+
- Bump deps
|
13
|
+
- Fix incorrect subcommand when saving image with border
|
14
|
+
- Refactor calculate border
|
15
|
+
|
16
|
+
## v0.25.0 (2025-01-05)
|
17
|
+
|
18
|
+
- Add `--before-after` or `-ba` flag to `sharpen` command
|
19
|
+
- Bump `pre-commit` hook and deps
|
20
|
+
- Bump copyright years
|
21
|
+
|
22
|
+
## v0.24.0 (2024-12-29)
|
23
|
+
|
24
|
+
- Bump `pre-commit` hook
|
25
|
+
- Pass image instead of filename when process `info` and `watermark` subcommand
|
26
|
+
- Refactor print result of `env` subcommand
|
27
|
+
- Support watermarking `gif` image
|
28
|
+
|
29
|
+
## v0.23.0 (2024-12-22)
|
30
|
+
|
31
|
+
- Add `-cw` or `--clockwise` option to `rotate` subcommand
|
32
|
+
- Add `-r` or `--rotation` option to `rotate` subcommand
|
33
|
+
- Refactor calculation of position of watermark
|
34
|
+
- Refactor setup logging again
|
35
|
+
- Use major Python versions for `pyenv`
|
36
|
+
|
37
|
+
## v0.22.1 (2024-12-15)
|
38
|
+
|
39
|
+
- Fix show all EXIF tags instead of selected fields
|
40
|
+
- Refactor building subparser for each subcommand
|
41
|
+
- Refactor setup logging
|
42
|
+
|
10
43
|
## v0.22.0 (2024-12-08)
|
11
44
|
|
12
45
|
- Add `--datetime` to `info` subcommand
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: fotolab
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.25.1
|
4
4
|
Summary: A console program that manipulate images.
|
5
5
|
Keywords: photography,photo
|
6
6
|
Author-email: Kian-Meng Ang <kianmeng@cpan.org>
|
@@ -61,7 +61,7 @@ fotolab -h
|
|
61
61
|
|
62
62
|
```console
|
63
63
|
usage: fotolab [-h] [-o] [-op] [-od OUTPUT_DIR] [-q] [-v] [-d] [-V]
|
64
|
-
{animate,auto,border,contrast,info,resize,rotate,
|
64
|
+
{animate,auto,border,contrast,env,info,montage,resize,rotate,sharpen,watermark} ...
|
65
65
|
|
66
66
|
A console program to manipulate photos.
|
67
67
|
|
@@ -70,19 +70,19 @@ changelog: https://github.com/kianmeng/fotolab/blob/master/CHANGELOG.md
|
|
70
70
|
issues: https://github.com/kianmeng/fotolab/issues
|
71
71
|
|
72
72
|
positional arguments:
|
73
|
-
{animate,auto,border,contrast,info,resize,rotate,
|
73
|
+
{animate,auto,border,contrast,env,info,montage,resize,rotate,sharpen,watermark}
|
74
74
|
sub-command help
|
75
75
|
animate animate an image
|
76
76
|
auto auto adjust (resize, contrast, and watermark) a photo
|
77
77
|
border add border to image
|
78
78
|
contrast contrast an image
|
79
|
+
env print environment information for bug reporting
|
79
80
|
info info an image
|
81
|
+
montage montage a list of image
|
80
82
|
resize resize an image
|
81
83
|
rotate rotate an image
|
82
|
-
montage montage a list of image
|
83
84
|
sharpen sharpen an image
|
84
85
|
watermark watermark an image
|
85
|
-
env print environment information for bug reporting
|
86
86
|
|
87
87
|
options:
|
88
88
|
-h, --help show this help message and exit
|
@@ -206,7 +206,7 @@ fotolab info -h
|
|
206
206
|
<!--help-info !-->
|
207
207
|
|
208
208
|
```console
|
209
|
-
usage: fotolab info [-h] [-s] [--camera] IMAGE_FILENAME
|
209
|
+
usage: fotolab info [-h] [-s] [--camera] [--datetime] IMAGE_FILENAME
|
210
210
|
|
211
211
|
positional arguments:
|
212
212
|
IMAGE_FILENAME set the image filename
|
@@ -215,6 +215,7 @@ options:
|
|
215
215
|
-h, --help show this help message and exit
|
216
216
|
-s, --sort show image info by sorted field name
|
217
217
|
--camera show the camera maker details
|
218
|
+
--datetime show the datetime
|
218
219
|
```
|
219
220
|
|
220
221
|
<!--help-info !-->
|
@@ -228,13 +229,17 @@ fotolab rotate -h
|
|
228
229
|
<!--help-rotate !-->
|
229
230
|
|
230
231
|
```console
|
231
|
-
usage: fotolab rotate [-h]
|
232
|
+
usage: fotolab rotate [-h] [-r ROTATION] [-cw]
|
233
|
+
IMAGE_FILENAMES [IMAGE_FILENAMES ...]
|
232
234
|
|
233
235
|
positional arguments:
|
234
|
-
IMAGE_FILENAMES
|
236
|
+
IMAGE_FILENAMES set the image filenames
|
235
237
|
|
236
238
|
options:
|
237
|
-
-h, --help
|
239
|
+
-h, --help show this help message and exit
|
240
|
+
-r, --rotation ROTATION
|
241
|
+
Rotation angle in degrees (default: '0')
|
242
|
+
-cw, --clockwise Rotate clockwise (default: 'False)
|
238
243
|
```
|
239
244
|
|
240
245
|
<!--help-rotate !-->
|
@@ -295,7 +300,7 @@ fotolab sharpen -h
|
|
295
300
|
<!--help-sharpen !-->
|
296
301
|
|
297
302
|
```console
|
298
|
-
usage: fotolab sharpen [-h] [-r RADIUS] [-p PERCENT] [-t THRESHOLD]
|
303
|
+
usage: fotolab sharpen [-h] [-r RADIUS] [-p PERCENT] [-t THRESHOLD] [-ba]
|
299
304
|
IMAGE_FILENAMES [IMAGE_FILENAMES ...]
|
300
305
|
|
301
306
|
positional arguments:
|
@@ -310,6 +315,7 @@ options:
|
|
310
315
|
-t, --threshold THRESHOLD
|
311
316
|
set the minimum brightness changed to be sharpened
|
312
317
|
(default: '3')
|
318
|
+
-ba, --before-after generate a GIF showing before and after changes
|
313
319
|
```
|
314
320
|
|
315
321
|
<!--help-sharpen !-->
|
@@ -380,7 +386,7 @@ options:
|
|
380
386
|
|
381
387
|
## Copyright and License
|
382
388
|
|
383
|
-
Copyright (C) 2024 Kian-Meng Ang
|
389
|
+
Copyright (C) 2024,2025 Kian-Meng Ang
|
384
390
|
|
385
391
|
This program is free software: you can redistribute it and/or modify it under
|
386
392
|
the terms of the GNU Affero General Public License as published by the Free
|
@@ -22,85 +22,81 @@
|
|
22
22
|
},
|
23
23
|
"pillow": {
|
24
24
|
"hashes": [
|
25
|
-
"sha256:
|
26
|
-
"sha256:
|
27
|
-
"sha256:
|
28
|
-
"sha256:
|
29
|
-
"sha256:
|
30
|
-
"sha256:
|
31
|
-
"sha256:
|
32
|
-
"sha256:
|
33
|
-
"sha256:
|
34
|
-
"sha256:
|
35
|
-
"sha256:
|
36
|
-
"sha256:
|
37
|
-
"sha256:
|
38
|
-
"sha256:
|
39
|
-
"sha256:
|
40
|
-
"sha256:
|
41
|
-
"sha256:
|
42
|
-
"sha256:
|
43
|
-
"sha256:
|
44
|
-
"sha256:
|
45
|
-
"sha256:
|
46
|
-
"sha256:
|
47
|
-
"sha256:
|
48
|
-
"sha256:
|
49
|
-
"sha256:
|
50
|
-
"sha256:
|
51
|
-
"sha256:
|
52
|
-
"sha256:
|
53
|
-
"sha256:
|
54
|
-
"sha256:
|
55
|
-
"sha256:
|
56
|
-
"sha256:
|
57
|
-
"sha256:
|
58
|
-
"sha256:
|
59
|
-
"sha256:
|
60
|
-
"sha256:
|
61
|
-
"sha256:
|
62
|
-
"sha256:
|
63
|
-
"sha256:
|
64
|
-
"sha256:
|
65
|
-
"sha256:
|
66
|
-
"sha256:
|
67
|
-
"sha256:
|
68
|
-
"sha256:
|
69
|
-
"sha256:
|
70
|
-
"sha256:
|
71
|
-
"sha256:
|
72
|
-
"sha256:
|
73
|
-
"sha256:
|
74
|
-
"sha256:
|
75
|
-
"sha256:
|
76
|
-
"sha256:
|
77
|
-
"sha256:
|
78
|
-
"sha256:
|
79
|
-
"sha256:
|
80
|
-
"sha256:
|
81
|
-
"sha256:
|
82
|
-
"sha256:
|
83
|
-
"sha256:
|
84
|
-
"sha256:
|
85
|
-
"sha256:
|
86
|
-
"sha256:
|
87
|
-
"sha256:
|
88
|
-
"sha256:
|
89
|
-
"sha256:
|
90
|
-
"sha256:
|
91
|
-
"sha256:
|
92
|
-
"sha256:
|
93
|
-
"sha256:
|
94
|
-
"sha256:
|
95
|
-
"sha256:
|
96
|
-
"sha256:f1b82c27e89fffc6da125d5eb0ca6e68017faf5efc078128cfaa42cf5cb38798",
|
97
|
-
"sha256:fba162b8872d30fea8c52b258a542c5dfd7b235fb5cb352240c8d63b414013eb",
|
98
|
-
"sha256:fbbcb7b57dc9c794843e3d1258c0fbf0f48656d46ffe9e09b63bbd6e8cd5d0a2",
|
99
|
-
"sha256:fcb4621042ac4b7865c179bb972ed0da0218a076dc1820ffc48b1d74c1e37fe9"
|
25
|
+
"sha256:015c6e863faa4779251436db398ae75051469f7c903b043a48f078e437656f83",
|
26
|
+
"sha256:0a2f91f8a8b367e7a57c6e91cd25af510168091fb89ec5146003e424e1558a96",
|
27
|
+
"sha256:11633d58b6ee5733bde153a8dafd25e505ea3d32e261accd388827ee987baf65",
|
28
|
+
"sha256:2062ffb1d36544d42fcaa277b069c88b01bb7298f4efa06731a7fd6cc290b81a",
|
29
|
+
"sha256:31eba6bbdd27dde97b0174ddf0297d7a9c3a507a8a1480e1e60ef914fe23d352",
|
30
|
+
"sha256:3362c6ca227e65c54bf71a5f88b3d4565ff1bcbc63ae72c34b07bbb1cc59a43f",
|
31
|
+
"sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20",
|
32
|
+
"sha256:36ba10b9cb413e7c7dfa3e189aba252deee0602c86c309799da5a74009ac7a1c",
|
33
|
+
"sha256:3764d53e09cdedd91bee65c2527815d315c6b90d7b8b79759cc48d7bf5d4f114",
|
34
|
+
"sha256:3a5fe20a7b66e8135d7fd617b13272626a28278d0e578c98720d9ba4b2439d49",
|
35
|
+
"sha256:3cdcdb0b896e981678eee140d882b70092dac83ac1cdf6b3a60e2216a73f2b91",
|
36
|
+
"sha256:4637b88343166249fe8aa94e7c4a62a180c4b3898283bb5d3d2fd5fe10d8e4e0",
|
37
|
+
"sha256:4db853948ce4e718f2fc775b75c37ba2efb6aaea41a1a5fc57f0af59eee774b2",
|
38
|
+
"sha256:4dd43a78897793f60766563969442020e90eb7847463eca901e41ba186a7d4a5",
|
39
|
+
"sha256:54251ef02a2309b5eec99d151ebf5c9904b77976c8abdcbce7891ed22df53884",
|
40
|
+
"sha256:54ce1c9a16a9561b6d6d8cb30089ab1e5eb66918cb47d457bd996ef34182922e",
|
41
|
+
"sha256:593c5fd6be85da83656b93ffcccc2312d2d149d251e98588b14fbc288fd8909c",
|
42
|
+
"sha256:5bb94705aea800051a743aa4874bb1397d4695fb0583ba5e425ee0328757f196",
|
43
|
+
"sha256:67cd427c68926108778a9005f2a04adbd5e67c442ed21d95389fe1d595458756",
|
44
|
+
"sha256:70ca5ef3b3b1c4a0812b5c63c57c23b63e53bc38e758b37a951e5bc466449861",
|
45
|
+
"sha256:73ddde795ee9b06257dac5ad42fcb07f3b9b813f8c1f7f870f402f4dc54b5269",
|
46
|
+
"sha256:758e9d4ef15d3560214cddbc97b8ef3ef86ce04d62ddac17ad39ba87e89bd3b1",
|
47
|
+
"sha256:7d33d2fae0e8b170b6a6c57400e077412240f6f5bb2a342cf1ee512a787942bb",
|
48
|
+
"sha256:7fdadc077553621911f27ce206ffcbec7d3f8d7b50e0da39f10997e8e2bb7f6a",
|
49
|
+
"sha256:8000376f139d4d38d6851eb149b321a52bb8893a88dae8ee7d95840431977081",
|
50
|
+
"sha256:837060a8599b8f5d402e97197d4924f05a2e0d68756998345c829c33186217b1",
|
51
|
+
"sha256:89dbdb3e6e9594d512780a5a1c42801879628b38e3efc7038094430844e271d8",
|
52
|
+
"sha256:8c730dc3a83e5ac137fbc92dfcfe1511ce3b2b5d7578315b63dbbb76f7f51d90",
|
53
|
+
"sha256:8e275ee4cb11c262bd108ab2081f750db2a1c0b8c12c1897f27b160c8bd57bbc",
|
54
|
+
"sha256:9044b5e4f7083f209c4e35aa5dd54b1dd5b112b108648f5c902ad586d4f945c5",
|
55
|
+
"sha256:93a18841d09bcdd774dcdc308e4537e1f867b3dec059c131fde0327899734aa1",
|
56
|
+
"sha256:9409c080586d1f683df3f184f20e36fb647f2e0bc3988094d4fd8c9f4eb1b3b3",
|
57
|
+
"sha256:96f82000e12f23e4f29346e42702b6ed9a2f2fea34a740dd5ffffcc8c539eb35",
|
58
|
+
"sha256:9aa9aeddeed452b2f616ff5507459e7bab436916ccb10961c4a382cd3e03f47f",
|
59
|
+
"sha256:9ee85f0696a17dd28fbcfceb59f9510aa71934b483d1f5601d1030c3c8304f3c",
|
60
|
+
"sha256:a07dba04c5e22824816b2615ad7a7484432d7f540e6fa86af60d2de57b0fcee2",
|
61
|
+
"sha256:a3cd561ded2cf2bbae44d4605837221b987c216cff94f49dfeed63488bb228d2",
|
62
|
+
"sha256:a697cd8ba0383bba3d2d3ada02b34ed268cb548b369943cd349007730c92bddf",
|
63
|
+
"sha256:a76da0a31da6fcae4210aa94fd779c65c75786bc9af06289cd1c184451ef7a65",
|
64
|
+
"sha256:a85b653980faad27e88b141348707ceeef8a1186f75ecc600c395dcac19f385b",
|
65
|
+
"sha256:a8d65b38173085f24bc07f8b6c505cbb7418009fa1a1fcb111b1f4961814a442",
|
66
|
+
"sha256:aa8dd43daa836b9a8128dbe7d923423e5ad86f50a7a14dc688194b7be5c0dea2",
|
67
|
+
"sha256:ab8a209b8485d3db694fa97a896d96dd6533d63c22829043fd9de627060beade",
|
68
|
+
"sha256:abc56501c3fd148d60659aae0af6ddc149660469082859fa7b066a298bde9482",
|
69
|
+
"sha256:ad5db5781c774ab9a9b2c4302bbf0c1014960a0a7be63278d13ae6fdf88126fe",
|
70
|
+
"sha256:ae98e14432d458fc3de11a77ccb3ae65ddce70f730e7c76140653048c71bfcbc",
|
71
|
+
"sha256:b20be51b37a75cc54c2c55def3fa2c65bb94ba859dde241cd0a4fd302de5ae0a",
|
72
|
+
"sha256:b523466b1a31d0dcef7c5be1f20b942919b62fd6e9a9be199d035509cbefc0ec",
|
73
|
+
"sha256:b5d658fbd9f0d6eea113aea286b21d3cd4d3fd978157cbf2447a6035916506d3",
|
74
|
+
"sha256:b6123aa4a59d75f06e9dd3dac5bf8bc9aa383121bb3dd9a7a612e05eabc9961a",
|
75
|
+
"sha256:bd165131fd51697e22421d0e467997ad31621b74bfc0b75956608cb2906dda07",
|
76
|
+
"sha256:bf902d7413c82a1bfa08b06a070876132a5ae6b2388e2712aab3a7cbc02205c6",
|
77
|
+
"sha256:c12fc111ef090845de2bb15009372175d76ac99969bdf31e2ce9b42e4b8cd88f",
|
78
|
+
"sha256:c1eec9d950b6fe688edee07138993e54ee4ae634c51443cfb7c1e7613322718e",
|
79
|
+
"sha256:c640e5a06869c75994624551f45e5506e4256562ead981cce820d5ab39ae2192",
|
80
|
+
"sha256:cc1331b6d5a6e144aeb5e626f4375f5b7ae9934ba620c0ac6b3e43d5e683a0f0",
|
81
|
+
"sha256:cfd5cd998c2e36a862d0e27b2df63237e67273f2fc78f47445b14e73a810e7e6",
|
82
|
+
"sha256:d3d8da4a631471dfaf94c10c85f5277b1f8e42ac42bade1ac67da4b4a7359b73",
|
83
|
+
"sha256:d44ff19eea13ae4acdaaab0179fa68c0c6f2f45d66a4d8ec1eda7d6cecbcc15f",
|
84
|
+
"sha256:dd0052e9db3474df30433f83a71b9b23bd9e4ef1de13d92df21a52c0303b8ab6",
|
85
|
+
"sha256:dd0e081319328928531df7a0e63621caf67652c8464303fd102141b785ef9547",
|
86
|
+
"sha256:dda60aa465b861324e65a78c9f5cf0f4bc713e4309f83bc387be158b077963d9",
|
87
|
+
"sha256:e06695e0326d05b06833b40b7ef477e475d0b1ba3a6d27da1bb48c23209bf457",
|
88
|
+
"sha256:e1abe69aca89514737465752b4bcaf8016de61b3be1397a8fc260ba33321b3a8",
|
89
|
+
"sha256:e267b0ed063341f3e60acd25c05200df4193e15a4a5807075cd71225a2386e26",
|
90
|
+
"sha256:e5449ca63da169a2e6068dd0e2fcc8d91f9558aba89ff6d02121ca8ab11e79e5",
|
91
|
+
"sha256:e63e4e5081de46517099dc30abe418122f54531a6ae2ebc8680bcd7096860eab",
|
92
|
+
"sha256:f189805c8be5ca5add39e6f899e6ce2ed824e65fb45f3c28cb2841911da19070",
|
93
|
+
"sha256:f7955ecf5609dee9442cbface754f2c6e541d9e6eda87fad7f7a989b0bdb9d71",
|
94
|
+
"sha256:f86d3a7a9af5d826744fabf4afd15b9dfef44fe69a98541f666f66fbb8d3fef9",
|
95
|
+
"sha256:fbd43429d0d7ed6533b25fc993861b8fd512c42d04514a0dd6337fb3ccf22761"
|
100
96
|
],
|
101
97
|
"index": "pypi",
|
102
98
|
"markers": "python_version >= '3.9'",
|
103
|
-
"version": "==11.
|
99
|
+
"version": "==11.1.0"
|
104
100
|
}
|
105
101
|
},
|
106
102
|
"develop": {
|
@@ -38,7 +38,7 @@ fotolab -h
|
|
38
38
|
|
39
39
|
```console
|
40
40
|
usage: fotolab [-h] [-o] [-op] [-od OUTPUT_DIR] [-q] [-v] [-d] [-V]
|
41
|
-
{animate,auto,border,contrast,info,resize,rotate,
|
41
|
+
{animate,auto,border,contrast,env,info,montage,resize,rotate,sharpen,watermark} ...
|
42
42
|
|
43
43
|
A console program to manipulate photos.
|
44
44
|
|
@@ -47,19 +47,19 @@ changelog: https://github.com/kianmeng/fotolab/blob/master/CHANGELOG.md
|
|
47
47
|
issues: https://github.com/kianmeng/fotolab/issues
|
48
48
|
|
49
49
|
positional arguments:
|
50
|
-
{animate,auto,border,contrast,info,resize,rotate,
|
50
|
+
{animate,auto,border,contrast,env,info,montage,resize,rotate,sharpen,watermark}
|
51
51
|
sub-command help
|
52
52
|
animate animate an image
|
53
53
|
auto auto adjust (resize, contrast, and watermark) a photo
|
54
54
|
border add border to image
|
55
55
|
contrast contrast an image
|
56
|
+
env print environment information for bug reporting
|
56
57
|
info info an image
|
58
|
+
montage montage a list of image
|
57
59
|
resize resize an image
|
58
60
|
rotate rotate an image
|
59
|
-
montage montage a list of image
|
60
61
|
sharpen sharpen an image
|
61
62
|
watermark watermark an image
|
62
|
-
env print environment information for bug reporting
|
63
63
|
|
64
64
|
options:
|
65
65
|
-h, --help show this help message and exit
|
@@ -183,7 +183,7 @@ fotolab info -h
|
|
183
183
|
<!--help-info !-->
|
184
184
|
|
185
185
|
```console
|
186
|
-
usage: fotolab info [-h] [-s] [--camera] IMAGE_FILENAME
|
186
|
+
usage: fotolab info [-h] [-s] [--camera] [--datetime] IMAGE_FILENAME
|
187
187
|
|
188
188
|
positional arguments:
|
189
189
|
IMAGE_FILENAME set the image filename
|
@@ -192,6 +192,7 @@ options:
|
|
192
192
|
-h, --help show this help message and exit
|
193
193
|
-s, --sort show image info by sorted field name
|
194
194
|
--camera show the camera maker details
|
195
|
+
--datetime show the datetime
|
195
196
|
```
|
196
197
|
|
197
198
|
<!--help-info !-->
|
@@ -205,13 +206,17 @@ fotolab rotate -h
|
|
205
206
|
<!--help-rotate !-->
|
206
207
|
|
207
208
|
```console
|
208
|
-
usage: fotolab rotate [-h]
|
209
|
+
usage: fotolab rotate [-h] [-r ROTATION] [-cw]
|
210
|
+
IMAGE_FILENAMES [IMAGE_FILENAMES ...]
|
209
211
|
|
210
212
|
positional arguments:
|
211
|
-
IMAGE_FILENAMES
|
213
|
+
IMAGE_FILENAMES set the image filenames
|
212
214
|
|
213
215
|
options:
|
214
|
-
-h, --help
|
216
|
+
-h, --help show this help message and exit
|
217
|
+
-r, --rotation ROTATION
|
218
|
+
Rotation angle in degrees (default: '0')
|
219
|
+
-cw, --clockwise Rotate clockwise (default: 'False)
|
215
220
|
```
|
216
221
|
|
217
222
|
<!--help-rotate !-->
|
@@ -272,7 +277,7 @@ fotolab sharpen -h
|
|
272
277
|
<!--help-sharpen !-->
|
273
278
|
|
274
279
|
```console
|
275
|
-
usage: fotolab sharpen [-h] [-r RADIUS] [-p PERCENT] [-t THRESHOLD]
|
280
|
+
usage: fotolab sharpen [-h] [-r RADIUS] [-p PERCENT] [-t THRESHOLD] [-ba]
|
276
281
|
IMAGE_FILENAMES [IMAGE_FILENAMES ...]
|
277
282
|
|
278
283
|
positional arguments:
|
@@ -287,6 +292,7 @@ options:
|
|
287
292
|
-t, --threshold THRESHOLD
|
288
293
|
set the minimum brightness changed to be sharpened
|
289
294
|
(default: '3')
|
295
|
+
-ba, --before-after generate a GIF showing before and after changes
|
290
296
|
```
|
291
297
|
|
292
298
|
<!--help-sharpen !-->
|
@@ -357,7 +363,7 @@ options:
|
|
357
363
|
|
358
364
|
## Copyright and License
|
359
365
|
|
360
|
-
Copyright (C) 2024 Kian-Meng Ang
|
366
|
+
Copyright (C) 2024,2025 Kian-Meng Ang
|
361
367
|
|
362
368
|
This program is free software: you can redistribute it and/or modify it under
|
363
369
|
the terms of the GNU Affero General Public License as published by the Free
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright (C) 2024 Kian-Meng Ang
|
1
|
+
# Copyright (C) 2024,2025 Kian-Meng Ang
|
2
2
|
#
|
3
3
|
# This program is free software: you can redistribute it and/or modify it under
|
4
4
|
# the terms of the GNU Affero General Public License as published by the Free
|
@@ -21,7 +21,7 @@ import subprocess
|
|
21
21
|
import sys
|
22
22
|
from pathlib import Path
|
23
23
|
|
24
|
-
__version__ = "0.
|
24
|
+
__version__ = "0.25.1"
|
25
25
|
|
26
26
|
log = logging.getLogger(__name__)
|
27
27
|
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright (
|
1
|
+
# Copyright (C) 2024,2025 Kian-Meng Ang
|
2
2
|
|
3
3
|
# This program is free software: you can redistribute it and/or modify
|
4
4
|
# it under the terms of the GNU General Public License as published by
|
@@ -23,46 +23,40 @@
|
|
23
23
|
import argparse
|
24
24
|
import logging
|
25
25
|
import sys
|
26
|
-
from typing import
|
27
|
-
|
28
|
-
import fotolab.
|
29
|
-
import fotolab.auto
|
30
|
-
import fotolab.border
|
31
|
-
import fotolab.contrast
|
32
|
-
import fotolab.env
|
33
|
-
import fotolab.info
|
34
|
-
import fotolab.montage
|
35
|
-
import fotolab.resize
|
36
|
-
import fotolab.rotate
|
37
|
-
import fotolab.sharpen
|
38
|
-
import fotolab.watermark
|
26
|
+
from typing import Optional, Sequence
|
27
|
+
|
28
|
+
import fotolab.subcommands
|
39
29
|
from fotolab import __version__
|
40
30
|
|
41
31
|
log = logging.getLogger(__name__)
|
42
32
|
|
43
33
|
|
44
34
|
def setup_logging(args: argparse.Namespace) -> None:
|
45
|
-
"""
|
46
|
-
if args.verbose == 0:
|
47
|
-
logging.getLogger("PIL").setLevel(logging.ERROR)
|
35
|
+
"""Sets up logging configuration based on command-line arguments.
|
48
36
|
|
37
|
+
Args:
|
38
|
+
args (argparse.Namespace): Namespace containing parsed arguments.
|
39
|
+
"""
|
49
40
|
if args.quiet:
|
50
41
|
logging.disable(logging.NOTSET)
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
42
|
+
return
|
43
|
+
|
44
|
+
if args.verbose == 0:
|
45
|
+
logging.getLogger("PIL").setLevel(logging.ERROR)
|
46
|
+
|
47
|
+
level = logging.DEBUG if args.debug else logging.INFO
|
48
|
+
format_string = (
|
49
|
+
"[%(asctime)s] %(levelname)s: %(name)s: %(message)s"
|
50
|
+
if args.debug
|
51
|
+
else "%(message)s"
|
52
|
+
)
|
53
|
+
|
54
|
+
logging.basicConfig(
|
55
|
+
level=level,
|
56
|
+
format=format_string,
|
57
|
+
stream=sys.stdout,
|
58
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
59
|
+
)
|
66
60
|
|
67
61
|
|
68
62
|
def build_parser() -> argparse.ArgumentParser:
|
@@ -136,17 +130,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
136
130
|
)
|
137
131
|
|
138
132
|
subparsers = parser.add_subparsers(help="sub-command help")
|
139
|
-
fotolab.
|
140
|
-
fotolab.auto.build_subparser(subparsers)
|
141
|
-
fotolab.border.build_subparser(subparsers)
|
142
|
-
fotolab.contrast.build_subparser(subparsers)
|
143
|
-
fotolab.info.build_subparser(subparsers)
|
144
|
-
fotolab.resize.build_subparser(subparsers)
|
145
|
-
fotolab.rotate.build_subparser(subparsers)
|
146
|
-
fotolab.montage.build_subparser(subparsers)
|
147
|
-
fotolab.sharpen.build_subparser(subparsers)
|
148
|
-
fotolab.watermark.build_subparser(subparsers)
|
149
|
-
fotolab.env.build_subparser(subparsers)
|
133
|
+
fotolab.subcommands.build_subparser(subparsers)
|
150
134
|
|
151
135
|
return parser
|
152
136
|
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# Copyright (C) 2024,2025 Kian-Meng Ang
|
2
|
+
#
|
3
|
+
# This program is free software: you can redistribute it and/or modify it under
|
4
|
+
# the terms of the GNU Affero General Public License as published by the Free
|
5
|
+
# Software Foundation, either version 3 of the License, or (at your option) any
|
6
|
+
# later version.
|
7
|
+
#
|
8
|
+
# This program is distributed in the hope that it will be useful, but WITHOUT
|
9
|
+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
10
|
+
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
11
|
+
# details.
|
12
|
+
#
|
13
|
+
# You should have received a copy of the GNU Affero General Public License
|
14
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
15
|
+
|
16
|
+
"""Common utils for subcommand."""
|
17
|
+
|
18
|
+
import importlib
|
19
|
+
import pkgutil
|
20
|
+
|
21
|
+
|
22
|
+
def build_subparser(subparsers):
|
23
|
+
"""Build subparser for each subcommands."""
|
24
|
+
iter_namespace = pkgutil.iter_modules(__path__, __name__ + ".")
|
25
|
+
|
26
|
+
subcommands = {
|
27
|
+
name: importlib.import_module(name)
|
28
|
+
for finder, name, ispkg in iter_namespace
|
29
|
+
}
|
30
|
+
|
31
|
+
for subcommand in subcommands.values():
|
32
|
+
subcommand.build_subparser(subparsers)
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright (C) 2024 Kian-Meng Ang
|
1
|
+
# Copyright (C) 2024,2025 Kian-Meng Ang
|
2
2
|
#
|
3
3
|
# This program is free software: you can redistribute it and/or modify it under
|
4
4
|
# the terms of the GNU Affero General Public License as published by the Free
|
@@ -18,10 +18,10 @@
|
|
18
18
|
import argparse
|
19
19
|
import logging
|
20
20
|
|
21
|
-
import fotolab.contrast
|
22
|
-
import fotolab.resize
|
23
|
-
import fotolab.sharpen
|
24
|
-
import fotolab.watermark
|
21
|
+
import fotolab.subcommands.contrast
|
22
|
+
import fotolab.subcommands.resize
|
23
|
+
import fotolab.subcommands.sharpen
|
24
|
+
import fotolab.subcommands.watermark
|
25
25
|
|
26
26
|
log = logging.getLogger(__name__)
|
27
27
|
|
@@ -70,6 +70,7 @@ def run(args: argparse.Namespace) -> None:
|
|
70
70
|
"camera": False,
|
71
71
|
"canvas": False,
|
72
72
|
"lowercase": False,
|
73
|
+
"before_after": False,
|
73
74
|
}
|
74
75
|
combined_args = argparse.Namespace(**vars(args), **extra_args)
|
75
76
|
combined_args.overwrite = True
|
@@ -77,7 +78,7 @@ def run(args: argparse.Namespace) -> None:
|
|
77
78
|
log.debug(args)
|
78
79
|
log.debug(combined_args)
|
79
80
|
|
80
|
-
fotolab.resize.run(combined_args)
|
81
|
-
fotolab.contrast.run(combined_args)
|
82
|
-
fotolab.sharpen.run(combined_args)
|
83
|
-
fotolab.watermark.run(combined_args)
|
81
|
+
fotolab.subcommands.resize.run(combined_args)
|
82
|
+
fotolab.subcommands.contrast.run(combined_args)
|
83
|
+
fotolab.subcommands.sharpen.run(combined_args)
|
84
|
+
fotolab.subcommands.watermark.run(combined_args)
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright (C) 2024 Kian-Meng Ang
|
1
|
+
# Copyright (C) 2024,2025 Kian-Meng Ang
|
2
2
|
#
|
3
3
|
# This program is free software: you can redistribute it and/or modify it under
|
4
4
|
# the terms of the GNU Affero General Public License as published by the Free
|
@@ -115,25 +115,28 @@ def run(args: argparse.Namespace) -> None:
|
|
115
115
|
for image_filename in args.image_filenames:
|
116
116
|
original_image = Image.open(image_filename)
|
117
117
|
|
118
|
-
|
119
|
-
args.width_left
|
120
|
-
or args.width_top
|
121
|
-
or args.width_right
|
122
|
-
or args.width_bottom
|
123
|
-
):
|
124
|
-
border = (
|
125
|
-
int(args.width_left),
|
126
|
-
int(args.width_top),
|
127
|
-
int(args.width_right),
|
128
|
-
int(args.width_bottom),
|
129
|
-
)
|
130
|
-
else:
|
131
|
-
border = args.width
|
132
|
-
|
118
|
+
border = get_border(args)
|
133
119
|
bordered_image = ImageOps.expand(
|
134
120
|
original_image,
|
135
121
|
border=border,
|
136
122
|
fill=ImageColor.getrgb(args.color),
|
137
123
|
)
|
138
124
|
|
139
|
-
save_image(args, bordered_image, image_filename, "
|
125
|
+
save_image(args, bordered_image, image_filename, "border")
|
126
|
+
|
127
|
+
|
128
|
+
def get_border(args: argparse.Namespace) -> tuple:
|
129
|
+
"""Calculate the border."""
|
130
|
+
if (
|
131
|
+
args.width_left
|
132
|
+
or args.width_top
|
133
|
+
or args.width_right
|
134
|
+
or args.width_bottom
|
135
|
+
):
|
136
|
+
return (
|
137
|
+
int(args.width_left),
|
138
|
+
int(args.width_top),
|
139
|
+
int(args.width_right),
|
140
|
+
int(args.width_bottom),
|
141
|
+
)
|
142
|
+
return args.width
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright (C) 2024 Kian-Meng Ang
|
1
|
+
# Copyright (C) 2024,2025 Kian-Meng Ang
|
2
2
|
#
|
3
3
|
# This program is free software: you can redistribute it and/or modify it under
|
4
4
|
# the terms of the GNU Affero General Public License as published by the Free
|
@@ -44,9 +44,9 @@ def run(_args: argparse.Namespace) -> None:
|
|
44
44
|
None
|
45
45
|
"""
|
46
46
|
sys_version = sys.version.replace("\n", "")
|
47
|
-
|
47
|
+
env = [
|
48
48
|
f"fotolab: {__version__}",
|
49
49
|
f"python: {sys_version}",
|
50
50
|
f"platform: {platform.platform()}",
|
51
|
-
|
52
|
-
)
|
51
|
+
]
|
52
|
+
print(*env, sep="\n")
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright (C) 2024 Kian-Meng Ang
|
1
|
+
# Copyright (C) 2024,2025 Kian-Meng Ang
|
2
2
|
#
|
3
3
|
# This program is free software: you can redistribute it and/or modify it under
|
4
4
|
# the terms of the GNU Affero General Public License as published by the Free
|
@@ -73,12 +73,20 @@ def run(args: argparse.Namespace) -> None:
|
|
73
73
|
None
|
74
74
|
"""
|
75
75
|
log.debug(args)
|
76
|
+
|
77
|
+
info = []
|
78
|
+
image = Image.open(args.image_filename)
|
79
|
+
|
76
80
|
if args.camera:
|
77
|
-
|
81
|
+
info.append(camera_metadata(image))
|
82
|
+
|
78
83
|
if args.datetime:
|
79
|
-
|
84
|
+
info.append(datetime(image))
|
85
|
+
|
86
|
+
if info:
|
87
|
+
print("\n".join(info))
|
80
88
|
else:
|
81
|
-
exif_tags = extract_exif_tags(
|
89
|
+
exif_tags = extract_exif_tags(image)
|
82
90
|
if exif_tags:
|
83
91
|
tag_name_width = max(map(len, exif_tags))
|
84
92
|
for tag_name, tag_value in exif_tags.items():
|
@@ -87,9 +95,8 @@ def run(args: argparse.Namespace) -> None:
|
|
87
95
|
print("No metadata found!")
|
88
96
|
|
89
97
|
|
90
|
-
def extract_exif_tags(
|
98
|
+
def extract_exif_tags(image: Image.Image, sort: bool = False) -> dict:
|
91
99
|
"""Extract Exif metadata from image."""
|
92
|
-
image = Image.open(image_filename)
|
93
100
|
exif = image._getexif()
|
94
101
|
log.debug(exif)
|
95
102
|
|
@@ -106,14 +113,14 @@ def extract_exif_tags(image_filename: str, sort: bool = False) -> dict:
|
|
106
113
|
return filtered_info
|
107
114
|
|
108
115
|
|
109
|
-
def datetime(
|
116
|
+
def datetime(image: Image.Image):
|
110
117
|
"""Extract datetime metadata."""
|
111
|
-
exif_tags = extract_exif_tags(
|
118
|
+
exif_tags = extract_exif_tags(image)
|
112
119
|
return exif_tags["DateTime"]
|
113
120
|
|
114
121
|
|
115
|
-
def camera_metadata(
|
122
|
+
def camera_metadata(image: Image.Image):
|
116
123
|
"""Extract camera and model metadata."""
|
117
|
-
exif_tags = extract_exif_tags(
|
124
|
+
exif_tags = extract_exif_tags(image)
|
118
125
|
metadata = f'{exif_tags["Make"]} {exif_tags["Model"]}'
|
119
126
|
return metadata.strip()
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright (C) 2024 Kian-Meng Ang
|
1
|
+
# Copyright (C) 2024,2025 Kian-Meng Ang
|
2
2
|
#
|
3
3
|
# This program is free software: you can redistribute it and/or modify it under
|
4
4
|
# the terms of the GNU Affero General Public License as published by the Free
|
@@ -63,9 +63,9 @@ def run(args: argparse.Namespace) -> None:
|
|
63
63
|
montaged_image = Image.new("RGB", (total_width, total_height))
|
64
64
|
|
65
65
|
x_offset = 0
|
66
|
-
for
|
67
|
-
montaged_image.paste(
|
68
|
-
x_offset +=
|
66
|
+
for image in images:
|
67
|
+
montaged_image.paste(image, (x_offset, 0))
|
68
|
+
x_offset += image.width
|
69
69
|
|
70
70
|
output_image_filename = args.image_filenames[0].name
|
71
71
|
save_image(args, montaged_image, output_image_filename, "montage")
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright (C) 2024 Kian-Meng Ang
|
1
|
+
# Copyright (C) 2024,2025 Kian-Meng Ang
|
2
2
|
#
|
3
3
|
# This program is free software: you can redistribute it and/or modify it under
|
4
4
|
# the terms of the GNU Affero General Public License as published by the Free
|
@@ -40,6 +40,21 @@ def build_subparser(subparsers) -> None:
|
|
40
40
|
metavar="IMAGE_FILENAMES",
|
41
41
|
)
|
42
42
|
|
43
|
+
rotate_parser.add_argument(
|
44
|
+
"-r",
|
45
|
+
"--rotation",
|
46
|
+
type=int,
|
47
|
+
default=0,
|
48
|
+
help="Rotation angle in degrees (default: '%(default)s')",
|
49
|
+
)
|
50
|
+
|
51
|
+
rotate_parser.add_argument(
|
52
|
+
"-cw",
|
53
|
+
"--clockwise",
|
54
|
+
action="store_true",
|
55
|
+
help="Rotate clockwise (default: '%(default)s)",
|
56
|
+
)
|
57
|
+
|
43
58
|
|
44
59
|
def run(args: argparse.Namespace) -> None:
|
45
60
|
"""Run rotate subcommand.
|
@@ -52,10 +67,14 @@ def run(args: argparse.Namespace) -> None:
|
|
52
67
|
"""
|
53
68
|
log.debug(args)
|
54
69
|
|
70
|
+
rotation = args.rotation
|
71
|
+
if args.clockwise:
|
72
|
+
rotation = -rotation
|
73
|
+
|
55
74
|
for image_filename in args.image_filenames:
|
56
75
|
original_image = Image.open(image_filename)
|
57
76
|
rotated_image = original_image.rotate(
|
58
|
-
|
77
|
+
rotation,
|
59
78
|
expand=True,
|
60
79
|
)
|
61
80
|
save_image(args, rotated_image, image_filename, "rotate")
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright (C) 2024 Kian-Meng Ang
|
1
|
+
# Copyright (C) 2024,2025 Kian-Meng Ang
|
2
2
|
#
|
3
3
|
# This program is free software: you can redistribute it and/or modify it under
|
4
4
|
# the terms of the GNU Affero General Public License as published by the Free
|
@@ -17,10 +17,11 @@
|
|
17
17
|
|
18
18
|
import argparse
|
19
19
|
import logging
|
20
|
+
from pathlib import Path
|
20
21
|
|
21
22
|
from PIL import Image, ImageFilter
|
22
23
|
|
23
|
-
from fotolab import save_image
|
24
|
+
from fotolab import _open_image, save_image
|
24
25
|
|
25
26
|
log = logging.getLogger(__name__)
|
26
27
|
|
@@ -76,6 +77,15 @@ def build_subparser(subparsers) -> None:
|
|
76
77
|
metavar="THRESHOLD",
|
77
78
|
)
|
78
79
|
|
80
|
+
sharpen_parser.add_argument(
|
81
|
+
"-ba",
|
82
|
+
"--before-after",
|
83
|
+
default=False,
|
84
|
+
action="store_true",
|
85
|
+
dest="before_after",
|
86
|
+
help="generate a GIF showing before and after changes",
|
87
|
+
)
|
88
|
+
|
79
89
|
|
80
90
|
def run(args: argparse.Namespace) -> None:
|
81
91
|
"""Run sharpen subcommand.
|
@@ -95,4 +105,31 @@ def run(args: argparse.Namespace) -> None:
|
|
95
105
|
args.radius, percent=args.percent, threshold=args.threshold
|
96
106
|
)
|
97
107
|
)
|
98
|
-
|
108
|
+
if args.before_after:
|
109
|
+
save_gif_image(args, image_filename, original_image, sharpen_image)
|
110
|
+
else:
|
111
|
+
save_image(args, sharpen_image, image_filename, "sharpen")
|
112
|
+
|
113
|
+
|
114
|
+
def save_gif_image(args, image_filename, original_image, sharpen_image):
|
115
|
+
"""Save the original and sharpen image."""
|
116
|
+
image_file = Path(image_filename)
|
117
|
+
new_filename = Path(
|
118
|
+
args.output_dir,
|
119
|
+
image_file.with_name(f"sharpen_gif_{image_file.stem}.gif"),
|
120
|
+
)
|
121
|
+
new_filename.parent.mkdir(parents=True, exist_ok=True)
|
122
|
+
|
123
|
+
log.info("sharpen gif image: %s", new_filename)
|
124
|
+
original_image.save(
|
125
|
+
new_filename,
|
126
|
+
format="gif",
|
127
|
+
append_images=[sharpen_image],
|
128
|
+
save_all=True,
|
129
|
+
duration=2500,
|
130
|
+
loop=0,
|
131
|
+
optimize=True,
|
132
|
+
)
|
133
|
+
|
134
|
+
if args.open:
|
135
|
+
_open_image(new_filename)
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright (C) 2024 Kian-Meng Ang
|
1
|
+
# Copyright (C) 2024,2025 Kian-Meng Ang
|
2
2
|
#
|
3
3
|
# This program is free software: you can redistribute it and/or modify it under
|
4
4
|
# the terms of the GNU Affero General Public License as published by the Free
|
@@ -19,10 +19,10 @@ import argparse
|
|
19
19
|
import logging
|
20
20
|
import math
|
21
21
|
|
22
|
-
from PIL import Image, ImageColor, ImageDraw, ImageFont
|
22
|
+
from PIL import Image, ImageColor, ImageDraw, ImageFont, ImageSequence
|
23
23
|
|
24
24
|
from fotolab import save_image
|
25
|
-
from fotolab.info import camera_metadata
|
25
|
+
from fotolab.subcommands.info import camera_metadata
|
26
26
|
|
27
27
|
log = logging.getLogger(__name__)
|
28
28
|
|
@@ -157,13 +157,72 @@ def run(args: argparse.Namespace) -> None:
|
|
157
157
|
log.debug(args)
|
158
158
|
|
159
159
|
for image_filename in args.image_filenames:
|
160
|
-
|
161
|
-
|
160
|
+
image = Image.open(image_filename)
|
161
|
+
if image.format == "GIF":
|
162
|
+
watermark_gif_image(image, args)
|
163
|
+
else:
|
164
|
+
watermarked_image = watermark_non_gif_image(image, args)
|
165
|
+
save_image(args, watermarked_image, image_filename, "watermark")
|
162
166
|
|
163
167
|
|
164
|
-
def
|
168
|
+
def watermark_gif_image(original_image: Image.Image, args: argparse.Namespace):
|
169
|
+
"""Watermark the image."""
|
170
|
+
watermarked_image = original_image.copy()
|
171
|
+
|
172
|
+
frames = []
|
173
|
+
|
174
|
+
for frame in ImageSequence.Iterator(original_image):
|
175
|
+
frame = frame.convert("RGBA")
|
176
|
+
draw = ImageDraw.Draw(frame)
|
177
|
+
|
178
|
+
font = ImageFont.load_default(calc_font_size(original_image, args))
|
179
|
+
log.debug("default font: %s", " ".join(font.getname()))
|
180
|
+
|
181
|
+
text = args.text
|
182
|
+
if args.camera and camera_metadata(original_image):
|
183
|
+
text = camera_metadata(original_image)
|
184
|
+
|
185
|
+
if args.lowercase:
|
186
|
+
text = text.lower()
|
187
|
+
|
188
|
+
(left, top, right, bottom) = draw.textbbox(
|
189
|
+
xy=(0, 0), text=text, font=font
|
190
|
+
)
|
191
|
+
text_width = right - left
|
192
|
+
text_height = bottom - top
|
193
|
+
(position_x, position_y) = calc_position(
|
194
|
+
watermarked_image,
|
195
|
+
text_width,
|
196
|
+
text_height,
|
197
|
+
args.position,
|
198
|
+
calc_padding(original_image, args),
|
199
|
+
)
|
200
|
+
|
201
|
+
draw.text(
|
202
|
+
(position_x, position_y),
|
203
|
+
text,
|
204
|
+
font=font,
|
205
|
+
fill=(*ImageColor.getrgb(args.font_color), 128),
|
206
|
+
stroke_width=calc_font_outline_width(original_image, args),
|
207
|
+
stroke_fill=(*ImageColor.getrgb(args.outline_color), 128),
|
208
|
+
)
|
209
|
+
frames.append(frame)
|
210
|
+
|
211
|
+
frames[0].save(
|
212
|
+
"foo.gif",
|
213
|
+
format="GIF",
|
214
|
+
append_images=frames[1:],
|
215
|
+
save_all=True,
|
216
|
+
duration=original_image.info.get("duration", 100),
|
217
|
+
loop=original_image.info.get("loop", 0),
|
218
|
+
disposal=original_image.info.get("disposal", 2),
|
219
|
+
)
|
220
|
+
|
221
|
+
|
222
|
+
def watermark_non_gif_image(
|
223
|
+
original_image: Image.Image, args: argparse.Namespace
|
224
|
+
):
|
165
225
|
"""Watermark the image."""
|
166
|
-
original_image = Image.open(image_filename)
|
167
226
|
watermarked_image = original_image.copy()
|
168
227
|
|
169
228
|
draw = ImageDraw.Draw(watermarked_image)
|
@@ -172,8 +231,8 @@ def watermark_image(image_filename, args):
|
|
172
231
|
log.debug("default font: %s", " ".join(font.getname()))
|
173
232
|
|
174
233
|
text = args.text
|
175
|
-
if args.camera and camera_metadata(
|
176
|
-
text = camera_metadata(
|
234
|
+
if args.camera and camera_metadata(original_image):
|
235
|
+
text = camera_metadata(original_image)
|
177
236
|
|
178
237
|
if args.lowercase:
|
179
238
|
text = text.lower()
|
@@ -237,19 +296,14 @@ def calc_padding(image, args) -> int:
|
|
237
296
|
|
238
297
|
def calc_position(image, text_width, text_height, position, padding) -> tuple:
|
239
298
|
"""Calculate the boundary coordinates of the watermark text."""
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
elif position == "bottom-right":
|
252
|
-
position_x = image.width - text_width - padding
|
253
|
-
position_y = image.height - text_height - padding
|
254
|
-
|
255
|
-
return (position_x, position_y)
|
299
|
+
positions = {
|
300
|
+
"top-left": (padding, padding),
|
301
|
+
"top-right": (image.width - text_width - padding, padding),
|
302
|
+
"bottom-left": (padding, image.height - text_height - padding),
|
303
|
+
"bottom-right": (
|
304
|
+
image.width - text_width - padding,
|
305
|
+
image.height - text_height - padding,
|
306
|
+
),
|
307
|
+
}
|
308
|
+
|
309
|
+
return positions.get(position, (0, 0))
|
fotolab-0.22.0/.python-version
DELETED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|