fotolab 0.21.1__tar.gz → 0.24.0__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.21.1 → fotolab-0.24.0}/.pre-commit-config.yaml +1 -1
  2. fotolab-0.24.0/.python-version +5 -0
  3. {fotolab-0.21.1 → fotolab-0.24.0}/CHANGELOG.md +30 -0
  4. {fotolab-0.21.1 → fotolab-0.24.0}/PKG-INFO +18 -11
  5. {fotolab-0.21.1 → fotolab-0.24.0}/Pipfile.lock +135 -101
  6. {fotolab-0.21.1 → fotolab-0.24.0}/README.md +17 -10
  7. {fotolab-0.21.1 → fotolab-0.24.0}/fotolab/__init__.py +1 -1
  8. {fotolab-0.21.1 → fotolab-0.24.0}/fotolab/cli.py +26 -42
  9. fotolab-0.24.0/fotolab/subcommands/__init__.py +32 -0
  10. {fotolab-0.21.1/fotolab → fotolab-0.24.0/fotolab/subcommands}/auto.py +8 -8
  11. {fotolab-0.21.1/fotolab → fotolab-0.24.0/fotolab/subcommands}/env.py +3 -3
  12. {fotolab-0.21.1/fotolab → fotolab-0.24.0/fotolab/subcommands}/info.py +30 -6
  13. {fotolab-0.21.1/fotolab → fotolab-0.24.0/fotolab/subcommands}/rotate.py +20 -1
  14. {fotolab-0.21.1/fotolab → fotolab-0.24.0/fotolab/subcommands}/watermark.py +79 -32
  15. {fotolab-0.21.1 → fotolab-0.24.0}/noxfile.py +28 -6
  16. fotolab-0.21.1/.python-version +0 -5
  17. {fotolab-0.21.1 → fotolab-0.24.0}/.coveragerc +0 -0
  18. {fotolab-0.21.1 → fotolab-0.24.0}/.gitignore +0 -0
  19. {fotolab-0.21.1 → fotolab-0.24.0}/CONTRIBUTING.md +0 -0
  20. {fotolab-0.21.1 → fotolab-0.24.0}/LICENSE.md +0 -0
  21. {fotolab-0.21.1 → fotolab-0.24.0}/Pipfile +0 -0
  22. {fotolab-0.21.1 → fotolab-0.24.0}/docs/Makefile +0 -0
  23. {fotolab-0.21.1 → fotolab-0.24.0}/docs/make.bat +0 -0
  24. {fotolab-0.21.1 → fotolab-0.24.0}/docs/source/CHANGELOG.md +0 -0
  25. {fotolab-0.21.1 → fotolab-0.24.0}/docs/source/CONTRIBUTING.md +0 -0
  26. {fotolab-0.21.1 → fotolab-0.24.0}/docs/source/LICENSE.md +0 -0
  27. {fotolab-0.21.1 → fotolab-0.24.0}/docs/source/README.md +0 -0
  28. {fotolab-0.21.1 → fotolab-0.24.0}/docs/source/_static/logo.jpg +0 -0
  29. {fotolab-0.21.1 → fotolab-0.24.0}/docs/source/conf.py +0 -0
  30. {fotolab-0.21.1 → fotolab-0.24.0}/docs/source/index.rst +0 -0
  31. {fotolab-0.21.1 → fotolab-0.24.0}/fotolab/__main__.py +0 -0
  32. {fotolab-0.21.1/fotolab → fotolab-0.24.0/fotolab/subcommands}/animate.py +0 -0
  33. {fotolab-0.21.1/fotolab → fotolab-0.24.0/fotolab/subcommands}/border.py +0 -0
  34. {fotolab-0.21.1/fotolab → fotolab-0.24.0/fotolab/subcommands}/contrast.py +0 -0
  35. {fotolab-0.21.1/fotolab → fotolab-0.24.0/fotolab/subcommands}/montage.py +0 -0
  36. {fotolab-0.21.1/fotolab → fotolab-0.24.0/fotolab/subcommands}/resize.py +0 -0
  37. {fotolab-0.21.1/fotolab → fotolab-0.24.0/fotolab/subcommands}/sharpen.py +0 -0
  38. {fotolab-0.21.1 → fotolab-0.24.0}/pyproject.toml +0 -0
  39. {fotolab-0.21.1 → fotolab-0.24.0}/tests/__init__.py +0 -0
  40. {fotolab-0.21.1 → fotolab-0.24.0}/tests/conftest.py +0 -0
  41. {fotolab-0.21.1 → fotolab-0.24.0}/tests/test_env.py +0 -0
  42. {fotolab-0.21.1 → fotolab-0.24.0}/tests/test_help_flag.py +0 -0
  43. {fotolab-0.21.1 → fotolab-0.24.0}/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.0
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,36 @@ and this project adheres to [0-based versioning](https://0ver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## v0.24.0 (2024-12-29)
11
+
12
+ - Bump `pre-commit` hook
13
+ - Pass image instead of filename when process `info` and `watermark` subcommand
14
+ - Refactor print result of `env` subcommand
15
+ - Support watermarking `gif` image
16
+
17
+ ## v0.23.0 (2024-12-22)
18
+
19
+ - Add `-cw` or `--clockwise` option to `rotate` subcommand
20
+ - Add `-r` or `--rotation` option to `rotate` subcommand
21
+ - Refactor calculation of position of watermark
22
+ - Refactor setup logging again
23
+ - Use major Python versions for `pyenv`
24
+
25
+ ## v0.22.1 (2024-12-15)
26
+
27
+ - Fix show all EXIF tags instead of selected fields
28
+ - Refactor building subparser for each subcommand
29
+ - Refactor setup logging
30
+
31
+ ## v0.22.0 (2024-12-08)
32
+
33
+ - Add `--datetime` to `info` subcommand
34
+ - Add `--no-lowercase` to `watermark` subcommand
35
+ - Bump deps
36
+ - Support major, minor, micro release for `release` job in `nox`
37
+ - Update help message in readme
38
+ - Use `camera_data` from `info` subcommand in `watermark` subcommand
39
+
10
40
  ## v0.21.1 (2024-12-01)
11
41
 
12
42
  - Add requires-python field to project
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fotolab
3
- Version: 0.21.1
3
+ Version: 0.24.0
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] 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
@@ -214,6 +214,8 @@ positional arguments:
214
214
  options:
