fotolab 0.13.0__tar.gz → 0.15.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 (40) hide show
  1. {fotolab-0.13.0 → fotolab-0.15.0}/.pre-commit-config.yaml +2 -2
  2. {fotolab-0.13.0 → fotolab-0.15.0}/CHANGELOG.md +24 -0
  3. {fotolab-0.13.0 → fotolab-0.15.0}/PKG-INFO +29 -6
  4. {fotolab-0.13.0 → fotolab-0.15.0}/Pipfile.lock +78 -78
  5. {fotolab-0.13.0 → fotolab-0.15.0}/README.md +28 -5
  6. {fotolab-0.13.0 → fotolab-0.15.0}/fotolab/__init__.py +3 -3
  7. fotolab-0.15.0/fotolab/animate.py +114 -0
  8. {fotolab-0.13.0 → fotolab-0.15.0}/fotolab/cli.py +14 -0
  9. {fotolab-0.13.0 → fotolab-0.15.0}/fotolab/resize.py +39 -5
  10. {fotolab-0.13.0 → fotolab-0.15.0}/fotolab/sharpen.py +3 -3
  11. {fotolab-0.13.0 → fotolab-0.15.0}/fotolab/watermark.py +46 -7
  12. {fotolab-0.13.0 → fotolab-0.15.0}/.coveragerc +0 -0
  13. {fotolab-0.13.0 → fotolab-0.15.0}/.gitignore +0 -0
  14. {fotolab-0.13.0 → fotolab-0.15.0}/.python-version +0 -0
  15. {fotolab-0.13.0 → fotolab-0.15.0}/CONTRIBUTING.md +0 -0
  16. {fotolab-0.13.0 → fotolab-0.15.0}/LICENSE.md +0 -0
  17. {fotolab-0.13.0 → fotolab-0.15.0}/Pipfile +0 -0
  18. {fotolab-0.13.0 → fotolab-0.15.0}/docs/Makefile +0 -0
  19. {fotolab-0.13.0 → fotolab-0.15.0}/docs/make.bat +0 -0
  20. {fotolab-0.13.0 → fotolab-0.15.0}/docs/source/CHANGELOG.md +0 -0
  21. {fotolab-0.13.0 → fotolab-0.15.0}/docs/source/CONTRIBUTING.md +0 -0
  22. {fotolab-0.13.0 → fotolab-0.15.0}/docs/source/LICENSE.md +0 -0
  23. {fotolab-0.13.0 → fotolab-0.15.0}/docs/source/README.md +0 -0
  24. {fotolab-0.13.0 → fotolab-0.15.0}/docs/source/_static/logo.jpg +0 -0
  25. {fotolab-0.13.0 → fotolab-0.15.0}/docs/source/conf.py +0 -0
  26. {fotolab-0.13.0 → fotolab-0.15.0}/docs/source/index.rst +0 -0
  27. {fotolab-0.13.0 → fotolab-0.15.0}/fotolab/__main__.py +0 -0
  28. {fotolab-0.13.0 → fotolab-0.15.0}/fotolab/auto.py +0 -0
  29. {fotolab-0.13.0 → fotolab-0.15.0}/fotolab/border.py +0 -0
  30. {fotolab-0.13.0 → fotolab-0.15.0}/fotolab/contrast.py +0 -0
  31. {fotolab-0.13.0 → fotolab-0.15.0}/fotolab/env.py +0 -0
  32. {fotolab-0.13.0 → fotolab-0.15.0}/fotolab/info.py +0 -0
  33. {fotolab-0.13.0 → fotolab-0.15.0}/fotolab/montage.py +0 -0
  34. {fotolab-0.13.0 → fotolab-0.15.0}/pyproject.toml +0 -0
  35. {fotolab-0.13.0 → fotolab-0.15.0}/tests/__init__.py +0 -0
  36. {fotolab-0.13.0 → fotolab-0.15.0}/tests/conftest.py +0 -0
  37. {fotolab-0.13.0 → fotolab-0.15.0}/tests/test_env.py +0 -0
  38. {fotolab-0.13.0 → fotolab-0.15.0}/tests/test_help_flag.py +0 -0
  39. {fotolab-0.13.0 → fotolab-0.15.0}/tests/test_quiet_flag.py +0 -0
  40. {fotolab-0.13.0 → fotolab-0.15.0}/tox.ini +0 -0
@@ -45,7 +45,7 @@ repos:
45
45
  - --py=312
46
46
 
47
47
  - repo: https://github.com/psf/black
48
- rev: 24.4.0
48
+ rev: 24.4.2
49
49
  hooks:
50
50
  - id: black
51
51
  language_version: python3.12
@@ -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.9.0
110
+ rev: v1.10.0
111
111
  hooks:
112
112
  - id: mypy
113
113
  exclude: docs/
