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.
Files changed (43) hide show
  1. {fotolab-0.22.0 → fotolab-0.25.1}/.pre-commit-config.yaml +1 -1
  2. fotolab-0.25.1/.python-version +5 -0
  3. {fotolab-0.22.0 → fotolab-0.25.1}/CHANGELOG.md +33 -0
  4. {fotolab-0.22.0 → fotolab-0.25.1}/PKG-INFO +17 -11
  5. {fotolab-0.22.0 → fotolab-0.25.1}/Pipfile.lock +72 -76
  6. {fotolab-0.22.0 → fotolab-0.25.1}/README.md +16 -10
  7. {fotolab-0.22.0 → fotolab-0.25.1}/fotolab/__init__.py +2 -2
  8. {fotolab-0.22.0 → fotolab-0.25.1}/fotolab/cli.py +27 -43
  9. fotolab-0.25.1/fotolab/subcommands/__init__.py +32 -0
  10. {fotolab-0.22.0/fotolab → fotolab-0.25.1/fotolab/subcommands}/animate.py +1 -1
  11. {fotolab-0.22.0/fotolab → fotolab-0.25.1/fotolab/subcommands}/auto.py +10 -9
  12. {fotolab-0.22.0/fotolab → fotolab-0.25.1/fotolab/subcommands}/border.py +20 -17
  13. {fotolab-0.22.0/fotolab → fotolab-0.25.1/fotolab/subcommands}/contrast.py +1 -1
  14. {fotolab-0.22.0/fotolab → fotolab-0.25.1/fotolab/subcommands}/env.py +4 -4
  15. {fotolab-0.22.0/fotolab → fotolab-0.25.1/fotolab/subcommands}/info.py +17 -10
  16. {fotolab-0.22.0/fotolab → fotolab-0.25.1/fotolab/subcommands}/montage.py +4 -4
  17. {fotolab-0.22.0/fotolab → fotolab-0.25.1/fotolab/subcommands}/resize.py +1 -1
  18. {fotolab-0.22.0/fotolab → fotolab-0.25.1/fotolab/subcommands}/rotate.py +21 -2
  19. {fotolab-0.22.0/fotolab → fotolab-0.25.1/fotolab/subcommands}/sharpen.py +40 -3
  20. {fotolab-0.22.0/fotolab → fotolab-0.25.1/fotolab/subcommands}/watermark.py +79 -25
  21. fotolab-0.22.0/.python-version +0 -5
  22. {fotolab-0.22.0 → fotolab-0.25.1}/.coveragerc +0 -0
  23. {fotolab-0.22.0 → fotolab-0.25.1}/.gitignore +0 -0
  24. {fotolab-0.22.0 → fotolab-0.25.1}/CONTRIBUTING.md +0 -0
  25. {fotolab-0.22.0 → fotolab-0.25.1}/LICENSE.md +0 -0
  26. {fotolab-0.22.0 → fotolab-0.25.1}/Pipfile +0 -0
  27. {fotolab-0.22.0 → fotolab-0.25.1}/docs/Makefile +0 -0
  28. {fotolab-0.22.0 → fotolab-0.25.1}/docs/make.bat +0 -0
  29. {fotolab-0.22.0 → fotolab-0.25.1}/docs/source/CHANGELOG.md +0 -0
  30. {fotolab-0.22.0 → fotolab-0.25.1}/docs/source/CONTRIBUTING.md +0 -0
  31. {fotolab-0.22.0 → fotolab-0.25.1}/docs/source/LICENSE.md +0 -0
  32. {fotolab-0.22.0 → fotolab-0.25.1}/docs/source/README.md +0 -0
  33. {fotolab-0.22.0 → fotolab-0.25.1}/docs/source/_static/logo.jpg +0 -0
  34. {fotolab-0.22.0 → fotolab-0.25.1}/docs/source/conf.py +0 -0
  35. {fotolab-0.22.0 → fotolab-0.25.1}/docs/source/index.rst +0 -0
  36. {fotolab-0.22.0 → fotolab-0.25.1}/fotolab/__main__.py +0 -0
  37. {fotolab-0.22.0 → fotolab-0.25.1}/noxfile.py +0 -0
  38. {fotolab-0.22.0 → fotolab-0.25.1}/pyproject.toml +0 -0
  39. {fotolab-0.22.0 → fotolab-0.25.1}/tests/__init__.py +0 -0
  40. {fotolab-0.22.0 → fotolab-0.25.1}/tests/conftest.py +0 -0
  41. {fotolab-0.22.0 → fotolab-0.25.1}/tests/test_env.py +0 -0
  42. {fotolab-0.22.0 → fotolab-0.25.1}/tests/test_help_flag.py +0 -0
  43. {fotolab-0.22.0 → fotolab-0.25.1}/tests/test_quiet_flag.py +0 -0