215
215
  -h, --help show this help message and exit
216
216
  -s, --sort show image info by sorted field name
217
+ --camera show the camera maker details
218
+ --datetime show the datetime
217
219
  ```
218
220
 
219
221
  <!--help-info !-->
@@ -227,13 +229,17 @@ fotolab rotate -h
227
229
  <!--help-rotate !-->
228
230
 
229
231
  ```console
230
- usage: fotolab rotate [-h] IMAGE_FILENAMES [IMAGE_FILENAMES ...]
232
+ usage: fotolab rotate [-h] [-r ROTATION] [-cw]
233
+ IMAGE_FILENAMES [IMAGE_FILENAMES ...]
231
234
 
232
235
  positional arguments:
233
- IMAGE_FILENAMES set the image filenames
236
+ IMAGE_FILENAMES set the image filenames
234
237
 
235
238
  options:
236
- -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)
237
243
  ```
238
244
 
239
245
  <!--help-rotate !-->
@@ -326,7 +332,7 @@ usage: fotolab watermark [-h] [-t WATERMARK_TEXT]
326
332
  [-p {top-left,top-right,bottom-left,bottom-right}]
327
333
  [-pd PADDING] [-fs FONT_SIZE] [-fc FONT_COLOR]
328
334
  [-ow OUTLINE_WIDTH] [-oc OUTLINE_COLOR] [--camera]
329
- [-l]
335
+ [-l | --lowercase | --no-lowercase]
330
336
  IMAGE_FILENAMES [IMAGE_FILENAMES ...]
331
337
 
332
338
  positional arguments:
@@ -354,7 +360,8 @@ options:
354
360
  set the outline color of the watermark text (default:
355
361
  'black')
356
362
  --camera use camera metadata as watermark
357
- -l, --lowercase lowercase the watermark text
363
+ -l, --lowercase, --no-lowercase
364
+ lowercase the watermark text
358
365
  ```
359
366
 
360
367
  <!--help-watermark !-->
@@ -16,6 +16,10 @@
16
16
  ]
17
17
  },