@@ -7,6 +7,30 @@ and this project adheres to [0-based versioning](https://0ver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## v0.15.0 (2024-05-05)
11
+
12
+ ### Added
13
+
14
+ - Add `-v` or `--verbose` flag to toggle the debug logs of PIL
15
+
16
+ ### Changed
17
+
18
+ - Bump deps and `pre-commit` hooks
19
+ - Resize image to the right aspect ratio based on image's dimension
20
+ - Resize watermark font based on the image's dimension
21
+ - Remove empty lines in help message
22
+ - Show absolute image path in logs
23
+
24
+ ## v0.14.0 (2024-04-28)
25
+
26
+ ### Added
27
+
28
+ - Add `animate` subcommand to create animated `gif` image
29
+
30
+ ### Changed
31
+
32
+ - Bump deps and `pre-commit` hooks
33
+
10
34
  ## v0.13.0 (2024-04-21)
11
35
 
12
36
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fotolab
3
- Version: 0.13.0
3
+ Version: 0.15.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>
@@ -57,8 +57,8 @@ fotolab -h
57
57
  ```
58
58
 
59
59
  ```console
60
- usage: fotolab [-h] [-o] [-op] [-od OUTPUT_DIR] [-q] [-d] [-V]
61
- {auto,border,contrast,info,resize,montage,sharpen,watermark,env}
60
+ usage: fotolab [-h] [-o] [-op] [-od OUTPUT_DIR] [-q] [-v] [-d] [-V]
61
+ {animate,auto,border,contrast,info,resize,montage,sharpen,watermark,env}
62
62
  ...
63
63
 
64
64
  A console program to manipulate photos.
@@ -68,8 +68,9 @@ A console program to manipulate photos.
68
68
  issues: https://github.com/kianmeng/fotolab/issues
69
69
 
70
70
  positional arguments:
71
- {auto,border,contrast,info,resize,montage,sharpen,watermark,env}
71
+ {animate,auto,border,contrast,info,resize,montage,sharpen,watermark,env}
72
72
  sub-command help
73
+ animate animate an image
73
74
  auto auto adjust (resize, contrast, and watermark) a photo
74
75
  border add border to image
75
76
  contrast contrast an image
@@ -87,10 +88,33 @@ optional arguments:
87
88
  -od OUTPUT_DIR, --output-dir OUTPUT_DIR
88
89
  set default output folder (default: 'output')
89
90
  -q, --quiet suppress all logging
91
+ -v, --verbose show verbosity of debugging log, use -vv, -vvv for more details
90
92
  -d, --debug show debugging log and stacktrace
91
93
  -V, --version show program's version number and exit
92
94
  ```
93
95
 
96
+ ### fotolab animate
97
+
98
+ ```console
99
+ fotolab animate -h
100
+ ```
101
+
102
+ ```console
103
+ usage: fotolab animate [-h] [-f FORMAT] [-d DURATION] [-l LOOP]
104
+ IMAGE_FILENAMES [IMAGE_FILENAMES ...]
105
+
106
+ positional arguments:
107
+ IMAGE_FILENAMES set the image filenames
108
+
109
+ optional arguments:
110
+ -h, --help show this help message and exit
111
+ -f FORMAT, --format FORMAT
112
+ set the image format (default: 'gif')
113
+ -d DURATION, --duration DURATION
114
+ set the duration in milliseconds (default: '2500')
115
+ -l LOOP, --loop LOOP set the loop cycle (default: '0')
116
+ ```
117
+
94
118
  ### fotolab auto
95
119
 
96
120
  ```console
@@ -179,7 +203,6 @@ fotolab montage -h
179
203
  ```
180
204
 
181
205
  ```console
182
-
183
206
  usage: fotolab montage [-h] IMAGE_FILENAMES [IMAGE_FILENAMES ...]
184
207
 
185
208
  positional arguments:
@@ -196,7 +219,7 @@ fotolab resize -h
196
219
  ```
197
220
 
198
221
  ```console
199
- usage: fotolab resize [-h] [-wh WIDTH] [-ht HEIGHT]
222
+ usage: fotolab resize [-h] [-wh WIDTH | -ht HEIGHT]
200
223
  IMAGE_FILENAMES [IMAGE_FILENAMES ...]
201
224
 
202
225
  positional arguments:
@@ -115,61 +115,61 @@
115
115
  "toml"
116
116
  ],