@@ -107,7 +107,7 @@ repos:
107
107
  - --disable=R0801,W0212
108
108
 
109
109
  - repo: https://github.com/pre-commit/mirrors-mypy
110
- rev: v1.13.0
110
+ rev: v1.14.1
111
111
  hooks:
112
112
  - id: mypy
113
113
  exclude: docs/
@@ -0,0 +1,5 @@
1
+ 3.9
2
+ 3.10
3
+ 3.11
4
+ 3.12
5
+ 3.13
@@ -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.22.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,montage,sharpen,watermark,env} ...
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,montage,sharpen,watermark,env}
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] IMAGE_FILENAMES [IMAGE_FILENAMES ...]
232
+ usage: fotolab rotate [-h] [-r ROTATION] [-cw]
233
+ IMAGE_FILENAMES [IMAGE_FILENAMES ...]
232
234
 
233
235
  positional arguments:
234
- IMAGE_FILENAMES set the image filenames
236
+ IMAGE_FILENAMES set the image filenames
235
237
 
236
238
  options:
237
- -h, --help show this help message and exit
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:00177a63030d612148e659b55ba99527803288cea7c75fb05766ab7981a8c1b7",
26
- "sha256:006bcdd307cc47ba43e924099a038cbf9591062e6c50e570819743f5607404f5",
27
- "sha256:084a07ef0821cfe4858fe86652fffac8e187b6ae677e9906e192aafcc1b69903",
28
- "sha256:0ae08bd8ffc41aebf578c2af2f9d8749d91f448b3bfd41d7d9ff573d74f2a6b2",
29
- "sha256:0e038b0745997c7dcaae350d35859c9715c71e92ffb7e0f4a8e8a16732150f38",
30
- "sha256:1187739620f2b365de756ce086fdb3604573337cc28a0d3ac4a01ab6b2d2a6d2",
31
- "sha256:16095692a253047fe3ec028e951fa4221a1f3ed3d80c397e83541a3037ff67c9",
32
- "sha256:1a61b54f87ab5786b8479f81c4b11f4d61702830354520837f8cc791ebba0f5f",
33
- "sha256:1c1d72714f429a521d8d2d018badc42414c3077eb187a59579f28e4270b4b0fc",
34
- "sha256:1e2688958a840c822279fda0086fec1fdab2f95bf2b717b66871c4ad9859d7e8",
35
- "sha256:20ec184af98a121fb2da42642dea8a29ec80fc3efbaefb86d8fdd2606619045d",
36
- "sha256:21a0d3b115009ebb8ac3d2ebec5c2982cc693da935f4ab7bb5c8ebe2f47d36f2",
37
- "sha256:224aaa38177597bb179f3ec87eeefcce8e4f85e608025e9cfac60de237ba6316",
38
- "sha256:2679d2258b7f1192b378e2893a8a0a0ca472234d4c2c0e6bdd3380e8dfa21b6a",
39
- "sha256:27a7860107500d813fcd203b4ea19b04babe79448268403172782754870dac25",
40
- "sha256:290f2cc809f9da7d6d622550bbf4c1e57518212da51b6a30fe8e0a270a5b78bd",
41
- "sha256:2e46773dc9f35a1dd28bd6981332fd7f27bec001a918a72a79b4133cf5291dba",
42
- "sha256:3107c66e43bda25359d5ef446f59c497de2b5ed4c7fdba0894f8d6cf3822dafc",
43
- "sha256:375b8dd15a1f5d2feafff536d47e22f69625c1aa92f12b339ec0b2ca40263273",
44
- "sha256:45c566eb10b8967d71bf1ab8e4a525e5a93519e29ea071459ce517f6b903d7fa",
45
- "sha256:499c3a1b0d6fc8213519e193796eb1a86a1be4b1877d678b30f83fd979811d1a",
46
- "sha256:4ad70c4214f67d7466bea6a08061eba35c01b1b89eaa098040a35272a8efb22b",
47
- "sha256:4b60c9520f7207aaf2e1d94de026682fc227806c6e1f55bba7606d1c94dd623a",
48
- "sha256:5178952973e588b3f1360868847334e9e3bf49d19e169bbbdfaf8398002419ae",
49
- "sha256:52a2d8323a465f84faaba5236567d212c3668f2ab53e1c74c15583cf507a0291",
50
- "sha256:598b4e238f13276e0008299bd2482003f48158e2b11826862b1eb2ad7c768b97",
51
- "sha256:5bd2d3bdb846d757055910f0a59792d33b555800813c3b39ada1829c372ccb06",
52
- "sha256:5c39ed17edea3bc69c743a8dd3e9853b7509625c2462532e62baa0732163a904",
53
- "sha256:5d203af30149ae339ad1b4f710d9844ed8796e97fda23ffbc4cc472968a47d0b",
54
- "sha256:5ddbfd761ee00c12ee1be86c9c0683ecf5bb14c9772ddbd782085779a63dd55b",
55
- "sha256:607bbe123c74e272e381a8d1957083a9463401f7bd01287f50521ecb05a313f8",
56
- "sha256:61b887f9ddba63ddf62fd02a3ba7add935d053b6dd7d58998c630e6dbade8527",
57
- "sha256:6619654954dc4936fcff82db8eb6401d3159ec6be81e33c6000dfd76ae189947",
58
- "sha256:674629ff60030d144b7bca2b8330225a9b11c482ed408813924619c6f302fdbb",
59
- "sha256:6ec0d5af64f2e3d64a165f490d96368bb5dea8b8f9ad04487f9ab60dc4bb6003",
60
- "sha256:6f4dba50cfa56f910241eb7f883c20f1e7b1d8f7d91c750cd0b318bad443f4d5",
61
- "sha256:70fbbdacd1d271b77b7721fe3cdd2d537bbbd75d29e6300c672ec6bb38d9672f",
62
- "sha256:72bacbaf24ac003fea9bff9837d1eedb6088758d41e100c1552930151f677739",
63
- "sha256:7326a1787e3c7b0429659e0a944725e1b03eeaa10edd945a86dead1913383944",
64
- "sha256:73853108f56df97baf2bb8b522f3578221e56f646ba345a372c78326710d3830",
65
- "sha256:73e3a0200cdda995c7e43dd47436c1548f87a30bb27fb871f352a22ab8dcf45f",
66
- "sha256:75acbbeb05b86bc53cbe7b7e6fe00fbcf82ad7c684b3ad82e3d711da9ba287d3",
67
- "sha256:8069c5179902dcdce0be9bfc8235347fdbac249d23bd90514b7a47a72d9fecf4",
68
- "sha256:846e193e103b41e984ac921b335df59195356ce3f71dcfd155aa79c603873b84",
69
- "sha256:8594f42df584e5b4bb9281799698403f7af489fba84c34d53d1c4bfb71b7c4e7",
70
- "sha256:86510e3f5eca0ab87429dd77fafc04693195eec7fd6a137c389c3eeb4cfb77c6",
71
- "sha256:8853a3bf12afddfdf15f57c4b02d7ded92c7a75a5d7331d19f4f9572a89c17e6",
72
- "sha256:88a58d8ac0cc0e7f3a014509f0455248a76629ca9b604eca7dc5927cc593c5e9",
73
- "sha256:8ba470552b48e5835f1d23ecb936bb7f71d206f9dfeee64245f30c3270b994de",
74
- "sha256:8c676b587da5673d3c75bd67dd2a8cdfeb282ca38a30f37950511766b26858c4",
75
- "sha256:8ec4a89295cd6cd4d1058a5e6aec6bf51e0eaaf9714774e1bfac7cfc9051db47",
76
- "sha256:94f3e1780abb45062287b4614a5bc0874519c86a777d4a7ad34978e86428b8dd",
77
- "sha256:9a0f748eaa434a41fccf8e1ee7a3eed68af1b690e75328fd7a60af123c193b50",
78
- "sha256:a5629742881bcbc1f42e840af185fd4d83a5edeb96475a575f4da50d6ede337c",
79
- "sha256:a65149d8ada1055029fcb665452b2814fe7d7082fcb0c5bed6db851cb69b2086",
80
- "sha256:b3c5ac4bed7519088103d9450a1107f76308ecf91d6dabc8a33a2fcfb18d0fba",
81
- "sha256:b4fd7bd29610a83a8c9b564d457cf5bd92b4e11e79a4ee4716a63c959699b306",
82
- "sha256:bcd1fb5bb7b07f64c15618c89efcc2cfa3e95f0e3bcdbaf4642509de1942a699",
83
- "sha256:c12b5ae868897c7338519c03049a806af85b9b8c237b7d675b8c5e089e4a618e",
84
- "sha256:c26845094b1af3c91852745ae78e3ea47abf3dbcd1cf962f16b9a5fbe3ee8488",
85
- "sha256:c6a660307ca9d4867caa8d9ca2c2658ab685de83792d1876274991adec7b93fa",
86
- "sha256:c809a70e43c7977c4a42aefd62f0131823ebf7dd73556fa5d5950f5b354087e2",
87
- "sha256:c8b2351c85d855293a299038e1f89db92a2f35e8d2f783489c6f0b2b5f3fe8a3",
88
- "sha256:cb929ca942d0ec4fac404cbf520ee6cac37bf35be479b970c4ffadf2b6a1cad9",
89
- "sha256:d2c0a187a92a1cb5ef2c8ed5412dd8d4334272617f532d4ad4de31e0495bd923",
90
- "sha256:d69bfd8ec3219ae71bcde1f942b728903cad25fafe3100ba2258b973bd2bc1b2",
91
- "sha256:daffdf51ee5db69a82dd127eabecce20729e21f7a3680cf7cbb23f0829189790",
92
- "sha256:e58876c91f97b0952eb766123bfef372792ab3f4e3e1f1a2267834c2ab131734",
93
- "sha256:eda2616eb2313cbb3eebbe51f19362eb434b18e3bb599466a1ffa76a033fb916",
94
- "sha256:ee217c198f2e41f184f3869f3e485557296d505b5195c513b2bfe0062dc537f1",
95
- "sha256:f02541ef64077f22bf4924f225c0fd1248c168f86e4b7abdedd87d6ebaceab0f",
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.0.0"
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,montage,sharpen,watermark,env} ...
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,montage,sharpen,watermark,env}
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] IMAGE_FILENAMES [IMAGE_FILENAMES ...]
209
+ usage: fotolab rotate [-h] [-r ROTATION] [-cw]
210
+ IMAGE_FILENAMES [IMAGE_FILENAMES ...]
209
211
 
