diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e95a08a --- /dev/null +++ b/.gitignore @@ -0,0 +1,214 @@ +# Created by https://www.toptal.com/developers/gitignore/api/macos,python +# Edit at https://www.toptal.com/developers/gitignore?templates=macos,python + + +logos/ +images/ +documents/ + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +# End of https://www.toptal.com/developers/gitignore/api/macos,python diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..0da067d --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +python-pptx = "*" +customtkinter = "*" +pillow = "*" +ctktooltip = "*" + +[dev-packages] + +[requires] +python_version = "3.11" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..dbd0097 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,311 @@ +{ + "_meta": { + "hash": { + "sha256": "a448e15b2fa1f9589db822a76125dda8441999cb527c6301f9a0801a9a76b80a" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.11" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "ctktooltip": { + "hashes": [ + "sha256:08596120b12c8fa9b9bff2931a3aef3a29719380def9a0a50380c3b6852b4564", + "sha256:dd91fc208596e14d0eea03eaa57b0d6095be995538ce2b87538e4266fa7a753d" + ], + "index": "pypi", + "markers": "python_version >= '3.6'", + "version": "==0.8" + }, + "customtkinter": { + "hashes": [ + "sha256:14ad3e7cd3cb3b9eb642b9d4e8711ae80d3f79fb82545ad11258eeffb2e6b37c", + "sha256:fd8db3bafa961c982ee6030dba80b4c2e25858630756b513986db19113d8d207" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==5.2.2" + }, + "darkdetect": { + "hashes": [ + "sha256:a7509ccf517eaad92b31c214f593dbcf138ea8a43b2935406bbd565e15527a85", + "sha256:b5428e1170263eb5dea44c25dc3895edd75e6f52300986353cd63533fe7df8b1" + ], + "markers": "python_version >= '3.6'", + "version": "==0.8.0" + }, + "lxml": { + "hashes": [ + "sha256:01220dca0d066d1349bd6a1726856a78f7929f3878f7e2ee83c296c69495309e", + "sha256:02ced472497b8362c8e902ade23e3300479f4f43e45f4105c85ef43b8db85229", + "sha256:052d99051e77a4f3e8482c65014cf6372e61b0a6f4fe9edb98503bb5364cfee3", + "sha256:07da23d7ee08577760f0a71d67a861019103e4812c87e2fab26b039054594cc5", + "sha256:094cb601ba9f55296774c2d57ad68730daa0b13dc260e1f941b4d13678239e70", + "sha256:0a7056921edbdd7560746f4221dca89bb7a3fe457d3d74267995253f46343f15", + "sha256:0c120f43553ec759f8de1fee2f4794452b0946773299d44c36bfe18e83caf002", + "sha256:0d7b36afa46c97875303a94e8f3ad932bf78bace9e18e603f2085b652422edcd", + "sha256:0fdf3a3059611f7585a78ee10399a15566356116a4288380921a4b598d807a22", + "sha256:109fa6fede314cc50eed29e6e56c540075e63d922455346f11e4d7a036d2b8cf", + "sha256:146173654d79eb1fc97498b4280c1d3e1e5d58c398fa530905c9ea50ea849b22", + "sha256:1473427aff3d66a3fa2199004c3e601e6c4500ab86696edffdbc84954c72d832", + "sha256:1483fd3358963cc5c1c9b122c80606a3a79ee0875bcac0204149fa09d6ff2727", + "sha256:168f2dfcfdedf611eb285efac1516c8454c8c99caf271dccda8943576b67552e", + "sha256:17e8d968d04a37c50ad9c456a286b525d78c4a1c15dd53aa46c1d8e06bf6fa30", + "sha256:18feb4b93302091b1541221196a2155aa296c363fd233814fa11e181adebc52f", + "sha256:1afe0a8c353746e610bd9031a630a95bcfb1a720684c3f2b36c4710a0a96528f", + "sha256:1d04f064bebdfef9240478f7a779e8c5dc32b8b7b0b2fc6a62e39b928d428e51", + "sha256:1fdc9fae8dd4c763e8a31e7630afef517eab9f5d5d31a278df087f307bf601f4", + "sha256:1ffc23010330c2ab67fac02781df60998ca8fe759e8efde6f8b756a20599c5de", + "sha256:20094fc3f21ea0a8669dc4c61ed7fa8263bd37d97d93b90f28fc613371e7a875", + "sha256:213261f168c5e1d9b7535a67e68b1f59f92398dd17a56d934550837143f79c42", + "sha256:218c1b2e17a710e363855594230f44060e2025b05c80d1f0661258142b2add2e", + "sha256:23e0553b8055600b3bf4a00b255ec5c92e1e4aebf8c2c09334f8368e8bd174d6", + "sha256:25f1b69d41656b05885aa185f5fdf822cb01a586d1b32739633679699f220391", + "sha256:2b3778cb38212f52fac9fe913017deea2fdf4eb1a4f8e4cfc6b009a13a6d3fcc", + "sha256:2bc9fd5ca4729af796f9f59cd8ff160fe06a474da40aca03fcc79655ddee1a8b", + "sha256:2c226a06ecb8cdef28845ae976da407917542c5e6e75dcac7cc33eb04aaeb237", + "sha256:2c3406b63232fc7e9b8783ab0b765d7c59e7c59ff96759d8ef9632fca27c7ee4", + "sha256:2c86bf781b12ba417f64f3422cfc302523ac9cd1d8ae8c0f92a1c66e56ef2e86", + "sha256:2d9b8d9177afaef80c53c0a9e30fa252ff3036fb1c6494d427c066a4ce6a282f", + "sha256:2dec2d1130a9cda5b904696cec33b2cfb451304ba9081eeda7f90f724097300a", + "sha256:2dfab5fa6a28a0b60a20638dc48e6343c02ea9933e3279ccb132f555a62323d8", + "sha256:2ecdd78ab768f844c7a1d4a03595038c166b609f6395e25af9b0f3f26ae1230f", + "sha256:315f9542011b2c4e1d280e4a20ddcca1761993dda3afc7a73b01235f8641e903", + "sha256:36aef61a1678cb778097b4a6eeae96a69875d51d1e8f4d4b491ab3cfb54b5a03", + "sha256:384aacddf2e5813a36495233b64cb96b1949da72bef933918ba5c84e06af8f0e", + "sha256:3879cc6ce938ff4eb4900d901ed63555c778731a96365e53fadb36437a131a99", + "sha256:3c174dc350d3ec52deb77f2faf05c439331d6ed5e702fc247ccb4e6b62d884b7", + "sha256:3eb44520c4724c2e1a57c0af33a379eee41792595023f367ba3952a2d96c2aab", + "sha256:406246b96d552e0503e17a1006fd27edac678b3fcc9f1be71a2f94b4ff61528d", + "sha256:41ce1f1e2c7755abfc7e759dc34d7d05fd221723ff822947132dc934d122fe22", + "sha256:423b121f7e6fa514ba0c7918e56955a1d4470ed35faa03e3d9f0e3baa4c7e492", + "sha256:44264ecae91b30e5633013fb66f6ddd05c006d3e0e884f75ce0b4755b3e3847b", + "sha256:482c2f67761868f0108b1743098640fbb2a28a8e15bf3f47ada9fa59d9fe08c3", + "sha256:4b0c7a688944891086ba192e21c5229dea54382f4836a209ff8d0a660fac06be", + "sha256:4c1fefd7e3d00921c44dc9ca80a775af49698bbfd92ea84498e56acffd4c5469", + "sha256:4e109ca30d1edec1ac60cdbe341905dc3b8f55b16855e03a54aaf59e51ec8c6f", + "sha256:501d0d7e26b4d261fca8132854d845e4988097611ba2531408ec91cf3fd9d20a", + "sha256:516f491c834eb320d6c843156440fe7fc0d50b33e44387fcec5b02f0bc118a4c", + "sha256:51806cfe0279e06ed8500ce19479d757db42a30fd509940b1701be9c86a5ff9a", + "sha256:562e7494778a69086f0312ec9689f6b6ac1c6b65670ed7d0267e49f57ffa08c4", + "sha256:56b9861a71575f5795bde89256e7467ece3d339c9b43141dbdd54544566b3b94", + "sha256:5b8f5db71b28b8c404956ddf79575ea77aa8b1538e8b2ef9ec877945b3f46442", + "sha256:5c2fb570d7823c2bbaf8b419ba6e5662137f8166e364a8b2b91051a1fb40ab8b", + "sha256:5c54afdcbb0182d06836cc3d1be921e540be3ebdf8b8a51ee3ef987537455f84", + "sha256:5d6a6972b93c426ace71e0be9a6f4b2cfae9b1baed2eed2006076a746692288c", + "sha256:609251a0ca4770e5a8768ff902aa02bf636339c5a93f9349b48eb1f606f7f3e9", + "sha256:62d172f358f33a26d6b41b28c170c63886742f5b6772a42b59b4f0fa10526cb1", + "sha256:62f7fdb0d1ed2065451f086519865b4c90aa19aed51081979ecd05a21eb4d1be", + "sha256:658f2aa69d31e09699705949b5fc4719cbecbd4a97f9656a232e7d6c7be1a367", + "sha256:65ab5685d56914b9a2a34d67dd5488b83213d680b0c5d10b47f81da5a16b0b0e", + "sha256:68934b242c51eb02907c5b81d138cb977b2129a0a75a8f8b60b01cb8586c7b21", + "sha256:68b87753c784d6acb8a25b05cb526c3406913c9d988d51f80adecc2b0775d6aa", + "sha256:69959bd3167b993e6e710b99051265654133a98f20cec1d9b493b931942e9c16", + "sha256:6a7095eeec6f89111d03dabfe5883a1fd54da319c94e0fb104ee8f23616b572d", + "sha256:6b038cc86b285e4f9fea2ba5ee76e89f21ed1ea898e287dc277a25884f3a7dfe", + "sha256:6ba0d3dcac281aad8a0e5b14c7ed6f9fa89c8612b47939fc94f80b16e2e9bc83", + "sha256:6e91cf736959057f7aac7adfc83481e03615a8e8dd5758aa1d95ea69e8931dba", + "sha256:6ee8c39582d2652dcd516d1b879451500f8db3fe3607ce45d7c5957ab2596040", + "sha256:6f651ebd0b21ec65dfca93aa629610a0dbc13dbc13554f19b0113da2e61a4763", + "sha256:71a8dd38fbd2f2319136d4ae855a7078c69c9a38ae06e0c17c73fd70fc6caad8", + "sha256:74068c601baff6ff021c70f0935b0c7bc528baa8ea210c202e03757c68c5a4ff", + "sha256:7437237c6a66b7ca341e868cda48be24b8701862757426852c9b3186de1da8a2", + "sha256:747a3d3e98e24597981ca0be0fd922aebd471fa99d0043a3842d00cdcad7ad6a", + "sha256:74bcb423462233bc5d6066e4e98b0264e7c1bed7541fff2f4e34fe6b21563c8b", + "sha256:78d9b952e07aed35fe2e1a7ad26e929595412db48535921c5013edc8aa4a35ce", + "sha256:7b1cd427cb0d5f7393c31b7496419da594fe600e6fdc4b105a54f82405e6626c", + "sha256:7d3d1ca42870cdb6d0d29939630dbe48fa511c203724820fc0fd507b2fb46577", + "sha256:7e2f58095acc211eb9d8b5771bf04df9ff37d6b87618d1cbf85f92399c98dae8", + "sha256:7f41026c1d64043a36fda21d64c5026762d53a77043e73e94b71f0521939cc71", + "sha256:81b4e48da4c69313192d8c8d4311e5d818b8be1afe68ee20f6385d0e96fc9512", + "sha256:86a6b24b19eaebc448dc56b87c4865527855145d851f9fc3891673ff97950540", + "sha256:874a216bf6afaf97c263b56371434e47e2c652d215788396f60477540298218f", + "sha256:89e043f1d9d341c52bf2af6d02e6adde62e0a46e6755d5eb60dc6e4f0b8aeca2", + "sha256:8c72e9563347c7395910de6a3100a4840a75a6f60e05af5e58566868d5eb2d6a", + "sha256:8dc2c0395bea8254d8daebc76dcf8eb3a95ec2a46fa6fae5eaccee366bfe02ce", + "sha256:8f0de2d390af441fe8b2c12626d103540b5d850d585b18fcada58d972b74a74e", + "sha256:92e67a0be1639c251d21e35fe74df6bcc40cba445c2cda7c4a967656733249e2", + "sha256:94d6c3782907b5e40e21cadf94b13b0842ac421192f26b84c45f13f3c9d5dc27", + "sha256:97acf1e1fd66ab53dacd2c35b319d7e548380c2e9e8c54525c6e76d21b1ae3b1", + "sha256:9ada35dd21dc6c039259596b358caab6b13f4db4d4a7f8665764d616daf9cc1d", + "sha256:9c52100e2c2dbb0649b90467935c4b0de5528833c76a35ea1a2691ec9f1ee7a1", + "sha256:9e41506fec7a7f9405b14aa2d5c8abbb4dbbd09d88f9496958b6d00cb4d45330", + "sha256:9e4b47ac0f5e749cfc618efdf4726269441014ae1d5583e047b452a32e221920", + "sha256:9fb81d2824dff4f2e297a276297e9031f46d2682cafc484f49de182aa5e5df99", + "sha256:a0eabd0a81625049c5df745209dc7fcef6e2aea7793e5f003ba363610aa0a3ff", + "sha256:a3d819eb6f9b8677f57f9664265d0a10dd6551d227afb4af2b9cd7bdc2ccbf18", + "sha256:a87de7dd873bf9a792bf1e58b1c3887b9264036629a5bf2d2e6579fe8e73edff", + "sha256:aa617107a410245b8660028a7483b68e7914304a6d4882b5ff3d2d3eb5948d8c", + "sha256:aac0bbd3e8dd2d9c45ceb82249e8bdd3ac99131a32b4d35c8af3cc9db1657179", + "sha256:ab6dd83b970dc97c2d10bc71aa925b84788c7c05de30241b9e96f9b6d9ea3080", + "sha256:ace2c2326a319a0bb8a8b0e5b570c764962e95818de9f259ce814ee666603f19", + "sha256:ae5fe5c4b525aa82b8076c1a59d642c17b6e8739ecf852522c6321852178119d", + "sha256:b11a5d918a6216e521c715b02749240fb07ae5a1fefd4b7bf12f833bc8b4fe70", + "sha256:b1c8c20847b9f34e98080da785bb2336ea982e7f913eed5809e5a3c872900f32", + "sha256:b369d3db3c22ed14c75ccd5af429086f166a19627e84a8fdade3f8f31426e52a", + "sha256:b710bc2b8292966b23a6a0121f7a6c51d45d2347edcc75f016ac123b8054d3f2", + "sha256:bd96517ef76c8654446fc3db9242d019a1bb5fe8b751ba414765d59f99210b79", + "sha256:c00f323cc00576df6165cc9d21a4c21285fa6b9989c5c39830c3903dc4303ef3", + "sha256:c162b216070f280fa7da844531169be0baf9ccb17263cf5a8bf876fcd3117fa5", + "sha256:c1a69e58a6bb2de65902051d57fde951febad631a20a64572677a1052690482f", + "sha256:c1f794c02903c2824fccce5b20c339a1a14b114e83b306ff11b597c5f71a1c8d", + "sha256:c24037349665434f375645fa9d1f5304800cec574d0310f618490c871fd902b3", + "sha256:c300306673aa0f3ed5ed9372b21867690a17dba38c68c44b287437c362ce486b", + "sha256:c56a1d43b2f9ee4786e4658c7903f05da35b923fb53c11025712562d5cc02753", + "sha256:c6379f35350b655fd817cd0d6cbeef7f265f3ae5fedb1caae2eb442bbeae9ab9", + "sha256:c802e1c2ed9f0c06a65bc4ed0189d000ada8049312cfeab6ca635e39c9608957", + "sha256:cb83f8a875b3d9b458cada4f880fa498646874ba4011dc974e071a0a84a1b033", + "sha256:cf120cce539453ae086eacc0130a324e7026113510efa83ab42ef3fcfccac7fb", + "sha256:dd36439be765e2dde7660212b5275641edbc813e7b24668831a5c8ac91180656", + "sha256:dd5350b55f9fecddc51385463a4f67a5da829bc741e38cf689f38ec9023f54ab", + "sha256:df5c7333167b9674aa8ae1d4008fa4bc17a313cc490b2cca27838bbdcc6bb15b", + "sha256:e63601ad5cd8f860aa99d109889b5ac34de571c7ee902d6812d5d9ddcc77fa7d", + "sha256:e92ce66cd919d18d14b3856906a61d3f6b6a8500e0794142338da644260595cd", + "sha256:e99f5507401436fdcc85036a2e7dc2e28d962550afe1cbfc07c40e454256a859", + "sha256:ea2e2f6f801696ad7de8aec061044d6c8c0dd4037608c7cab38a9a4d316bfb11", + "sha256:eafa2c8658f4e560b098fe9fc54539f86528651f61849b22111a9b107d18910c", + "sha256:ecd4ad8453ac17bc7ba3868371bffb46f628161ad0eefbd0a855d2c8c32dd81a", + "sha256:ee70d08fd60c9565ba8190f41a46a54096afa0eeb8f76bd66f2c25d3b1b83005", + "sha256:eec1bb8cdbba2925bedc887bc0609a80e599c75b12d87ae42ac23fd199445654", + "sha256:ef0c1fe22171dd7c7c27147f2e9c3e86f8bdf473fed75f16b0c2e84a5030ce80", + "sha256:f2901429da1e645ce548bf9171784c0f74f0718c3f6150ce166be39e4dd66c3e", + "sha256:f422a209d2455c56849442ae42f25dbaaba1c6c3f501d58761c619c7836642ec", + "sha256:f65e5120863c2b266dbcc927b306c5b78e502c71edf3295dfcb9501ec96e5fc7", + "sha256:f7d4a670107d75dfe5ad080bed6c341d18c4442f9378c9f58e5851e86eb79965", + "sha256:f914c03e6a31deb632e2daa881fe198461f4d06e57ac3d0e05bbcab8eae01945", + "sha256:fb66442c2546446944437df74379e9cf9e9db353e61301d1a0e26482f43f0dd8" + ], + "markers": "python_version >= '3.6'", + "version": "==5.3.0" + }, + "packaging": { + "hashes": [ + "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", + "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" + ], + "markers": "python_version >= '3.8'", + "version": "==24.1" + }, + "pillow": { + "hashes": [ + "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885", + "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea", + "sha256:06b2f7898047ae93fad74467ec3d28fe84f7831370e3c258afa533f81ef7f3df", + "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5", + "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c", + "sha256:0ae24a547e8b711ccaaf99c9ae3cd975470e1a30caa80a6aaee9a2f19c05701d", + "sha256:134ace6dc392116566980ee7436477d844520a26a4b1bd4053f6f47d096997fd", + "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06", + "sha256:1b5dea9831a90e9d0721ec417a80d4cbd7022093ac38a568db2dd78363b00908", + "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a", + "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be", + "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0", + "sha256:298478fe4f77a4408895605f3482b6cc6222c018b2ce565c2b6b9c354ac3229b", + "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80", + "sha256:2db98790afc70118bd0255c2eeb465e9767ecf1f3c25f9a1abb8ffc8cfd1fe0a", + "sha256:32cda9e3d601a52baccb2856b8ea1fc213c90b340c542dcef77140dfa3278a9e", + "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9", + "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696", + "sha256:43efea75eb06b95d1631cb784aa40156177bf9dd5b4b03ff38979e048258bc6b", + "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309", + "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e", + "sha256:5161eef006d335e46895297f642341111945e2c1c899eb406882a6c61a4357ab", + "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d", + "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060", + "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d", + "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d", + "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4", + "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3", + "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6", + "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb", + "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94", + "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b", + "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496", + "sha256:73664fe514b34c8f02452ffb73b7a92c6774e39a647087f83d67f010eb9a0cf0", + "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319", + "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b", + "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856", + "sha256:7970285ab628a3779aecc35823296a7869f889b8329c16ad5a71e4901a3dc4ef", + "sha256:7a8d4bade9952ea9a77d0c3e49cbd8b2890a399422258a77f357b9cc9be8d680", + "sha256:7c1ee6f42250df403c5f103cbd2768a28fe1a0ea1f0f03fe151c8741e1469c8b", + "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42", + "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e", + "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597", + "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a", + "sha256:87dd88ded2e6d74d31e1e0a99a726a6765cda32d00ba72dc37f0651f306daaa8", + "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3", + "sha256:8d4d5063501b6dd4024b8ac2f04962d661222d120381272deea52e3fc52d3736", + "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da", + "sha256:930044bb7679ab003b14023138b50181899da3f25de50e9dbee23b61b4de2126", + "sha256:950be4d8ba92aca4b2bb0741285a46bfae3ca699ef913ec8416c1b78eadd64cd", + "sha256:961a7293b2457b405967af9c77dcaa43cc1a8cd50d23c532e62d48ab6cdd56f5", + "sha256:9b885f89040bb8c4a1573566bbb2f44f5c505ef6e74cec7ab9068c900047f04b", + "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026", + "sha256:a02364621fe369e06200d4a16558e056fe2805d3468350df3aef21e00d26214b", + "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc", + "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46", + "sha256:b15e02e9bb4c21e39876698abf233c8c579127986f8207200bc8a8f6bb27acf2", + "sha256:b2724fdb354a868ddf9a880cb84d102da914e99119211ef7ecbdc613b8c96b3c", + "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe", + "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984", + "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a", + "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70", + "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca", + "sha256:c76e5786951e72ed3686e122d14c5d7012f16c8303a674d18cdcd6d89557fc5b", + "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91", + "sha256:cfdd747216947628af7b259d274771d84db2268ca062dd5faf373639d00113a3", + "sha256:d7480af14364494365e89d6fddc510a13e5a2c3584cb19ef65415ca57252fb84", + "sha256:dbc6ae66518ab3c5847659e9988c3b60dc94ffb48ef9168656e0019a93dbf8a1", + "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5", + "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be", + "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f", + "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc", + "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9", + "sha256:e88d5e6ad0d026fba7bdab8c3f225a69f063f116462c49892b0149e21b6c0a0e", + "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141", + "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef", + "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22", + "sha256:f7baece4ce06bade126fb84b8af1c33439a76d8a6fd818970215e0560ca28c27", + "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e", + "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==10.4.0" + }, + "python-pptx": { + "hashes": [ + "sha256:160838e0b8565a8b1f67947675886e9fea18aa5e795db7ae531606d68e785cba", + "sha256:479a8af0eaf0f0d76b6f00b0887732874ad2e3188230315290cd1f9dd9cc7095" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==1.0.2" + }, + "typing-extensions": { + "hashes": [ + "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", + "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" + ], + "markers": "python_version >= '3.8'", + "version": "==4.12.2" + }, + "xlsxwriter": { + "hashes": [ + "sha256:9977d0c661a72866a61f9f7a809e25ebbb0fb7036baa3b9fe74afcfca6b3cb8c", + "sha256:ecfd5405b3e0e228219bcaf24c2ca0915e012ca9464a14048021d21a995d490e" + ], + "markers": "python_version >= '3.6'", + "version": "==3.2.0" + } + }, + "develop": {} +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..1806797 --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +# PowerPoint Image Organizer + +PowerPoint Image Organizer is a Python application that allows users to easily create PowerPoint presentations with multiple images per slide. It provides a user-friendly GUI for selecting images, arranging their order, and customizing presentation settings. + +![GUI](samples/gui.png) +![Presentation](samples/arrange-4.png) + + +## Features + +- Create new PowerPoint presentations or add to existing ones +- Add multiple images to slides with automatic layout +- Customize margins, line width, and colors +- Apply rounded corners to images +- Preview selected images +- Reorder images within slides +- Supports various image formats (PNG, JPG, JPEG) + +## Installation + +1. Clone the repository: + +```bash +git clone https://github.com/MoAlkhateeb/powerpoint-image-organizer.git +``` + +2. Install the required dependencies: + +```bash +pipenv install +``` + +## Usage + +1. Run the application: + +```bash +python src/gui.py +``` + +1. Use the "Browse" button to select an existing presentation or enter a new filename. +2. Adjust presentation settings as needed (margins, line width, color, etc.). +3. Click "Add Images" to select the images you want to include. +4. Arrange the order of images using the "Move Up" and "Move Down" buttons. +5. Click "Generate Presentation" to create your PowerPoint file. + +## Contributing +Contributions are welcome! Please feel free to submit a Pull Request. + +## License +This project is licensed under the MIT License - see the LICENSE file for details. \ No newline at end of file diff --git a/samples/arrange-1.png b/samples/arrange-1.png new file mode 100644 index 0000000..089326d Binary files /dev/null and b/samples/arrange-1.png differ diff --git a/samples/arrange-3.png b/samples/arrange-3.png new file mode 100644 index 0000000..48225d8 Binary files /dev/null and b/samples/arrange-3.png differ diff --git a/samples/arrange-4.png b/samples/arrange-4.png new file mode 100644 index 0000000..3d7b1f6 Binary files /dev/null and b/samples/arrange-4.png differ diff --git a/samples/gui.png b/samples/gui.png new file mode 100644 index 0000000..370387e Binary files /dev/null and b/samples/gui.png differ diff --git a/src/gui.py b/src/gui.py new file mode 100644 index 0000000..307b89f --- /dev/null +++ b/src/gui.py @@ -0,0 +1,332 @@ +from pathlib import Path +from typing import List + +from PIL import Image +import customtkinter as ctk +from CTkToolTip import CTkToolTip +from tkinter import filedialog, messagebox, ttk + +from pptx_generator import PPTXGenerator +from pptx_settings import PPTXSettings + + +class PPTXGeneratorGUI: + def __init__(self): + self.root = ctk.CTk() + self.root.title("PPTX Generator") + self.root.geometry("950x750") + + self.pptx_generator = PPTXGenerator() + self.settings = PPTXSettings() + self.selected_images: List[Path] = [] + self.current_presentation_path = None + + self.create_widgets() + + def create_widgets(self): + # Main frame + main_frame = ctk.CTkFrame(self.root) + main_frame.pack(fill=ctk.BOTH, expand=True, padx=20, pady=20) + + # Left panel for settings + left_panel = ctk.CTkFrame(main_frame) + left_panel.pack(side=ctk.LEFT, fill=ctk.BOTH, expand=True, padx=(0, 10)) + + # Right panel for image selection and preview + right_panel = ctk.CTkFrame(main_frame) + right_panel.pack(side=ctk.RIGHT, fill=ctk.BOTH, expand=True, padx=(10, 0)) + + # Settings widgets + self.create_settings_widgets(left_panel) + + # Image selection and preview widgets + self.create_image_widgets(right_panel) + + # Bottom panel for generate button + bottom_panel = ctk.CTkFrame(self.root) + bottom_panel.pack(side=ctk.BOTTOM, fill=ctk.X, padx=20, pady=20) + + self.create_action_buttons(bottom_panel) + + def create_settings_widgets(self, parent): + ctk.CTkLabel( + parent, text="Presentation Settings", font=("Arial", 16, "bold") + ).pack(pady=(0, 10)) + + # Presentation file selection + file_frame = ctk.CTkFrame(parent) + file_frame.pack(fill=ctk.X, pady=(0, 10)) + + self.file_entry = ctk.CTkEntry(file_frame, width=200) + self.file_entry.pack(side=ctk.LEFT, padx=(0, 5)) + self.create_tooltip( + self.file_entry, + "Enter the path to an existing presentation or a new file name", + ) + + file_btn = ctk.CTkButton( + file_frame, text="Browse", command=self.select_presentation_file + ) + file_btn.pack(side=ctk.LEFT) + + # Override/Append option + self.override_var = ctk.BooleanVar(value=True) + override_checkbox = ctk.CTkCheckBox( + parent, text="Override existing presentation", variable=self.override_var + ) + override_checkbox.pack(pady=(0, 10)) + self.create_tooltip( + override_checkbox, + "If checked, will override the existing presentation. If unchecked, will append to it.", + ) + + # Margins + margins_frame = ctk.CTkFrame(parent) + margins_frame.pack(fill=ctk.X, pady=(0, 10)) + + ctk.CTkLabel(margins_frame, text="Margins (inches):").grid( + row=0, column=0, sticky="w", padx=5, pady=5 + ) + self.top_margin = ctk.CTkEntry(margins_frame, width=50) + self.top_margin.grid(row=0, column=1, padx=5, pady=5) + self.top_margin.insert(0, str(self.settings.top_margin.inches)) + self.create_tooltip(self.top_margin, "Top margin in inches") + + self.left_margin = ctk.CTkEntry(margins_frame, width=50) + self.left_margin.grid(row=0, column=2, padx=5, pady=5) + self.left_margin.insert(0, str(self.settings.left_margin.inches)) + self.create_tooltip(self.left_margin, "Left margin in inches") + + self.right_margin = ctk.CTkEntry(margins_frame, width=50) + self.right_margin.grid(row=1, column=1, padx=5, pady=5) + self.right_margin.insert(0, str(self.settings.right_margin.inches)) + self.create_tooltip(self.right_margin, "Right margin in inches") + + self.bottom_margin = ctk.CTkEntry(margins_frame, width=50) + self.bottom_margin.grid(row=1, column=2, padx=5, pady=5) + self.bottom_margin.insert(0, str(self.settings.bottom_margin.inches)) + self.create_tooltip(self.bottom_margin, "Bottom margin in inches") + + # Center margins + center_margins_frame = ctk.CTkFrame(parent) + center_margins_frame.pack(fill=ctk.X, pady=(0, 10)) + + ctk.CTkLabel(center_margins_frame, text="Center Margins (inches):").grid( + row=0, column=0, sticky="w", padx=5, pady=5 + ) + self.h_center_margin = ctk.CTkEntry(center_margins_frame, width=50) + self.h_center_margin.grid(row=0, column=1, padx=5, pady=5) + self.h_center_margin.insert(0, str(self.settings.h_center_margin.inches)) + self.create_tooltip(self.h_center_margin, "Horizontal center margin in inches") + + self.v_center_margin = ctk.CTkEntry(center_margins_frame, width=50) + self.v_center_margin.grid(row=0, column=2, padx=5, pady=5) + self.v_center_margin.insert(0, str(self.settings.v_center_margin.inches)) + self.create_tooltip(self.v_center_margin, "Vertical center margin in inches") + + # Line width + line_width_frame = ctk.CTkFrame(parent) + line_width_frame.pack(fill=ctk.X, pady=(0, 10)) + + ctk.CTkLabel(line_width_frame, text="Line Width (pt):").pack( + side=ctk.LEFT, padx=5, pady=5 + ) + self.line_width = ctk.CTkEntry(line_width_frame, width=50) + self.line_width.pack(side=ctk.LEFT, padx=5, pady=5) + self.line_width.insert(0, str(self.settings.line_width.pt)) + self.create_tooltip(self.line_width, "Line width in points") + + # Color + color_frame = ctk.CTkFrame(parent) + color_frame.pack(fill=ctk.X, pady=(0, 10)) + + ctk.CTkLabel(color_frame, text="Color:").pack(side=ctk.LEFT, padx=5, pady=5) + self.color = ctk.CTkEntry(color_frame, width=100) + self.color.pack(side=ctk.LEFT, padx=5, pady=5) + self.color.insert(0, "#{:02x}{:02x}{:02x}".format(*self.settings.color)) + self.create_tooltip(self.color, "Color in hex format (e.g., #FF0000 for red)") + + # Rounded corners + self.rounded = ctk.CTkCheckBox(parent, text="Rounded Corners") + self.rounded.pack(pady=(0, 10)) + self.rounded.select() if self.settings.rounded else self.rounded.deselect() + self.create_tooltip(self.rounded, "Enable rounded corners for images") + + def create_image_widgets(self, parent): + ctk.CTkLabel(parent, text="Image Selection", font=("Arial", 16, "bold")).pack( + pady=(0, 10) + ) + + select_btn = ctk.CTkButton( + parent, text="Add Images", command=self.select_images + ) + select_btn.pack(pady=(0, 10)) + + self.image_listbox = ttk.Treeview( + parent, columns=("Order", "Filename"), show="headings", height=10 + ) + self.image_listbox.heading("Order", text="Order") + self.image_listbox.heading("Filename", text="Filename") + self.image_listbox.column("Order", width=50) + self.image_listbox.column("Filename", width=200) + self.image_listbox.pack(fill=ctk.BOTH, expand=True, pady=(0, 10)) + + self.image_listbox.bind("<>", self.on_image_select) + + button_frame = ctk.CTkFrame(parent) + button_frame.pack(fill=ctk.X, pady=(0, 10)) + + move_up_btn = ctk.CTkButton( + button_frame, text="Move Up", command=self.move_image_up + ) + move_up_btn.pack(side=ctk.LEFT, padx=(0, 5)) + + move_down_btn = ctk.CTkButton( + button_frame, text="Move Down", command=self.move_image_down + ) + move_down_btn.pack(side=ctk.LEFT, padx=(0, 5)) + + delete_btn = ctk.CTkButton( + button_frame, text="Delete", command=self.delete_selected_image + ) + delete_btn.pack(side=ctk.LEFT, padx=(0, 5)) + + delete_all_btn = ctk.CTkButton( + button_frame, text="Delete All", command=self.delete_all_images + ) + delete_all_btn.pack(side=ctk.LEFT) + + preview_frame = ctk.CTkFrame(parent) + preview_frame.pack(fill=ctk.BOTH, expand=True) + + self.preview_label = ctk.CTkLabel(preview_frame, text="") + self.preview_label.pack() + + def create_action_buttons(self, parent): + generate_btn = ctk.CTkButton( + parent, text="Generate Presentation", command=self.generate_presentation + ) + generate_btn.pack(side=ctk.TOP, pady=10) + + def select_presentation_file(self): + file_path = filedialog.asksaveasfilename( + defaultextension=".pptx", + filetypes=[("PowerPoint Presentation", "*.pptx")], + title="Select Existing Presentation or Enter New Filename", + ) + if file_path: + self.file_entry.delete(0, ctk.END) + self.file_entry.insert(0, file_path) + self.current_presentation_path = file_path + + def select_images(self): + filetypes = ( + ("Image files", "*.png *.jpg *.jpeg *.gif *.bmp"), + ("All files", "*.*"), + ) + image_paths = filedialog.askopenfilenames(filetypes=filetypes) + new_images = [Path(path) for path in image_paths] + self.selected_images.extend(new_images) + self.update_image_listbox() + + def update_image_listbox(self): + self.image_listbox.delete(*self.image_listbox.get_children()) + for index, image in enumerate(self.selected_images, start=1): + self.image_listbox.insert("", "end", values=(index, image.name)) + + def on_image_select(self, event): + selected_item = self.image_listbox.selection() + if selected_item: + index = int(self.image_listbox.item(selected_item)["values"][0]) - 1 + self.show_image_preview(self.selected_images[index]) + + def show_image_preview(self, image_path): + image = Image.open(image_path) + photo = ctk.CTkImage(image, size=(200, 200)) + self.preview_label.configure(image=photo) + self.preview_label.image = photo + + def move_image_up(self): + selected_item = self.image_listbox.selection() + if selected_item: + index = self.image_listbox.index(selected_item) + if index > 0: + self.selected_images[index], self.selected_images[index - 1] = ( + self.selected_images[index - 1], + self.selected_images[index], + ) + self.update_image_listbox() + self.image_listbox.selection_set( + self.image_listbox.get_children()[index - 1] + ) + + def move_image_down(self): + selected_item = self.image_listbox.selection() + if selected_item: + index = self.image_listbox.index(selected_item) + if index < len(self.selected_images) - 1: + self.selected_images[index], self.selected_images[index + 1] = ( + self.selected_images[index + 1], + self.selected_images[index], + ) + self.update_image_listbox() + self.image_listbox.selection_set( + self.image_listbox.get_children()[index + 1] + ) + + def delete_selected_image(self): + selected_items = self.image_listbox.selection() + if selected_items: + for item in reversed(selected_items): + index = self.image_listbox.index(item) + del self.selected_images[index] + self.update_image_listbox() + self.reset_image_preview() + + def delete_all_images(self): + self.selected_images.clear() + self.update_image_listbox() + self.reset_image_preview() + + def reset_image_preview(self): + self.preview_label.configure(image=None) + self.preview_label.image = None + + def generate_presentation(self): + try: + self.update_settings() + presentation_path = self.file_entry.get() or "new_presentation.pptx" + self.pptx_generator.create_presentation( + presentation_path, override=self.override_var.get() + ) + self.pptx_generator.add_images(self.selected_images) + self.pptx_generator.save_presentation(presentation_path) + messagebox.showinfo( + "Success", f"Presentation generated and saved to {presentation_path}" + ) + except Exception as e: + messagebox.showerror("Error", f"Failed to generate presentation: {str(e)}") + + def update_settings(self): + self.settings.top_margin = float(self.top_margin.get()) + self.settings.left_margin = float(self.left_margin.get()) + self.settings.right_margin = float(self.right_margin.get()) + self.settings.bottom_margin = float(self.bottom_margin.get()) + self.settings.h_center_margin = float(self.h_center_margin.get()) + self.settings.v_center_margin = float(self.v_center_margin.get()) + self.settings.line_width = float(self.line_width.get()) + self.settings.color = self.color.get() + self.settings.rounded = self.rounded.get() + + self.pptx_generator.settings = self.settings + + def create_tooltip(self, widget, text): + CTkToolTip(widget, message=text) + + def run(self): + self.root.mainloop() + + +if __name__ == "__main__": + app = PPTXGeneratorGUI() + app.run() diff --git a/src/image_layout_manager.py b/src/image_layout_manager.py new file mode 100644 index 0000000..1752095 --- /dev/null +++ b/src/image_layout_manager.py @@ -0,0 +1,91 @@ +from pptx.slide import Slide +from pptx.util import Inches +from pptx.dml.color import RGBColor +from pptx.enum.shapes import MSO_SHAPE +from typing import List, Tuple, Optional +from pptx_settings import PPTXSettings, Pathlike + + +class ImageLayoutManager: + def __init__(self, slide_size, settings: Optional[PPTXSettings] = None): + self.slide_width, self.slide_height = slide_size + self.settings = settings or PPTXSettings() + + def _calculate_dimensions( + self, num_columns: int, num_rows: int + ) -> Tuple[float, float]: + width = ( + self.slide_width + - self.settings.left_margin + - self.settings.right_margin + - (num_columns - 1) * self.settings.h_center_margin + ) / num_columns + + height = ( + self.slide_height + - self.settings.top_margin + - self.settings.bottom_margin + - (num_rows - 1) * self.settings.v_center_margin + ) / num_rows + return width, height + + def _get_position( + self, row: int, col: int, width: float, height: float + ) -> Tuple[float, float]: + x = self.settings.left_margin + col * (width + self.settings.h_center_margin) + y = self.settings.top_margin + row * (height + self.settings.v_center_margin) + return x, y + + def _add_image( + self, + slide: Slide, + image_path: Pathlike, + left: Inches, + top: Inches, + width: Inches, + height: Inches, + ) -> None: + pic = slide.shapes.add_picture(str(image_path), left, top, width, height) + if self.settings.rounded: + pic.auto_shape_type = MSO_SHAPE.ROUNDED_RECTANGLE + if self.settings.line_width > 0: + pic.line.color.rgb = RGBColor(*self.settings.color) + pic.line.width = self.settings.line_width + + def add_single_image(self, slide: Slide, image_path: Pathlike) -> None: + width, height = self._calculate_dimensions(1, 1) + self._add_image( + slide, + image_path, + self.settings.left_margin, + self.settings.top_margin, + width, + height, + ) + + def add_two_images(self, slide: Slide, image_paths: List[Pathlike]) -> None: + width, height = self._calculate_dimensions(2, 1) + for i, image_path in enumerate(image_paths): + x, y = self._get_position(0, i, width, height) + self._add_image(slide, image_path, x, y, width, height) + + def add_three_images(self, slide: Slide, image_paths: List[Pathlike]) -> None: + width, height = self._calculate_dimensions(2, 2) + for i, image_path in enumerate(image_paths): + if i < 2: + x, y = self._get_position(0, i, width, height) + else: + x = ( + self.settings.left_margin + + width / 2 + + self.settings.h_center_margin / 2 + ) + y = self.settings.top_margin + height + self.settings.v_center_margin + self._add_image(slide, image_path, x, y, width, height) + + def add_four_images(self, slide: Slide, image_paths: List[Pathlike]) -> None: + width, height = self._calculate_dimensions(2, 2) + for i, image_path in enumerate(image_paths): + row, col = divmod(i, 2) + x, y = self._get_position(row, col, width, height) + self._add_image(slide, image_path, x, y, width, height) diff --git a/src/pptx_generator.py b/src/pptx_generator.py new file mode 100644 index 0000000..1325c83 --- /dev/null +++ b/src/pptx_generator.py @@ -0,0 +1,74 @@ +from pathlib import Path +from typing import Optional, List + +from pptx.slide import Slide +from pptx import Presentation + +from image_layout_manager import ImageLayoutManager +from pptx_settings import PPTXSettings, Pathlike, BLANK_SLIDE_LAYOUT + + +class PPTXGenerator: + def __init__(self, settings: Optional[PPTXSettings] = None): + self.presentation = None + self.settings = settings or PPTXSettings() + self.slide_size = None + + def create_presentation( + self, presentation_path: Pathlike, override: bool = False + ) -> None: + if Path(presentation_path).exists() and not override: + self.presentation = Presentation(str(presentation_path)) + else: + self.presentation = Presentation() + + self.slide_size = ( + self.presentation.slide_width, + self.presentation.slide_height, + ) + + def add_images(self, images: List[Pathlike]) -> None: + images_grouped_by_four = [images[i : i + 4] for i in range(0, len(images), 4)] + + for group in images_grouped_by_four: + slide = self._create_empty_slide() + self._arrange_images(slide, group) + + def save_presentation(self, presentation_path: Pathlike) -> None: + self.presentation.save(str(presentation_path)) + + def _create_empty_slide(self) -> Slide: + return self.presentation.slides.add_slide( + self.presentation.slide_layouts[BLANK_SLIDE_LAYOUT] + ) + + def _arrange_images(self, slide: Slide, images: List[Pathlike]) -> None: + if not images: + return + + layout_manager = ImageLayoutManager(self.slide_size, self.settings) + + match len(images): + case 1: + layout_manager.add_single_image(slide, images[0]) + case 2: + layout_manager.add_two_images(slide, images) + case 3: + layout_manager.add_three_images(slide, images) + case 4: + layout_manager.add_four_images(slide, images) + case _: + raise ValueError("Only up to 4 images can be added to a slide") + + +if __name__ == "__main__": + pptx_generator = PPTXGenerator() + pptx_generator.create_presentation("presentation.pptx", override=False) + + pptx_generator.settings.rounded = True + pptx_generator.settings.color = "springgreen" + images = list(Path("images").glob("group_1/*.jpg")) + + pptx_generator.add_images(images) + + pptx_generator.save_presentation("presentation2.pptx") diff --git a/src/pptx_settings.py b/src/pptx_settings.py new file mode 100644 index 0000000..32799b4 --- /dev/null +++ b/src/pptx_settings.py @@ -0,0 +1,140 @@ +from pathlib import Path +from typing import Union, Tuple +from pptx.util import Inches, Pt +from PIL import ImageColor + +Pathlike = Union[str, Path] +BLANK_SLIDE_LAYOUT: int = 6 +RGBTriplet = Tuple[int, int, int] +HexColorOrName = str + + +class PPTXSettings: + def __init__(self): + self._top_margin = Inches(1.35) + self._left_margin = Inches(0.53) + self._right_margin = Inches(0.53) + self._bottom_margin = Inches(0.66) + self._h_center_margin = Inches(0.5) + self._v_center_margin = Inches(0.5) + self._line_width = Pt(2.25) + self._color = (0, 102, 204) + self.rounded = False + + @staticmethod + def _inches_to_float(value: Inches) -> float: + """Convert Inches to float value in inches.""" + return float(value.inches) + + @staticmethod + def _pt_to_float(value: Pt) -> float: + """Convert Pt to float value in points.""" + return float(value.pt) + + @staticmethod + def _to_inches(value: Union[float, Inches]) -> Inches: + """Convert a float to Inches if it's not already an Inches object.""" + return Inches(value) if isinstance(value, (int, float)) else value + + @staticmethod + def _to_pt(value: Union[float, Pt]) -> Pt: + """Convert a float to Pt if it's not already a Pt object.""" + return Pt(value) if isinstance(value, (int, float)) else value + + @property + def top_margin(self) -> Inches: + return self._top_margin + + @top_margin.setter + def top_margin(self, value: Union[float, Inches]) -> None: + self._top_margin = self._to_inches(value) + + @property + def left_margin(self) -> Inches: + return self._left_margin + + @left_margin.setter + def left_margin(self, value: Union[float, Inches]) -> None: + self._left_margin = self._to_inches(value) + + @property + def right_margin(self) -> Inches: + return self._right_margin + + @right_margin.setter + def right_margin(self, value: Union[float, Inches]) -> None: + self._right_margin = self._to_inches(value) + + @property + def bottom_margin(self) -> Inches: + return self._bottom_margin + + @bottom_margin.setter + def bottom_margin(self, value: Union[float, Inches]) -> None: + self._bottom_margin = self._to_inches(value) + + @property + def h_center_margin(self) -> Inches: + return self._h_center_margin + + @h_center_margin.setter + def h_center_margin(self, value: Union[float, Inches]) -> None: + self._h_center_margin = self._to_inches(value) + + @property + def v_center_margin(self) -> Inches: + return self._v_center_margin + + @v_center_margin.setter + def v_center_margin(self, value: Union[float, Inches]) -> None: + self._v_center_margin = self._to_inches(value) + + @property + def line_width(self) -> Pt: + return self._line_width + + @line_width.setter + def line_width(self, value: Union[float, Pt]) -> None: + self._line_width = self._to_pt(value) + + @staticmethod + def color_to_rgb(color_value: HexColorOrName) -> RGBTriplet: + """Convert a hex color string or a named color to an RGB triplet, ensuring no alpha channel.""" + rgb = ImageColor.getrgb(color_value) + return rgb[:3] + + @staticmethod + def validate_rgb(color: RGBTriplet) -> None: + """Validate that each RGB component is within the 0-255 range.""" + if not all(0 <= c <= 255 for c in color): + raise ValueError(f"Each color component must be between 0 and 255: {color}") + + @property + def color(self) -> RGBTriplet: + """Get the current RGB color.""" + return self._color + + @color.setter + def color(self, value: Union[RGBTriplet, HexColorOrName]) -> None: + """Set the color, converting from hex or named color if necessary, and validate it.""" + if isinstance(value, str): + value = self.color_to_rgb(value) + + value = value[:3] + + self.validate_rgb(value) + self._color = value + + def __repr__(self) -> str: + return ( + f"PPTXSettings(" + f"top_margin={self._inches_to_float(self.top_margin):.2f} in, " + f"left_margin={self._inches_to_float(self.left_margin):.2f} in, " + f"right_margin={self._inches_to_float(self.right_margin):.2f} in, " + f"bottom_margin={self._inches_to_float(self.bottom_margin):.2f} in, " + f"h_center_margin={self._inches_to_float(self.h_center_margin):.2f} in, " + f"v_center_margin={self._inches_to_float(self.v_center_margin):.2f} in, " + f"line_width={self._pt_to_float(self.line_width):.2f} pt, " + f"color={self.color}, " + f"rounded={self.rounded})" + )