commit 3a16af64d89a83239f698deaa18605dccad0966e Author: ge Date: Fri Apr 21 21:59:06 2023 +0300 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..849ddff --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +dist/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..e61fe0b --- /dev/null +++ b/README.md @@ -0,0 +1,48 @@ +mee +=== + +Minimalistic EXIF Editor. + +MEE allows you to get, remove and change EXIF tags in images. + +In addition to this, support for custom tags is implemented here. Custom tags are written in JSON format to the UserComment tag. MEE can operate with them in the same way as with ordinary tags. Be aware of the 64 kilobytes limit for the UserComment tag. + +Custom tags entered there can only be interpreted manually or with MEE. + +Installation +============ + +No prebuilt package provided. Build it by yourself. + +1. Install `poetry` +2. Clone repo and run `poetry build` +3. Run `pip install ./dist/*.tar.gz` + +Usage +===== + +``` +Minimalistic EXIF Editor. + +Commands overview: + ls, get, set, del operate with EXIF tags. + cget, cset, cdel operate with JSON in UserComment EXIF tag. + +Usage: + mee ls + mee get [-k ] + mee set -k -v [-o ] + mee del -k [-o ] + mee cget [-k ] + mee cset -k -v [-o ] + mee cdel -k [-o ] + mee (--help | --version) + +Options: + -o, --output output file. Same as input file by default. + -k, --key set EXIF/JSON key. + -v, --value EXIF/JSON key value. + --help print this message and exit. + --version print version and exit. +``` + diff --git a/mee.py b/mee.py new file mode 100644 index 0000000..a3d959a --- /dev/null +++ b/mee.py @@ -0,0 +1,146 @@ +"""Minimalistic EXIF Editor. + +Commands overview: + ls, get, set, del operate with EXIF tags. + cget, cset, cdel operate with JSON in UserComment EXIF tag. + +Usage: + mee ls + mee get [-k ] + mee set -k -v [-o ] + mee del -k [-o ] + mee cget [-k ] + mee cset -k -v [-o ] + mee cdel -k [-o ] + mee (--help | --version) + +Options: + -o, --output output file. Same as input file by default. + -k, --key set EXIF/JSON key. + -v, --value EXIF/JSON key value. + --help print this message and exit. + --version print version and exit. +""" + +__version__ = "0.1.0" + +import re +import sys +import json +from tempfile import NamedTemporaryFile +from typing import Optional +from pathlib import Path + +from docopt import docopt +from exif import Image + + +def open_image(infile: Path) -> Image: + with open(infile, "rb") as in_img: + return Image(in_img) + + +def commit_image(img: Image, infile: Path = None, outfile: Path = None) -> None: + if not outfile: + with NamedTemporaryFile(suffix="~") as out_tmp, open(infile, "wb") as out_img: + out_tmp.write(img.get_file()) + out_tmp.seek(0) + out_img.write(out_tmp.read()) + else: + with open(outfile, "wb") as out_img: + out_img.write(img.get_file()) + + +def get_exif(key: str, infile: Path, echo: bool = True) -> None: + image = open_image(infile) + value = image.get(key, "n/a") + if echo: + print(f"{key}: {value}") + else: + return value + + +def ls_exif(infile: Path) -> None: + image = open_image(infile) + tags = image.list_all() + print(tags) + + +def set_exif(key: str, value: str, infile: Path, outfile: Path = None) -> None: + image = open_image(infile) + image.set(key, value) + commit_image(image, infile, outfile) + + +def del_exif(key: str, infile: Path, outfile: Path = None) -> None: + image = open_image(infile) + image.delete(key) + commit_image(image, infile, outfile) + + +def get_user_comment(infile: Path) -> dict: + user_comment = get_exif("user_comment", infile, echo=False) + try: + return json.loads(user_comment) + except (TypeError, json.JSONDecodeError): + return {} + + +def get_json(infile: Path, key: str = None) -> None: + user_comment = get_user_comment(infile) + if key: + try: + print(user_comment[key]) + except KeyError as e: + sys.exit(f"No key: {e}") + else: + print(json.dumps(user_comment, indent=4, sort_keys=True)) + + +def mod_json(key: str, value: str, infile: Path, outfile: Path = None, delete: bool = False) -> None: + user_comment = get_user_comment(infile) + if delete: + del user_comment[key] + else: + user_comment[key] = value + try: + del_exif("user_comment", infile, outfile) + except AttributeError: + pass + set_exif("user_comment", json.dumps(user_comment), infile, outfile) + + +def cli() -> None: + args = docopt(__doc__, version=__version__) + key = args["--key"] + value = args["--value"] + infile = args[''] + outfile = args['--output'] + + if args['ls']: + ls_exif(infile) + + if args['get']: + if not key: + ls_exif(infile) + else: + get_exif(key, infile) + + if args["set"]: + set_exif(key, value, infile, outfile) + + if args['del']: + del_exif(key, infile, outfile) + + if args['cget']: + get_json(infile, key) + + if args["cset"]: + mod_json(key, value, infile, outfile) + + if args['cdel']: + mod_json(key, None, infile, outfile, delete=True) + + +if __name__ == "__main__": + cli() diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..fdc2d78 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,43 @@ +# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. + +[[package]] +name = "docopt" +version = "0.6.2" +description = "Pythonic argument parser, that will make you smile" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, +] + +[[package]] +name = "exif" +version = "1.6.0" +description = "Read and modify image EXIF metadata using Python." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exif-1.6.0-py3-none-any.whl", hash = "sha256:2d002b5740eb897da43de1e6a8f4ebacf5171b4d0b8bc0182c70aebde1be86d3"}, + {file = "exif-1.6.0.tar.gz", hash = "sha256:36288d1ffc60030084a04c26f50cc32e19383a36cd234fcfa1fb1c6f698e1d36"}, +] + +[package.dependencies] +plum-py = ">=0.5.0,<2.0.0" + +[[package]] +name = "plum-py" +version = "0.8.6" +description = "Pack/Unpack Memory." +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "plum_py-0.8.6-py3-none-any.whl", hash = "sha256:c0ce6d03fc4bf09dbd838b520085e9dfb690d1604f3eb7a553fb6331c1155b78"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.10" +content-hash = "1d8a15c0ec1aedf4cc7db64b156a95869a294604a356c1fc8be4e8d0e955a501" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..019ea5f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,19 @@ +[tool.poetry] +name = "mee" +version = "0.1.0" +description = "Minimalistic EXIF Editor." +authors = ["ge"] +license = "Unlicense" +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.10" +exif = "^1.6.0" +docopt = "^0.6.2" + +[tool.poetry.scripts] +mee = 'mee:cli' + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api"