117
117
  "hashes": [
118
- "sha256:00838a35b882694afda09f85e469c96367daa3f3f2b097d846a7216993d37f4c",
119
- "sha256:0513b9508b93da4e1716744ef6ebc507aff016ba115ffe8ecff744d1322a7b63",
120
- "sha256:09c3255458533cb76ef55da8cc49ffab9e33f083739c8bd4f58e79fecfe288f7",
121
- "sha256:09ef9199ed6653989ebbcaacc9b62b514bb63ea2f90256e71fea3ed74bd8ff6f",
122
- "sha256:09fa497a8ab37784fbb20ab699c246053ac294d13fc7eb40ec007a5043ec91f8",
123
- "sha256:0f9f50e7ef2a71e2fae92774c99170eb8304e3fdf9c8c3c7ae9bab3e7229c5cf",
124
- "sha256:137eb07173141545e07403cca94ab625cc1cc6bc4c1e97b6e3846270e7e1fea0",
125
- "sha256:1f384c3cc76aeedce208643697fb3e8437604b512255de6d18dae3f27655a384",
126
- "sha256:201bef2eea65e0e9c56343115ba3814e896afe6d36ffd37bab783261db430f76",
127
- "sha256:38dd60d7bf242c4ed5b38e094baf6401faa114fc09e9e6632374388a404f98e7",
128
- "sha256:3b799445b9f7ee8bf299cfaed6f5b226c0037b74886a4e11515e569b36fe310d",
129
- "sha256:3ea79bb50e805cd6ac058dfa3b5c8f6c040cb87fe83de10845857f5535d1db70",
130
- "sha256:40209e141059b9370a2657c9b15607815359ab3ef9918f0196b6fccce8d3230f",
131
- "sha256:41c9c5f3de16b903b610d09650e5e27adbfa7f500302718c9ffd1c12cf9d6818",
132
- "sha256:54eb8d1bf7cacfbf2a3186019bcf01d11c666bd495ed18717162f7eb1e9dd00b",
133
- "sha256:598825b51b81c808cb6f078dcb972f96af96b078faa47af7dfcdf282835baa8d",
134
- "sha256:5fc1de20b2d4a061b3df27ab9b7c7111e9a710f10dc2b84d33a4ab25065994ec",
135
- "sha256:623512f8ba53c422fcfb2ce68362c97945095b864cda94a92edbaf5994201083",
136
- "sha256:690db6517f09336559dc0b5f55342df62370a48f5469fabf502db2c6d1cffcd2",
137
- "sha256:69eb372f7e2ece89f14751fbcbe470295d73ed41ecd37ca36ed2eb47512a6ab9",
138
- "sha256:73bfb9c09951125d06ee473bed216e2c3742f530fc5acc1383883125de76d9cd",
139
- "sha256:742a76a12aa45b44d236815d282b03cfb1de3b4323f3e4ec933acfae08e54ade",
140
- "sha256:7c95949560050d04d46b919301826525597f07b33beba6187d04fa64d47ac82e",
141
- "sha256:8130a2aa2acb8788e0b56938786c33c7c98562697bf9f4c7d6e8e5e3a0501e4a",
142
- "sha256:8a2b2b78c78293782fd3767d53e6474582f62443d0504b1554370bde86cc8227",
143
- "sha256:8ce1415194b4a6bd0cdcc3a1dfbf58b63f910dcb7330fe15bdff542c56949f87",
144
- "sha256:9ca28a302acb19b6af89e90f33ee3e1906961f94b54ea37de6737b7ca9d8827c",
145
- "sha256:a4cdc86d54b5da0df6d3d3a2f0b710949286094c3a6700c21e9015932b81447e",
146
- "sha256:aa5b1c1bfc28384f1f53b69a023d789f72b2e0ab1b3787aae16992a7ca21056c",
147
- "sha256:aadacf9a2f407a4688d700e4ebab33a7e2e408f2ca04dbf4aef17585389eff3e",
148
- "sha256:ae71e7ddb7a413dd60052e90528f2f65270aad4b509563af6d03d53e979feafd",
149
- "sha256:b14706df8b2de49869ae03a5ccbc211f4041750cd4a66f698df89d44f4bd30ec",
150
- "sha256:b1a93009cb80730c9bca5d6d4665494b725b6e8e157c1cb7f2db5b4b122ea562",
151
- "sha256:b2991665420a803495e0b90a79233c1433d6ed77ef282e8e152a324bbbc5e0c8",
152
- "sha256:b2c5edc4ac10a7ef6605a966c58929ec6c1bd0917fb8c15cb3363f65aa40e677",
153
- "sha256:b4d33f418f46362995f1e9d4f3a35a1b6322cb959c31d88ae56b0298e1c22357",
154
- "sha256:b91cbc4b195444e7e258ba27ac33769c41b94967919f10037e6355e998af255c",
155
- "sha256:c74880fc64d4958159fbd537a091d2a585448a8f8508bf248d72112723974cbd",
156
- "sha256:c901df83d097649e257e803be22592aedfd5182f07b3cc87d640bbb9afd50f49",
157
- "sha256:cac99918c7bba15302a2d81f0312c08054a3359eaa1929c7e4b26ebe41e9b286",
158
- "sha256:cc4f1358cb0c78edef3ed237ef2c86056206bb8d9140e73b6b89fbcfcbdd40e1",
159
- "sha256:ccd341521be3d1b3daeb41960ae94a5e87abe2f46f17224ba5d6f2b8398016cf",
160
- "sha256:ce4b94265ca988c3f8e479e741693d143026632672e3ff924f25fab50518dd51",
161
- "sha256:cf271892d13e43bc2b51e6908ec9a6a5094a4df1d8af0bfc360088ee6c684409",
162
- "sha256:d5ae728ff3b5401cc320d792866987e7e7e880e6ebd24433b70a33b643bb0384",
163
- "sha256:d71eec7d83298f1af3326ce0ff1d0ea83c7cb98f72b577097f9083b20bdaf05e",
164
- "sha256:d898fe162d26929b5960e4e138651f7427048e72c853607f2b200909794ed978",
165
- "sha256:d89d7b2974cae412400e88f35d86af72208e1ede1a541954af5d944a8ba46c57",
166
- "sha256:dfa8fe35a0bb90382837b238fff375de15f0dcdb9ae68ff85f7a63649c98527e",
167
- "sha256:e0be5efd5127542ef31f165de269f77560d6cdef525fffa446de6f7e9186cfb2",
168
- "sha256:fdfafb32984684eb03c2d83e1e51f64f0906b11e64482df3c5db936ce3839d48",
169
- "sha256:ff7687ca3d7028d8a5f0ebae95a6e4827c5616b31a4ee1192bdfde697db110d4"
118
+ "sha256:075299460948cd12722a970c7eae43d25d37989da682997687b34ae6b87c0ef0",
119
+ "sha256:07dfdd492d645eea1bd70fb1d6febdcf47db178b0d99161d8e4eed18e7f62fe7",
120
+ "sha256:0cbdf2cae14a06827bec50bd58e49249452d211d9caddd8bd80e35b53cb04631",
121
+ "sha256:2055c4fb9a6ff624253d432aa471a37202cd8f458c033d6d989be4499aed037b",
122
+ "sha256:262fffc1f6c1a26125d5d573e1ec379285a3723363f3bd9c83923c9593a2ac25",
123
+ "sha256:280132aada3bc2f0fac939a5771db4fbb84f245cb35b94fae4994d4c1f80dae7",
124
+ "sha256:2b57780b51084d5223eee7b59f0d4911c31c16ee5aa12737c7a02455829ff067",
125
+ "sha256:2bd7065249703cbeb6d4ce679c734bef0ee69baa7bff9724361ada04a15b7e3b",
126
+ "sha256:3235d7c781232e525b0761730e052388a01548bd7f67d0067a253887c6e8df46",
127
+ "sha256:33c020d3322662e74bc507fb11488773a96894aa82a622c35a5a28673c0c26f5",
128
+ "sha256:357754dcdfd811462a725e7501a9b4556388e8ecf66e79df6f4b988fa3d0b39a",
129
+ "sha256:39793731182c4be939b4be0cdecde074b833f6171313cf53481f869937129ed3",
130
+ "sha256:3c2b77f295edb9fcdb6a250f83e6481c679335ca7e6e4a955e4290350f2d22a4",
131
+ "sha256:41327143c5b1d715f5f98a397608f90ab9ebba606ae4e6f3389c2145410c52b1",
132
+ "sha256:427e1e627b0963ac02d7c8730ca6d935df10280d230508c0ba059505e9233475",
133
+ "sha256:432949a32c3e3f820af808db1833d6d1631664d53dd3ce487aa25d574e18ad1c",
134
+ "sha256:4ba01d9ba112b55bfa4b24808ec431197bb34f09f66f7cb4fd0258ff9d3711b1",
135
+ "sha256:4d0e206259b73af35c4ec1319fd04003776e11e859936658cb6ceffdeba0f5be",
136
+ "sha256:51431d0abbed3a868e967f8257c5faf283d41ec882f58413cf295a389bb22e58",
137
+ "sha256:565b2e82d0968c977e0b0f7cbf25fd06d78d4856289abc79694c8edcce6eb2de",
138
+ "sha256:6782cd6216fab5a83216cc39f13ebe30adfac2fa72688c5a4d8d180cd52e8f6a",
139
+ "sha256:6afd2e84e7da40fe23ca588379f815fb6dbbb1b757c883935ed11647205111cb",
140
+ "sha256:710c62b6e35a9a766b99b15cdc56d5aeda0914edae8bb467e9c355f75d14ee95",
141
+ "sha256:84921b10aeb2dd453247fd10de22907984eaf80901b578a5cf0bb1e279a587cb",
142
+ "sha256:85a5dbe1ba1bf38d6c63b6d2c42132d45cbee6d9f0c51b52c59aa4afba057517",
143
+ "sha256:9c6384cc90e37cfb60435bbbe0488444e54b98700f727f16f64d8bfda0b84656",
144
+ "sha256:9dd88fce54abbdbf4c42fb1fea0e498973d07816f24c0e27a1ecaf91883ce69e",
145
+ "sha256:a81eb64feded34f40c8986869a2f764f0fe2db58c0530d3a4afbcde50f314880",
146
+ "sha256:a898c11dca8f8c97b467138004a30133974aacd572818c383596f8d5b2eb04a9",
147
+ "sha256:a9960dd1891b2ddf13a7fe45339cd59ecee3abb6b8326d8b932d0c5da208104f",
148
+ "sha256:a9a7ef30a1b02547c1b23fa9a5564f03c9982fc71eb2ecb7f98c96d7a0db5cf2",
149
+ "sha256:ad97ec0da94b378e593ef532b980c15e377df9b9608c7c6da3506953182398af",
150
+ "sha256:adf032b6c105881f9d77fa17d9eebe0ad1f9bfb2ad25777811f97c5362aa07f2",
151
+ "sha256:bbfe6389c5522b99768a93d89aca52ef92310a96b99782973b9d11e80511f932",
152
+ "sha256:bd4bacd62aa2f1a1627352fe68885d6ee694bdaebb16038b6e680f2924a9b2cc",
153
+ "sha256:bf0b4b8d9caa8d64df838e0f8dcf68fb570c5733b726d1494b87f3da85db3a2d",
154
+ "sha256:c379cdd3efc0658e652a14112d51a7668f6bfca7445c5a10dee7eabecabba19d",
155
+ "sha256:c58536f6892559e030e6924896a44098bc1290663ea12532c78cef71d0df8493",
156
+ "sha256:cbe6581fcff7c8e262eb574244f81f5faaea539e712a058e6707a9d272fe5b64",
157
+ "sha256:ced268e82af993d7801a9db2dbc1d2322e786c5dc76295d8e89473d46c6b84d4",
158
+ "sha256:cf3539007202ebfe03923128fedfdd245db5860a36810136ad95a564a2fdffff",
159
+ "sha256:cf62d17310f34084c59c01e027259076479128d11e4661bb6c9acb38c5e19bb8",
160
+ "sha256:d0194d654e360b3e6cc9b774e83235bae6b9b2cac3be09040880bb0e8a88f4a1",
161
+ "sha256:d3d117890b6eee85887b1eed41eefe2e598ad6e40523d9f94c4c4b213258e4a4",
162
+ "sha256:db2de4e546f0ec4b2787d625e0b16b78e99c3e21bc1722b4977c0dddf11ca84e",
163
+ "sha256:e768d870801f68c74c2b669fc909839660180c366501d4cc4b87efd6b0eee375",
164
+ "sha256:e7c211f25777746d468d76f11719e64acb40eed410d81c26cefac641975beb88",
165
+ "sha256:eed462b4541c540d63ab57b3fc69e7d8c84d5957668854ee4e408b50e92ce26a",
166
+ "sha256:f0bfe42523893c188e9616d853c47685e1c575fe25f737adf473d0405dcfa7eb",
167
+ "sha256:f609ebcb0242d84b7adeee2b06c11a2ddaec5464d21888b2c8255f5fd6a98ae4",
168
+ "sha256:fea9d3ca80bcf17edb2c08a4704259dadac196fe5e9274067e7a20511fad1743",
169
+ "sha256:fed7a72d54bd52f4aeb6c6e951f363903bd7d70bc1cad64dd1f087980d309ab9"
170
170
  ],