210
212
  positional arguments:
211
- IMAGE_FILENAMES set the image filenames
213
+ IMAGE_FILENAMES set the image filenames
212
214
 
213
215
  options:
214
- -h, --help show this help message and exit
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.22.0"
24
+ __version__ = "0.25.1"
25
25
 
26
26
  log = logging.getLogger(__name__)
27
27
 
@@ -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
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 Dict, Optional, Sequence
27
-
28
- import fotolab.animate
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
- """Set up logging by level."""
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
- else:
52
- conf: Dict = {
53
- True: {
54
- "level": logging.DEBUG,
55
- "msg": "[%(asctime)s] %(levelname)s: %(name)s: %(message)s",
56
- },
57
- False: {"level": logging.INFO, "msg": "%(message)s"},
58
- }
59
-
60
- logging.basicConfig(
61
- level=conf[args.debug]["level"],
62
- stream=sys.stdout,
63
- format=conf[args.debug]["msg"],
64
- datefmt="%Y-%m-%d %H:%M:%S",
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.animate.build_subparser(subparsers)
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
@@ -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
- if (
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, "watermark")
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
@@ -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
- print(
47
+ env = [
48
48
  f"fotolab: {__version__}",
49
49
  f"python: {sys_version}",
50
50
  f"platform: {platform.platform()}",
51
- sep="\n",
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
- print(camera_metadata(args.image_filename))
81
+ info.append(camera_metadata(image))
82
+
78
83
  if args.datetime:
79
- print(datetime(args.image_filename))
84
+ info.append(datetime(image))
85
+
86
+ if info:
87
+ print("\n".join(info))
80
88
  else:
81
- exif_tags = extract_exif_tags(args.image_filename)
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(image_filename: str, sort: bool = False) -> dict:
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(image_filename):
116
+ def datetime(image: Image.Image):
110
117
  """Extract datetime metadata."""
111
- exif_tags = extract_exif_tags(image_filename)
118
+ exif_tags = extract_exif_tags(image)
112
119
  return exif_tags["DateTime"]
113
120
 
114
121
 
115
- def camera_metadata(image_filename):
122
+ def camera_metadata(image: Image.Image):
116
123
  """Extract camera and model metadata."""
117
- exif_tags = extract_exif_tags(image_filename)
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 img in images:
67
- montaged_image.paste(img, (x_offset, 0))
68
- x_offset += img.width
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
@@ -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
- 180,
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
- save_image(args, sharpen_image, image_filename, "sharpen")
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
- watermarked_image = watermark_image(image_filename, args)
161
- save_image(args, watermarked_image, image_filename, "watermark")
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 watermark_image(image_filename, args):
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(image_filename):
176
- text = camera_metadata(image_filename)
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
- (position_x, position_y) = (0, 0)
241
-
242
- if position == "top-left":
243
- position_x = 0 + padding
244
- position_y = 0 + padding
245
- elif position == "top-right":
246
- position_x = image.width - text_width - padding
247
- position_y = 0 + padding
248
- elif position == "bottom-left":
249
- position_x = 0 + padding
250
- position_y = image.height - text_height - padding
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))
@@ -1,5 +0,0 @@
1
- 3.9.20
2
- 3.10.15
3
- 3.11.10
4
- 3.12.7
5
- 3.13.0
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