18
18
  "default": {
19
+ "fotolab": {
20
+ "file": ".",
21
+ "markers": "python_version >= '3.9'"
22
+ },
19
23
  "pillow": {
20
24
  "hashes": [
21
25
  "sha256:00177a63030d612148e659b55ba99527803288cea7c75fb05766ab7981a8c1b7",
@@ -102,11 +106,11 @@
102
106
  "develop": {
103
107
  "alabaster": {
104
108
  "hashes": [
105
- "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e",
106
- "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b"
109
+ "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65",
110
+ "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92"
107
111
  ],
108
- "markers": "python_version >= '3.10'",
109
- "version": "==1.0.0"
112
+ "markers": "python_version >= '3.9'",
113
+ "version": "==0.7.16"
110
114
  },
111
115
  "argcomplete": {
112
116
  "hashes": [
@@ -272,71 +276,71 @@
272
276
  "toml"
273
277
  ],
274
278
  "hashes": [
275
- "sha256:00a1d69c112ff5149cabe60d2e2ee948752c975d95f1e1096742e6077affd376",
276
- "sha256:023bf8ee3ec6d35af9c1c6ccc1d18fa69afa1cb29eaac57cb064dbb262a517f9",
277
- "sha256:0294ca37f1ba500667b1aef631e48d875ced93ad5e06fa665a3295bdd1d95111",
278
- "sha256:06babbb8f4e74b063dbaeb74ad68dfce9186c595a15f11f5d5683f748fa1d172",
279
- "sha256:0809082ee480bb8f7416507538243c8863ac74fd8a5d2485c46f0f7499f2b491",
280
- "sha256:0b3fb02fe73bed561fa12d279a417b432e5b50fe03e8d663d61b3d5990f29546",
281
- "sha256:0b58c672d14f16ed92a48db984612f5ce3836ae7d72cdd161001cc54512571f2",
282
- "sha256:0bcd1069e710600e8e4cf27f65c90c7843fa8edfb4520fb0ccb88894cad08b11",
283
- "sha256:1032e178b76a4e2b5b32e19d0fd0abbce4b58e77a1ca695820d10e491fa32b08",
284
- "sha256:11a223a14e91a4693d2d0755c7a043db43d96a7450b4f356d506c2562c48642c",
285
- "sha256:12394842a3a8affa3ba62b0d4ab7e9e210c5e366fbac3e8b2a68636fb19892c2",
286
- "sha256:182e6cd5c040cec0a1c8d415a87b67ed01193ed9ad458ee427741c7d8513d963",
287
- "sha256:1d5b8007f81b88696d06f7df0cb9af0d3b835fe0c8dbf489bad70b45f0e45613",
288
- "sha256:1f76846299ba5c54d12c91d776d9605ae33f8ae2b9d1d3c3703cf2db1a67f2c0",
289
- "sha256:27fb4a050aaf18772db513091c9c13f6cb94ed40eacdef8dad8411d92d9992db",
290
- "sha256:29155cd511ee058e260db648b6182c419422a0d2e9a4fa44501898cf918866cf",
291
- "sha256:29fc0f17b1d3fea332f8001d4558f8214af7f1d87a345f3a133c901d60347c73",
292
- "sha256:2b6b4c83d8e8ea79f27ab80778c19bc037759aea298da4b56621f4474ffeb117",
293
- "sha256:2fdef0d83a2d08d69b1f2210a93c416d54e14d9eb398f6ab2f0a209433db19e1",
294
- "sha256:3c65d37f3a9ebb703e710befdc489a38683a5b152242664b973a7b7b22348a4e",
295
- "sha256:4f704f0998911abf728a7783799444fcbbe8261c4a6c166f667937ae6a8aa522",
296
- "sha256:51b44306032045b383a7a8a2c13878de375117946d68dcb54308111f39775a25",
297
- "sha256:53d202fd109416ce011578f321460795abfe10bb901b883cafd9b3ef851bacfc",
298
- "sha256:58809e238a8a12a625c70450b48e8767cff9eb67c62e6154a642b21ddf79baea",
299
- "sha256:5915fcdec0e54ee229926868e9b08586376cae1f5faa9bbaf8faf3561b393d52",
300
- "sha256:5beb1ee382ad32afe424097de57134175fea3faf847b9af002cc7895be4e2a5a",
301
- "sha256:5f8ae553cba74085db385d489c7a792ad66f7f9ba2ee85bfa508aeb84cf0ba07",
302
- "sha256:5fbd612f8a091954a0c8dd4c0b571b973487277d26476f8480bfa4b2a65b5d06",
303
- "sha256:6bd818b7ea14bc6e1f06e241e8234508b21edf1b242d49831831a9450e2f35fa",
304
- "sha256:6f01ba56b1c0e9d149f9ac85a2f999724895229eb36bd997b61e62999e9b0901",
305
- "sha256:73d2b73584446e66ee633eaad1a56aad577c077f46c35ca3283cd687b7715b0b",
306
- "sha256:7bb92c539a624cf86296dd0c68cd5cc286c9eef2d0c3b8b192b604ce9de20a17",
307
- "sha256:8165b796df0bd42e10527a3f493c592ba494f16ef3c8b531288e3d0d72c1f6f0",
308
- "sha256:862264b12ebb65ad8d863d51f17758b1684560b66ab02770d4f0baf2ff75da21",
309
- "sha256:8902dd6a30173d4ef09954bfcb24b5d7b5190cf14a43170e386979651e09ba19",
310
- "sha256:8cf717ee42012be8c0cb205dbbf18ffa9003c4cbf4ad078db47b95e10748eec5",
311
- "sha256:8ed9281d1b52628e81393f5eaee24a45cbd64965f41857559c2b7ff19385df51",
312
- "sha256:99b41d18e6b2a48ba949418db48159d7a2e81c5cc290fc934b7d2380515bd0e3",
313
- "sha256:9cb7fa111d21a6b55cbf633039f7bc2749e74932e3aa7cb7333f675a58a58bf3",
314
- "sha256:a181e99301a0ae128493a24cfe5cfb5b488c4e0bf2f8702091473d033494d04f",
315
- "sha256:a413a096c4cbac202433c850ee43fa326d2e871b24554da8327b01632673a076",
316
- "sha256:a6b1e54712ba3474f34b7ef7a41e65bd9037ad47916ccb1cc78769bae324c01a",
317
- "sha256:ade3ca1e5f0ff46b678b66201f7ff477e8fa11fb537f3b55c3f0568fbfe6e718",
318
- "sha256:b0ac3d42cb51c4b12df9c5f0dd2f13a4f24f01943627120ec4d293c9181219ba",
319
- "sha256:b369ead6527d025a0fe7bd3864e46dbee3aa8f652d48df6174f8d0bac9e26e0e",
320
- "sha256:b57b768feb866f44eeed9f46975f3d6406380275c5ddfe22f531a2bf187eda27",
321
- "sha256:b8d3a03d9bfcaf5b0141d07a88456bb6a4c3ce55c080712fec8418ef3610230e",
322
- "sha256:bc66f0bf1d7730a17430a50163bb264ba9ded56739112368ba985ddaa9c3bd09",
323
- "sha256:bf20494da9653f6410213424f5f8ad0ed885e01f7e8e59811f572bdb20b8972e",
324
- "sha256:c48167910a8f644671de9f2083a23630fbf7a1cb70ce939440cd3328e0919f70",
325
- "sha256:c481b47f6b5845064c65a7bc78bc0860e635a9b055af0df46fdf1c58cebf8e8f",
326
- "sha256:c7c8b95bf47db6d19096a5e052ffca0a05f335bc63cef281a6e8fe864d450a72",
327
- "sha256:c9b8e184898ed014884ca84c70562b4a82cbc63b044d366fedc68bc2b2f3394a",
328
- "sha256:cc8ff50b50ce532de2fa7a7daae9dd12f0a699bfcd47f20945364e5c31799fef",
329
- "sha256:d541423cdd416b78626b55f123412fcf979d22a2c39fce251b350de38c15c15b",
330
- "sha256:dab4d16dfef34b185032580e2f2f89253d302facba093d5fa9dbe04f569c4f4b",
331
- "sha256:dacbc52de979f2823a819571f2e3a350a7e36b8cb7484cdb1e289bceaf35305f",
332
- "sha256:df57bdbeffe694e7842092c5e2e0bc80fff7f43379d465f932ef36f027179806",
333
- "sha256:ed8fe9189d2beb6edc14d3ad19800626e1d9f2d975e436f84e19efb7fa19469b",
334
- "sha256:f3ddf056d3ebcf6ce47bdaf56142af51bb7fad09e4af310241e9db7a3a8022e1",
335
- "sha256:f8fe4984b431f8621ca53d9380901f62bfb54ff759a1348cd140490ada7b693c",
336
- "sha256:fe439416eb6380de434886b00c859304338f8b19f6f54811984f3420a2e03858"
279
+ "sha256:093896e530c38c8e9c996901858ac63f3d4171268db2c9c8b373a228f459bbc5",
280
+ "sha256:09b9f848b28081e7b975a3626e9081574a7b9196cde26604540582da60235fdf",
281
+ "sha256:0b0c69f4f724c64dfbfe79f5dfb503b42fe6127b8d479b2677f2b227478db2eb",
282
+ "sha256:13618bed0c38acc418896005732e565b317aa9e98d855a0e9f211a7ffc2d6638",
283
+ "sha256:13690e923a3932e4fad4c0ebfb9cb5988e03d9dcb4c5150b5fcbf58fd8bddfc4",
284
+ "sha256:177f01eeaa3aee4a5ffb0d1439c5952b53d5010f86e9d2667963e632e30082cc",
285
+ "sha256:193e3bffca48ad74b8c764fb4492dd875038a2f9925530cb094db92bb5e47bed",
286
+ "sha256:1defe91d41ce1bd44b40fabf071e6a01a5aa14de4a31b986aa9dfd1b3e3e414a",
287
+ "sha256:1f188a2402f8359cf0c4b1fe89eea40dc13b52e7b4fd4812450da9fcd210181d",
288
+ "sha256:202a2d645c5a46b84992f55b0a3affe4f0ba6b4c611abec32ee88358db4bb649",
289
+ "sha256:24eda3a24a38157eee639ca9afe45eefa8d2420d49468819ac5f88b10de84f4c",
290
+ "sha256:2e4e0f60cb4bd7396108823548e82fdab72d4d8a65e58e2c19bbbc2f1e2bfa4b",
291
+ "sha256:379c111d3558272a2cae3d8e57e6b6e6f4fe652905692d54bad5ea0ca37c5ad4",
292
+ "sha256:37cda8712145917105e07aab96388ae76e787270ec04bcb9d5cc786d7cbb8443",
293
+ "sha256:38c51297b35b3ed91670e1e4efb702b790002e3245a28c76e627478aa3c10d83",
294
+ "sha256:3985b9be361d8fb6b2d1adc9924d01dec575a1d7453a14cccd73225cb79243ee",
295
+ "sha256:3988665ee376abce49613701336544041f2117de7b7fbfe91b93d8ff8b151c8e",
296
+ "sha256:3ac47fa29d8d41059ea3df65bd3ade92f97ee4910ed638e87075b8e8ce69599e",
297
+ "sha256:3b4b4299dd0d2c67caaaf286d58aef5e75b125b95615dda4542561a5a566a1e3",
298
+ "sha256:3ea8bb1ab9558374c0ab591783808511d135a833c3ca64a18ec927f20c4030f0",
299
+ "sha256:3fe47da3e4fda5f1abb5709c156eca207eacf8007304ce3019eb001e7a7204cb",
300
+ "sha256:428ac484592f780e8cd7b6b14eb568f7c85460c92e2a37cb0c0e5186e1a0d076",
301
+ "sha256:44e6c85bbdc809383b509d732b06419fb4544dca29ebe18480379633623baafb",
302
+ "sha256:4674f0daa1823c295845b6a740d98a840d7a1c11df00d1fd62614545c1583787",
303
+ "sha256:4be32da0c3827ac9132bb488d331cb32e8d9638dd41a0557c5569d57cf22c9c1",
304
+ "sha256:4db3ed6a907b555e57cc2e6f14dc3a4c2458cdad8919e40b5357ab9b6db6c43e",
305
+ "sha256:5c52a036535d12590c32c49209e79cabaad9f9ad8aa4cbd875b68c4d67a9cbce",
306
+ "sha256:629a1ba2115dce8bf75a5cce9f2486ae483cb89c0145795603d6554bdc83e801",
307
+ "sha256:62a66ff235e4c2e37ed3b6104d8b478d767ff73838d1222132a7a026aa548764",
308
+ "sha256:63068a11171e4276f6ece913bde059e77c713b48c3a848814a6537f35afb8365",
309
+ "sha256:63c19702db10ad79151a059d2d6336fe0c470f2e18d0d4d1a57f7f9713875dcf",
310
+ "sha256:644ec81edec0f4ad17d51c838a7d01e42811054543b76d4ba2c5d6af741ce2a6",
311
+ "sha256:6535d996f6537ecb298b4e287a855f37deaf64ff007162ec0afb9ab8ba3b8b71",
312
+ "sha256:6f4548c5ead23ad13fb7a2c8ea541357474ec13c2b736feb02e19a3085fac002",
313
+ "sha256:716a78a342679cd1177bc8c2fe957e0ab91405bd43a17094324845200b2fddf4",
314
+ "sha256:74610105ebd6f33d7c10f8907afed696e79c59e3043c5f20eaa3a46fddf33b4c",
315
+ "sha256:768939f7c4353c0fac2f7c37897e10b1414b571fd85dd9fc49e6a87e37a2e0d8",
316
+ "sha256:86cffe9c6dfcfe22e28027069725c7f57f4b868a3f86e81d1c62462764dc46d4",
317
+ "sha256:8aae5aea53cbfe024919715eca696b1a3201886ce83790537d1c3668459c7146",
318
+ "sha256:8b2b8503edb06822c86d82fa64a4a5cb0760bb8f31f26e138ec743f422f37cfc",
319
+ "sha256:912e95017ff51dc3d7b6e2be158dedc889d9a5cc3382445589ce554f1a34c0ea",
320
+ "sha256:9a7b8ac36fd688c8361cbc7bf1cb5866977ece6e0b17c34aa0df58bda4fa18a4",
321
+ "sha256:9e89d5c8509fbd6c03d0dd1972925b22f50db0792ce06324ba069f10787429ad",
322
+ "sha256:ae270e79f7e169ccfe23284ff5ea2d52a6f401dc01b337efb54b3783e2ce3f28",
323
+ "sha256:b07c25d52b1c16ce5de088046cd2432b30f9ad5e224ff17c8f496d9cb7d1d451",
324
+ "sha256:b39e6011cd06822eb964d038d5dff5da5d98652b81f5ecd439277b32361a3a50",
325
+ "sha256:bd55f8fc8fa494958772a2a7302b0354ab16e0b9272b3c3d83cdb5bec5bd1779",
326
+ "sha256:c15b32a7aca8038ed7644f854bf17b663bc38e1671b5d6f43f9a2b2bd0c46f63",
327
+ "sha256:c1b4474beee02ede1eef86c25ad4600a424fe36cff01a6103cb4533c6bf0169e",
328
+ "sha256:c79c0685f142ca53256722a384540832420dff4ab15fec1863d7e5bc8691bdcc",
329
+ "sha256:c9ebfb2507751f7196995142f057d1324afdab56db1d9743aab7f50289abd022",
330
+ "sha256:d7ad66e8e50225ebf4236368cc43c37f59d5e6728f15f6e258c8639fa0dd8e6d",
331
+ "sha256:d82ab6816c3277dc962cfcdc85b1efa0e5f50fb2c449432deaf2398a2928ab94",
332
+ "sha256:d9fd2547e6decdbf985d579cf3fc78e4c1d662b9b0ff7cc7862baaab71c9cc5b",
333
+ "sha256:de38add67a0af869b0d79c525d3e4588ac1ffa92f39116dbe0ed9753f26eba7d",
334
+ "sha256:e19122296822deafce89a0c5e8685704c067ae65d45e79718c92df7b3ec3d331",
335
+ "sha256:e44961e36cb13c495806d4cac67640ac2866cb99044e210895b506c26ee63d3a",
336
+ "sha256:e4c81ed2820b9023a9a90717020315e63b17b18c274a332e3b6437d7ff70abe0",
337
+ "sha256:e683e6ecc587643f8cde8f5da6768e9d165cd31edf39ee90ed7034f9ca0eefee",
338
+ "sha256:f39e2f3530ed1626c66e7493be7a8423b023ca852aacdc91fb30162c350d2a92",
339
+ "sha256:f56f49b2553d7dd85fd86e029515a221e5c1f8cb3d9c38b470bc38bde7b8445a",
340
+ "sha256:fb9fc32399dca861584d96eccd6c980b69bbcd7c228d06fb74fe53e007aa8ef9"
337
341
  ],
338
342
  "markers": "python_version >= '3.9'",
339
- "version": "==7.6.4"
343
+ "version": "==7.6.8"
340
344
  },
341
345
  "distlib": {
342
346
  "hashes": [
@@ -431,16 +435,16 @@
431
435
  "version": "==0.21.0"
432
436
  },
433
437
  "fotolab": {
434
- "editable": true,
435
- "path": "."
438
+ "file": ".",
439
+ "markers": "python_version >= '3.9'"
436
440
  },
437
441
  "identify": {
438
442
  "hashes": [
439
- "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0",
440
- "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98"
443
+ "sha256:62f5dae9b5fef52c84cc188514e9ea4f3f636b1d8799ab5ebc475471f9e47a02",
444
+ "sha256:9edba65473324c2ea9684b1f944fe3191db3345e50b6d04571d10ed164f8d7bd"
441
445
  ],
442
- "markers": "python_version >= '3.8'",
443
- "version": "==2.6.1"
446
+ "markers": "python_version >= '3.9'",
447
+ "version": "==2.6.3"
444
448
  },
445
449
  "idna": {
446
450
  "hashes": [
@@ -584,12 +588,12 @@
584
588
  },
585
589
  "myst-parser": {
586
590
  "hashes": [
587
- "sha256:851c9dfb44e36e56d15d05e72f02b80da21a9e0d07cba96baf5e2d476bb91531",
588
- "sha256:b9317997552424448c6096c2558872fdb6f81d3ecb3a40ce84a7518798f3f28d"
591
+ "sha256:6457aaa33a5d474aca678b8ead9b3dc298e89c68e67012e73146ea6fd54babf1",
592
+ "sha256:88f0cb406cb363b077d176b51c476f62d60604d68a8dcdf4832e080441301a87"
589
593
  ],
590
594
  "index": "pypi",
591
- "markers": "python_version >= '3.10'",
592
- "version": "==4.0.0"
595
+ "markers": "python_version >= '3.8'",
596
+ "version": "==3.0.1"
593
597
  },
594
598
  "nodeenv": {
595
599
  "hashes": [
@@ -610,11 +614,11 @@
610
614
  },
611
615
  "packaging": {
612
616
  "hashes": [
613
- "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002",
614
- "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"
617
+ "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759",
618
+ "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"
615
619
  ],
616
620
  "markers": "python_version >= '3.8'",
617
- "version": "==24.1"
621
+ "version": "==24.2"
618
622
  },
619
623
  "pillow": {
620
624
  "hashes": [
@@ -757,12 +761,12 @@
757
761
  },
758
762
  "pytest": {
759
763
  "hashes": [
760
- "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181",
761
- "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"
764
+ "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6",
765
+ "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"
762
766
  ],
763
767
  "index": "pypi",
764
768
  "markers": "python_version >= '3.8'",
765
- "version": "==8.3.3"
769
+ "version": "==8.3.4"
766
770
  },
767
771
  "pytest-cov": {
768
772
  "hashes": [
@@ -874,21 +878,21 @@
874
878
  },
875
879
  "sphinx": {
876
880
  "hashes": [
877
- "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2",
878
- "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927"
881
+ "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe",
882
+ "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239"
879
883
  ],
880
884
  "index": "pypi",
881
- "markers": "python_version >= '3.10'",
882
- "version": "==8.1.3"
885
+ "markers": "python_version >= '3.9'",
886
+ "version": "==7.4.7"
883
887
  },
884
888
  "sphinx-autodoc-typehints": {
885
889
  "hashes": [
886
- "sha256:259e1026b218d563d72743f417fcc25906a9614897fe37f91bd8d7d58f748c3b",
887
- "sha256:53def4753239683835b19bfa8b68c021388bd48a096efcb02cdab508ece27363"
890
+ "sha256:3098e2c6d0ba99eacd013eb06861acc9b51c6e595be86ab05c08ee5506ac0c67",
891
+ "sha256:535c78ed2d6a1bad393ba9f3dfa2602cf424e2631ee207263e07874c38fde084"
888
892
  ],
889
893
  "index": "pypi",
890
- "markers": "python_version >= '3.10'",
891
- "version": "==2.5.0"
894
+ "markers": "python_version >= '3.9'",
895
+ "version": "==2.3.0"
892
896
  },
893
897
  "sphinx-copybutton": {
894
898
  "hashes": [
@@ -949,12 +953,42 @@
949
953
  },
950
954
  "tomli": {
951
955
  "hashes": [
952
- "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38",
953
- "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"
956
+ "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6",
957
+ "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd",
958
+ "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c",
959
+ "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b",
960
+ "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8",
961
+ "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6",
962
+ "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77",
963
+ "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff",
964
+ "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea",
965
+ "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192",
966
+ "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249",
967
+ "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee",
968
+ "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4",
969
+ "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98",
970
+ "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8",
971
+ "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4",
972
+ "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281",
973
+ "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744",
974
+ "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69",
975
+ "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13",
976
+ "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140",
977
+ "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e",
978
+ "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e",
979
+ "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc",
980
+ "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff",
981
+ "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec",
982
+ "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2",
983
+ "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222",
984
+ "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106",
985
+ "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272",
986
+ "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a",
987
+ "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"
954
988
  ],
955
989
  "index": "pypi",
956
990
  "markers": "python_version >= '3.8'",
957
- "version": "==2.0.2"
991
+ "version": "==2.2.1"
958
992
  },
959
993
  "urllib3": {
960
994
  "hashes": [
@@ -966,19 +1000,19 @@
966
1000
  },
967
1001
  "virtualenv": {
968
1002
  "hashes": [
969
- "sha256:142c6be10212543b32c6c45d3d3893dff89112cc588b7d0879ae5a1ec03a47ba",
970
- "sha256:f11f1b8a29525562925f745563bfd48b189450f61fb34c4f9cc79dd5aa32a1f4"
1003
+ "sha256:23eae1b4516ecd610481eda647f3a7c09aea295055337331bb4e6892ecce47b0",
1004
+ "sha256:2c9c3262bb8e7b87ea801d715fae4495e6032450c71d2309be9550e7364049aa"
971
1005
  ],
972
1006
  "markers": "python_version >= '3.8'",
973
- "version": "==20.27.1"
1007
+ "version": "==20.28.0"
974
1008
  },
975
1009
  "zipp": {
976
1010
  "hashes": [
977
- "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350",
978
- "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29"
1011
+ "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4",
1012
+ "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"
979
1013
  ],
980
- "markers": "python_version >= '3.8'",
981
- "version": "==3.20.2"
1014
+ "markers": "python_version >= '3.9'",
1015
+ "version": "==3.21.0"
982
1016
  }
983
1017
  }
984
1018
  }
@@ -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] 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
@@ -191,6 +191,8 @@ positional arguments:
191
191
  options:
192
192
  -h, --help show this help message and exit
193
193
  -s, --sort show image info by sorted field name
194
+ --camera show the camera maker details
195
+ --datetime show the datetime
194
196
  ```
195
197
 
196
198
  <!--help-info !-->
@@ -204,13 +206,17 @@ fotolab rotate -h
204
206
  <!--help-rotate !-->
205
207
 
206
208
  ```console
207
- usage: fotolab rotate [-h] IMAGE_FILENAMES [IMAGE_FILENAMES ...]
209
+ usage: fotolab rotate [-h] [-r ROTATION] [-cw]
210
+ IMAGE_FILENAMES [IMAGE_FILENAMES ...]
208
211
 
209
212
  positional arguments:
210
- IMAGE_FILENAMES set the image filenames
213
+ IMAGE_FILENAMES set the image filenames
211
214
 
212
215
  options:
213
- -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)
214
220
  ```
215
221
 
216
222
  <!--help-rotate !-->
@@ -303,7 +309,7 @@ usage: fotolab watermark [-h] [-t WATERMARK_TEXT]
303
309
  [-p {top-left,top-right,bottom-left,bottom-right}]
304
310
  [-pd PADDING] [-fs FONT_SIZE] [-fc FONT_COLOR]
305
311
  [-ow OUTLINE_WIDTH] [-oc OUTLINE_COLOR] [--camera]
306
- [-l]
312
+ [-l | --lowercase | --no-lowercase]
307
313
  IMAGE_FILENAMES [IMAGE_FILENAMES ...]
308
314
 
309
315
  positional arguments:
@@ -331,7 +337,8 @@ options:
331
337
  set the outline color of the watermark text (default:
332
338
  'black')
333
339
  --camera use camera metadata as watermark
334
- -l, --lowercase lowercase the watermark text
340
+ -l, --lowercase, --no-lowercase
341
+ lowercase the watermark text
335
342
  ```
336
343
 
337
344
  <!--help-watermark !-->
@@ -21,7 +21,7 @@ import subprocess
21
21
  import sys
22
22
  from pathlib import Path
23
23
 
24
- __version__ = "0.21.1"
24
+ __version__ = "0.24.0"
25
25
 
26
26
  log = logging.getLogger(__name__)
27
27
 
@@ -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 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)
@@ -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
 
@@ -77,7 +77,7 @@ def run(args: argparse.Namespace) -> None:
77
77
  log.debug(args)
78
78
  log.debug(combined_args)
79
79
 
80
- fotolab.resize.run(combined_args)
81
- fotolab.contrast.run(combined_args)
82
- fotolab.sharpen.run(combined_args)
83
- fotolab.watermark.run(combined_args)
80
+ fotolab.subcommands.resize.run(combined_args)
81
+ fotolab.subcommands.contrast.run(combined_args)
82
+ fotolab.subcommands.sharpen.run(combined_args)
83
+ fotolab.subcommands.watermark.run(combined_args)
@@ -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")
@@ -54,6 +54,14 @@ def build_subparser(subparsers) -> None:
54
54
  help="show the camera maker details",
55
55
  )
56
56
 
57
+ info_parser.add_argument(
58
+ "--datetime",
59
+ default=False,
60
+ action="store_true",
61
+ dest="datetime",
62
+ help="show the datetime",
63
+ )
64
+
57
65
 
58
66
  def run(args: argparse.Namespace) -> None:
59
67
  """Run info subcommand.
@@ -65,10 +73,20 @@ def run(args: argparse.Namespace) -> None:
65
73
  None
66
74
  """
67
75
  log.debug(args)
76
+
77
+ info = []
78
+ image = Image.open(args.image_filename)
79
+
68
80
  if args.camera:
69
- print(camera_metadata(args.image_filename))
81
+ info.append(camera_metadata(image))
82
+
83
+ if args.datetime:
84
+ info.append(datetime(image))
85
+
86
+ if info:
87
+ print("\n".join(info))
70
88
  else:
71
- exif_tags = extract_exif_tags(args.image_filename)
89
+ exif_tags = extract_exif_tags(image)
72
90
  if exif_tags:
73
91
  tag_name_width = max(map(len, exif_tags))
74
92
  for tag_name, tag_value in exif_tags.items():
@@ -77,9 +95,9 @@ def run(args: argparse.Namespace) -> None:
77
95
  print("No metadata found!")
78
96
 
79
97
 
80
- def extract_exif_tags(image_filename: str, sort: bool = False) -> dict:
98
+ def extract_exif_tags(image: Image.Image, sort: bool = False) -> dict:
81
99
  """Extract Exif metadata from image."""
82
- image = Image.open(image_filename)
100
+ print(type(image))
83
101
  exif = image._getexif()
84
102
  log.debug(exif)
85
103
 
@@ -96,8 +114,14 @@ def extract_exif_tags(image_filename: str, sort: bool = False) -> dict:
96
114
  return filtered_info
97
115
 
98
116
 
99
- def camera_metadata(image_filename):
117
+ def datetime(image: Image.Image):
118
+ """Extract datetime metadata."""
119
+ exif_tags = extract_exif_tags(image)
120
+ return exif_tags["DateTime"]
121
+
122
+
123
+ def camera_metadata(image: Image.Image):
100
124
  """Extract camera and model metadata."""
101
- exif_tags = extract_exif_tags(image_filename)
125
+ exif_tags = extract_exif_tags(image)
102
126
  metadata = f'{exif_tags["Make"]} {exif_tags["Model"]}'
103
127
  return metadata.strip()
@@ -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")
@@ -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 extract_exif_tags
25
+ from fotolab.subcommands.info import camera_metadata
26
26
 
27
27
  log = logging.getLogger(__name__)
28
28
 
@@ -139,7 +139,7 @@ def build_subparser(subparsers) -> None:
139
139
  "-l",
140
140
  "--lowercase",
141
141
  default=True,
142
- action="store_true",
142
+ action=argparse.BooleanOptionalAction,
143
143
  dest="lowercase",
144
144
  help="lowercase the watermark text",
145
145
  )
@@ -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")
166
+
167
+
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()
162
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
+ )
163
220
 
164
- def watermark_image(image_filename, args):
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()
@@ -200,13 +259,6 @@ def watermark_image(image_filename, args):
200
259
  return watermarked_image
201
260
 
202
261
 
203
- def camera_metadata(image_filename):
204
- """Extract camera and model metadata."""
205
- exif_tags = extract_exif_tags(image_filename)
206
- metadata = f'{exif_tags["Make"]} {exif_tags["Model"]}'
207
- return metadata.strip()
208
-
209
-
210
262
  def calc_font_size(image, args) -> int:
211
263
  """Calculate the font size based on the width of the image."""
212
264
  width, _height = image.size
@@ -244,19 +296,14 @@ def calc_padding(image, args) -> int:
244
296
 
245
297
  def calc_position(image, text_width, text_height, position, padding) -> tuple:
246
298
  """Calculate the boundary coordinates of the watermark text."""
247
- (position_x, position_y) = (0, 0)
248
-
249
- if position == "top-left":
250
- position_x = 0 + padding
251
- position_y = 0 + padding
252
- elif position == "top-right":
253
- position_x = image.width - text_width - padding
254
- position_y = 0 + padding
255
- elif position == "bottom-left":
256
- position_x = 0 + padding
257
- position_y = image.height - text_height - padding
258
- elif position == "bottom-right":
259
- position_x = image.width - text_width - padding
260
- position_y = image.height - text_height - padding
261
-
262
- 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))
@@ -118,8 +118,15 @@ def readme(session: nox.Session) -> None:
118
118
 
119
119
 
120
120
  @nox.session(python="3.13", reuse_venv=True)
121
- def release(_session: nox.Session) -> None:
122
- """Bump release."""
121
+ def release(session: nox.Session) -> None:
122
+ """Bump release.
123
+
124
+ To set which part of version explicitly:
125
+
126
+ nox -s release -- major
127
+ nox -s release -- minor
128
+ nox -s release -- micro (default)
129
+ """
123
130
  with open("fotolab/__init__.py", "r", encoding="utf8") as f:
124
131
  tree = ast.parse(f.read())
125
132
  current_version = None
@@ -134,11 +141,26 @@ def release(_session: nox.Session) -> None:
134
141
  raise ValueError("Missing __version__ variable in __init__.py")
135
142
 
136
143
  before_version = Version(current_version)
137
- after_version = (
138
- f"{before_version.major}."
139
- f"{before_version.minor}."
140
- f"{before_version.micro + 1}"
144
+
145
+ (major, minor, micro) = (
146
+ before_version.major,
147
+ before_version.minor,
148
+ before_version.micro,
141
149
  )
150
+ if "major" in session.posargs:
151
+ major = major + 1
152
+ minor = 0
153
+ micro = 0
154
+
155
+ if "minor" in session.posargs:
156
+ minor = minor + 1
157
+ micro = 0
158
+
159
+ if "micro" in session.posargs or session.posargs == []:
160
+ micro = micro + 1
161
+
162
+ after_version = f"{major}.{minor}.{micro}"
163
+
142
164
  _search_and_replace(
143
165
  "fotolab/__init__.py", str(before_version), after_version
144
166
  )
@@ -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