171
171
  "markers": "python_version >= '3.8'",
172
- "version": "==7.4.4"
172
+ "version": "==7.5.0"
173
173
  },
174
174
  "distlib": {
175
175
  "hashes": [
@@ -197,11 +197,11 @@
197
197
  },
198
198
  "filelock": {
199
199
  "hashes": [
200
- "sha256:404e5e9253aa60ad457cae1be07c0f0ca90a63931200a47d9b6a6af84fd7b45f",
201
- "sha256:d13f466618bfde72bd2c18255e269f72542c6e70e7bac83a0232d6b1cc5c8cf4"
200
+ "sha256:43339835842f110ca7ae60f1e1c160714c5a6afd15a2873419ab185334975c0f",
201
+ "sha256:6ea72da3be9b8c82afd3edcf99f2fffbb5076335a5ae4d03248bb5b6c3eae78a"
202
202
  ],
203
203
  "markers": "python_version >= '3.8'",
204
- "version": "==3.13.4"
204
+ "version": "==3.14.0"
205
205
  },
206
206
  "flake8": {
207
207
  "hashes": [
@@ -261,11 +261,11 @@
261
261
  },
262
262
  "identify": {
263
263
  "hashes": [
264
- "sha256:10a7ca245cfcd756a554a7288159f72ff105ad233c7c4b9c6f0f4d108f5f6791",
265
- "sha256:c4de0081837b211594f8e877a6b4fad7ca32bbfc1a9307fdd61c28bfe923f13e"
264
+ "sha256:37d93f380f4de590500d9dba7db359d0d3da95ffe7f9de1753faa159e71e7dfa",
265
+ "sha256:e5e00f54165f9047fbebeb4a560f9acfb8af4c88232be60a488e9b68d122745d"
266
266
  ],
267
267
  "markers": "python_version >= '3.8'",
268
- "version": "==2.5.35"
268
+ "version": "==2.5.36"
269
269
  },
270
270
  "importlib-metadata": {
271
271
  "hashes": [
@@ -386,19 +386,19 @@
386
386
  },
387
387
  "platformdirs": {
388
388
  "hashes": [
389
- "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068",
390
- "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"
389
+ "sha256:031cd18d4ec63ec53e82dceaac0417d218a6863f7745dfcc9efe7793b7039bdf",
390
+ "sha256:17d5a1161b3fd67b390023cb2d3b026bbd40abde6fdb052dfbd3a29c3ba22ee1"
391
391
  ],
392
392
  "markers": "python_version >= '3.8'",
393
- "version": "==4.2.0"
393
+ "version": "==4.2.1"
394
394
  },
395
395
  "pluggy": {
396
396
  "hashes": [
397
- "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981",
398
- "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"
397
+ "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1",
398
+ "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"
399
399
  ],
400
400
  "markers": "python_version >= '3.8'",
401
- "version": "==1.4.0"
401
+ "version": "==1.5.0"
402
402
  },
403
403
  "pre-commit": {
404
404
  "hashes": [
@@ -443,12 +443,12 @@
443
443
  },
444
444
  "pytest": {
445
445
  "hashes": [
446
- "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7",
447
- "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044"
446
+ "sha256:1733f0620f6cda4095bbf0d9ff8022486e91892245bb9e7d5542c018f612f233",
447
+ "sha256:d507d4482197eac0ba2bae2e9babf0672eb333017bcedaa5fb1a3d42c1174b3f"
448
448
  ],
449
449
  "index": "pypi",
450
450
  "markers": "python_version >= '3.8'",
451
- "version": "==8.1.1"
451
+ "version": "==8.2.0"
452
452
  },
453
453
  "pytest-cov": {
454
454
  "hashes": [
@@ -470,12 +470,12 @@
470
470
  },
471
471
  "pytest-xdist": {
472
472
  "hashes": [
473
- "sha256:cbb36f3d67e0c478baa57fa4edc8843887e0f6cfc42d677530a36d7472b32d8a",
474
- "sha256:d075629c7e00b611df89f490a5063944bee7a4362a5ff11c7cc7824a03dfce24"
473
+ "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7",
474
+ "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d"
475
475
  ],
476
476
  "index": "pypi",
477
- "markers": "python_version >= '3.7'",
478
- "version": "==3.5.0"
477
+ "markers": "python_version >= '3.8'",
478
+ "version": "==3.6.1"
479
479
  },
480
480
  "pyyaml": {
481
481
  "hashes": [
@@ -592,11 +592,11 @@
592
592
  },
593
593
  "virtualenv": {
594
594
  "hashes": [
595
- "sha256:7bb554bbdfeaacc3349fa614ea5bff6ac300fc7c335e9facf3a3bcfc703f45be",
596
- "sha256:8aac4332f2ea6ef519c648d0bc48a5b1d324994753519919bddbb1aff25a104e"
595
+ "sha256:604bfdceaeece392802e6ae48e69cec49168b9c5f4a44e483963f9242eb0e78b",
596
+ "sha256:7aa9982a728ae5892558bff6a2839c00b9ed145523ece2274fad6f414690ae75"
597
597
  ],
598
598
  "markers": "python_version >= '3.7'",
599
- "version": "==20.25.3"
599
+ "version": "==20.26.1"
600
600
  },
601
601
  "zipp": {
602
602
  "hashes": [
@@ -860,12 +860,12 @@
860
860
  },
861
861
  "myst-parser": {
862
862
  "hashes": [
863
- "sha256:7c36344ae39c8e740dad7fdabf5aa6fc4897a813083c6cc9990044eb93656b14",
864
- "sha256:ea929a67a6a0b1683cdbe19b8d2e724cd7643f8aa3e7bb18dd65beac3483bead"
863
+ "sha256:6457aaa33a5d474aca678b8ead9b3dc298e89c68e67012e73146ea6fd54babf1",
864
+ "sha256:88f0cb406cb363b077d176b51c476f62d60604d68a8dcdf4832e080441301a87"
865
865
  ],
866
866
  "index": "pypi",
867
867
  "markers": "python_version >= '3.8'",
868
- "version": "==2.0.0"
868
+ "version": "==3.0.1"
869
869
  },
870
870
  "packaging": {
871
871
  "hashes": [
@@ -35,8 +35,8 @@ fotolab -h
35
35
  ```
36
36
 
37
37
  ```console
38
- usage: fotolab [-h] [-o] [-op] [-od OUTPUT_DIR] [-q] [-d] [-V]
39
- {auto,border,contrast,info,resize,montage,sharpen,watermark,env}
38
+ usage: fotolab [-h] [-o] [-op] [-od OUTPUT_DIR] [-q] [-v] [-d] [-V]
39
+ {animate,auto,border,contrast,info,resize,montage,sharpen,watermark,env}
40
40
  ...
41
41
 
42
42
  A console program to manipulate photos.
@@ -46,8 +46,9 @@ A console program to manipulate photos.
46
46
  issues: https://github.com/kianmeng/fotolab/issues
47
47
 
48
48
  positional arguments:
49
- {auto,border,contrast,info,resize,montage,sharpen,watermark,env}
49
+ {animate,auto,border,contrast,info,resize,montage,sharpen,watermark,env}
50
50
  sub-command help
51
+ animate animate an image
51
52
  auto auto adjust (resize, contrast, and watermark) a photo
52
53
  border add border to image
53
54
  contrast contrast an image
@@ -65,10 +66,33 @@ optional arguments:
65
66
  -od OUTPUT_DIR, --output-dir OUTPUT_DIR
66
67
  set default output folder (default: 'output')
67
68
  -q, --quiet suppress all logging
69
+ -v, --verbose show verbosity of debugging log, use -vv, -vvv for more details
68
70
  -d, --debug show debugging log and stacktrace
69
71
  -V, --version show program's version number and exit
70
72
  ```
71
73
 
74
+ ### fotolab animate
75
+
76
+ ```console
77
+ fotolab animate -h
78
+ ```
79
+
80
+ ```console
81
+ usage: fotolab animate [-h] [-f FORMAT] [-d DURATION] [-l LOOP]
82
+ IMAGE_FILENAMES [IMAGE_FILENAMES ...]
83
+
84
+ positional arguments:
85
+ IMAGE_FILENAMES set the image filenames
86
+
87
+ optional arguments:
88
+ -h, --help show this help message and exit
89
+ -f FORMAT, --format FORMAT
90
+ set the image format (default: 'gif')
91
+ -d DURATION, --duration DURATION
92
+ set the duration in milliseconds (default: '2500')
93
+ -l LOOP, --loop LOOP set the loop cycle (default: '0')
94
+ ```
95
+
72
96
  ### fotolab auto
73
97
 
74
98
  ```console
@@ -157,7 +181,6 @@ fotolab montage -h
157
181
  ```
158
182
 
159
183
  ```console
160
-
161
184
  usage: fotolab montage [-h] IMAGE_FILENAMES [IMAGE_FILENAMES ...]
162
185
 
163
186
  positional arguments:
@@ -174,7 +197,7 @@ fotolab resize -h
174
197
  ```
175
198
 
176
199
  ```console
177
- usage: fotolab resize [-h] [-wh WIDTH] [-ht HEIGHT]
200
+ usage: fotolab resize [-h] [-wh WIDTH | -ht HEIGHT]
178
201
  IMAGE_FILENAMES [IMAGE_FILENAMES ...]
179
202
 
180
203
  positional arguments:
@@ -21,7 +21,7 @@ import subprocess
21
21
  import sys
22
22
  from pathlib import Path
23
23
 
24
- __version__ = "0.13.0"
24
+ __version__ = "0.15.0"
25
25
 
26
26
  log = logging.getLogger(__name__)
27
27
 
@@ -48,7 +48,7 @@ def save_image(args, new_image, output_filename, subcommand):
48
48
  )
49
49
  new_filename.parent.mkdir(parents=True, exist_ok=True)
50
50
 
51
- log.info("%s image: %s", subcommand, new_filename)
51
+ log.info("%s image: %s", subcommand, new_filename.resolve())
52
52
  new_image.save(new_filename)
53
53
 
54
54
  if args.open:
@@ -64,4 +64,4 @@ def _open_image(filename):
64
64
  elif sys.platform == "windows":
65
65
  os.startfile(filename)
66
66
 
67
- log.info("open image: %s using default program.", filename.resolve())
67
+ log.info("open image: %s", filename.resolve())
@@ -0,0 +1,114 @@
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
+ """Animate subcommand."""
17
+
18
+ import argparse
19
+ import logging
20
+ from pathlib import Path
21
+
22
+ from PIL import Image
23
+
24
+ from fotolab import _open_image
25
+
26
+ log = logging.getLogger(__name__)
27
+
28
+
29
+ def build_subparser(subparsers) -> None:
30
+ """Build the subparser."""
31
+ animate_parser = subparsers.add_parser("animate", help="animate an image")
32
+
33
+ animate_parser.set_defaults(func=run)
34
+
35
+ animate_parser.add_argument(
36
+ dest="image_filenames",
37
+ help="set the image filenames",
38
+ nargs="+",
39
+ type=str,
40
+ default=None,
41
+ metavar="IMAGE_FILENAMES",
42
+ )
43
+
44
+ animate_parser.add_argument(
45
+ "-f",
46
+ "--format",
47
+ dest="format",
48
+ type=str,
49
+ choices=["gif", "webp"],
50
+ default="gif",
51
+ help="set the image format (default: '%(default)s')",
52
+ metavar="FORMAT",
53
+ )
54
+
55
+ animate_parser.add_argument(
56
+ "-d",
57
+ "--duration",
58
+ dest="duration",
59
+ type=int,
60
+ default=2500,
61
+ help="set the duration in milliseconds (default: '%(default)s')",
62
+ metavar="DURATION",
63
+ )
64
+
65
+ animate_parser.add_argument(
66
+ "-l",
67
+ "--loop",
68
+ dest="loop",
69
+ type=int,
70
+ default=0,
71
+ help="set the loop cycle (default: '%(default)s')",
72
+ metavar="LOOP",
73
+ )
74
+
75
+
76
+ def run(args: argparse.Namespace) -> None:
77
+ """Run animate subcommand.
78
+
79
+ Args:
80
+ config (argparse.Namespace): Config from command line arguments
81
+
82
+ Returns:
83
+ None
84
+ """
85
+ log.debug(args)
86
+
87
+ first_image = args.image_filenames[0]
88
+ animated_image = Image.open(first_image)
89
+
90
+ append_images = []
91
+ for image_filename in args.image_filenames[1:]:
92
+ append_images.append(Image.open(image_filename))
93
+
94
+ image_file = Path(first_image)
95
+ new_filename = Path(
96
+ args.output_dir,
97
+ image_file.with_name(f"animate_{image_file.stem}.{args.format}"),
98
+ )
99
+ new_filename.parent.mkdir(parents=True, exist_ok=True)
100
+
101
+ log.info("animate image: %s", new_filename)
102
+
103
+ animated_image.save(
104
+ new_filename,
105
+ format=args.format,
106
+ append_images=append_images,
107
+ save_all=True,
108
+ duration=args.duration,
109
+ loop=args.loop,
110
+ optimize=True,
111
+ )
112
+
113
+ if args.open:
114
+ _open_image(new_filename)
@@ -25,6 +25,7 @@ import logging
25
25
  import sys
26
26
  from typing import Dict, Optional, Sequence
27
27
 
28
+ import fotolab.animate
28
29
  import fotolab.auto
29
30
  import fotolab.border
30
31
  import fotolab.contrast
@@ -41,6 +42,9 @@ log = logging.getLogger(__name__)
41
42
 
42
43
  def setup_logging(args: argparse.Namespace) -> None:
43
44
  """Set up logging by level."""
45
+ if args.verbose == 0:
46
+ logging.getLogger("PIL").setLevel(logging.ERROR)
47
+
44
48
  if args.quiet:
45
49
  logging.disable(logging.NOTSET)
46
50
  else:
@@ -104,6 +108,15 @@ def build_parser() -> argparse.ArgumentParser:
104
108
  help="suppress all logging",
105
109
  )
106
110
 
111
+ parser.add_argument(
112
+ "-v",
113
+ "--verbose",
114
+ default=0,
115
+ action="count",
116
+ dest="verbose",
117
+ help="show verbosity of debugging log, use -vv, -vvv for more details",
118
+ )
119
+
107
120
  parser.add_argument(
108
121
  "-d",
109
122
  "--debug",
@@ -121,6 +134,7 @@ def build_parser() -> argparse.ArgumentParser:
121
134
  )
122
135
 
123
136
  subparsers = parser.add_subparsers(help="sub-command help")
137
+ fotolab.animate.build_subparser(subparsers)
124
138
  fotolab.auto.build_subparser(subparsers)
125
139
  fotolab.border.build_subparser(subparsers)
126
140
  fotolab.contrast.build_subparser(subparsers)
@@ -17,6 +17,7 @@
17
17
 
18
18
  import argparse
19
19
  import logging
20
+ import math
20
21
 
21
22
  from PIL import Image
22
23
 
@@ -24,6 +25,9 @@ from fotolab import save_image
24
25
 
25
26
  log = logging.getLogger(__name__)
26
27
 
28
+ DEFAULT_WIDTH = 600
29
+ DEFAULT_HEIGHT = 277
30
+
27
31
 
28
32
  def build_subparser(subparsers) -> None:
29
33
  """Build the subparser."""
@@ -40,23 +44,25 @@ def build_subparser(subparsers) -> None:
40
44
  metavar="IMAGE_FILENAMES",
41
45
  )
42
46
 
43
- resize_parser.add_argument(
47
+ group = resize_parser.add_mutually_exclusive_group(required=False)
48
+
49
+ group.add_argument(
44
50
  "-wh",
45
51
  "--width",
46
52
  dest="width",
47
53
  help="set the width of the image (default: '%(default)s')",
48
54
  type=int,
49
- default="600",
55
+ default=DEFAULT_WIDTH,
50
56
  metavar="WIDTH",
51
57
  )
52
58
 
53
- resize_parser.add_argument(
59
+ group.add_argument(
54
60
  "-ht",
55
61
  "--height",
56
62
  dest="height",
57
63
  help="set the height of the image (default: '%(default)s')",
58
64
  type=int,
59
- default="277",
65
+ default=DEFAULT_HEIGHT,
60
66
  metavar="HEIGHT",
61
67
  )
62
68
 
@@ -74,9 +80,37 @@ def run(args: argparse.Namespace) -> None:
74
80
 
75
81
  for image_filename in args.image_filenames:
76
82
  original_image = Image.open(image_filename)
83
+
84
+ new_width, new_height = _calc_new_image_dimension(original_image, args)
77
85
  resized_image = original_image.copy()
78
86
  resized_image = resized_image.resize(
79
- (args.width, args.height), Image.Resampling.LANCZOS
87
+ (new_width, new_height), Image.Resampling.LANCZOS
80
88
  )
81
89
 
82
90
  save_image(args, resized_image, image_filename, "resize")
91
+
92
+
93
+ def _calc_new_image_dimension(image, args) -> tuple:
94
+ new_width = args.width
95
+ new_height = args.height
96
+
97
+ old_width, old_height = image.size
98
+ log.debug("old image dimension: %d x %d", old_width, old_height)
99
+
100
+ if args.width != DEFAULT_WIDTH:
101
+ aspect_ratio = old_height / old_width
102
+ log.debug("aspect ratio: %f", aspect_ratio)
103
+
104
+ new_height = math.ceil(args.width * aspect_ratio)
105
+ log.debug("new height: %d", new_height)
106
+
107
+ if args.height != DEFAULT_HEIGHT:
108
+ aspect_ratio = old_width / old_height
109
+ log.debug("aspect ratio: %f", aspect_ratio)
110
+
111
+ new_width = math.floor(args.height * aspect_ratio)
112
+ log.debug("new width: %d", new_width)
113
+
114
+ log.debug("new image dimension: %d x %d", new_width, new_height)
115
+
116
+ return (new_width, new_height)
@@ -44,7 +44,7 @@ def build_subparser(subparsers) -> None:
44
44
  "-r",
45
45
  "--radius",
46
46
  dest="radius",
47
- type=str,
47
+ type=int,
48
48
  default=1,
49
49
  help="set the radius or size of edges (default: '%(default)s')",
50
50
  metavar="RADIUS",
@@ -54,7 +54,7 @@ def build_subparser(subparsers) -> None:
54
54
  "-p",
55
55
  "--percent",
56
56
  dest="percent",
57
- type=str,
57
+ type=int,
58
58
  default=100,
59
59
  help=(
60
60
  "set the amount of overall strength of sharpening effect "
@@ -67,7 +67,7 @@ def build_subparser(subparsers) -> None:
67
67
  "-t",
68
68
  "--threshold",
69
69
  dest="threshold",
70
- type=str,
70
+ type=int,
71
71
  default=3,
72
72
  help=(
73
73
  "set the minimum brightness changed to be sharpened "
@@ -17,6 +17,7 @@
17
17
 
18
18
  import argparse
19
19
  import logging
20
+ import math
20
21
 
21
22
  from PIL import Image, ImageColor, ImageDraw, ImageFont
22
23
 
@@ -24,6 +25,9 @@ from fotolab import save_image
24
25
 
25
26
  log = logging.getLogger(__name__)
26
27
 
28
+ FONT_SIZE_ASPECT_RATIO = 12 / 600
29
+ FONT_PADDING_ASPECT_RATIO = 15 / 600
30
+ FONT_OUTLINE_WIDTH_ASPECT_RATIO = 2 / 600
27
31
  POSITIONS = ["top-left", "top-right", "bottom-left", "bottom-right"]
28
32
 
29
33
 
@@ -140,19 +144,21 @@ def run(args: argparse.Namespace) -> None:
140
144
 
141
145
  draw = ImageDraw.Draw(watermarked_image)
142
146
 
143
- font = ImageFont.truetype("arial.ttf", args.font_size)
147
+ font = ImageFont.truetype(
148
+ "arial.ttf", calc_font_size(original_image, args)
149
+ )
144
150
 
145
151
  (left, top, right, bottom) = draw.textbbox(
146
152
  xy=(0, 0), text=args.text, font=font
147
153
  )
148
154
  text_width = right - left
149
155
  text_height = bottom - top
150
- (position_x, position_y) = calculate_position(
156
+ (position_x, position_y) = calc_position(
151
157
  watermarked_image,
152
158
  text_width,
153
159
  text_height,
154
160
  args.position,
155
- args.padding,
161
+ calc_padding(original_image, args),
156
162
  )
157
163
 
158
164
  draw.text(
@@ -160,16 +166,49 @@ def run(args: argparse.Namespace) -> None:
160
166
  args.text,
161
167
  font=font,
162
168
  fill=(*ImageColor.getrgb(args.font_color), 128),
163
- stroke_width=args.outline_width,
169
+ stroke_width=calc_font_outline_width(original_image, args),
164
170
  stroke_fill=(*ImageColor.getrgb(args.outline_color), 128),
165
171
  )
166
172
 
167
173
  save_image(args, watermarked_image, image_filename, "watermark")
168
174
 
169
175
 
170
- def calculate_position(
171
- image, text_width, text_height, position, padding
172
- ) -> tuple:
176
+ def calc_font_size(image, args) -> int:
177
+ """Calculate the font size based on the width of the image."""
178
+ width, _height = image.size
179
+ new_font_size = args.font_size
180
+ if width > 600:
181
+ new_font_size = math.floor(FONT_SIZE_ASPECT_RATIO * width)
182
+
183
+ log.debug("new font size: %d", new_font_size)
184
+ return new_font_size
185
+
186
+
187
+ def calc_font_outline_width(image, args) -> int:
188
+ """Calculate the font padding based on the width of the image."""
189
+ width, _height = image.size
190
+ new_font_outline_width = args.outline_width
191
+ if width > 600:
192
+ new_font_outline_width = math.floor(
193
+ FONT_OUTLINE_WIDTH_ASPECT_RATIO * width
194
+ )
195
+
196
+ log.debug("new font outline width: %d", new_font_outline_width)
197
+ return new_font_outline_width
198
+
199
+
200
+ def calc_padding(image, args) -> int:
201
+ """Calculate the font padding based on the width of the image."""
202
+ width, _height = image.size
203
+ new_padding = args.padding
204
+ if width > 600:
205
+ new_padding = math.floor(FONT_PADDING_ASPECT_RATIO * width)
206
+
207
+ log.debug("new padding: %d", new_padding)
208
+ return new_padding
209
+
210
+
211
+ def calc_position(image, text_width, text_height, position, padding) -> tuple:
173
212
  """Calculate the boundary coordinates of the watermark text."""
174
213
  if position == "top-left":
175
214
  position_x = 0 + padding
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
File without changes