Add photo conversion

This commit is contained in:
James Grogan 2024-11-10 17:23:46 +00:00
parent c09179f5da
commit 3e7ce98755
10 changed files with 215 additions and 54 deletions

View file

@ -15,7 +15,7 @@ classifiers = [
"Topic :: System :: Distributed Computing" "Topic :: System :: Distributed Computing"
] ]
keywords = ["Personal tools and recipes"] keywords = ["Personal tools and recipes"]
dependencies = ["pydantic", "tinytag"] dependencies = ["pydantic", "tinytag", "pillow", "pillow_heif"]
[project.urls] [project.urls]
Repository = "https://git.jmsgrogan.com/jgrogan/recipes" Repository = "https://git.jmsgrogan.com/jgrogan/recipes"

View file

@ -4,6 +4,7 @@ from typing import Callable
import logging import logging
from jgutils.filesystem import get_files_recursive from jgutils.filesystem import get_files_recursive
from jgutils.tasks import run_tasks
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -19,19 +20,29 @@ def _get_converted_path(input_path: Path,
output_filename = str(input_path.stem) + f".{output_ext}" output_filename = str(input_path.stem) + f".{output_ext}"
return output_dir / input_path.parent / output_filename return output_dir / input_path.parent / output_filename
def convert(input_dir: Path, def _should_convert(overwrite_existing, path):
if overwrite_existing:
return True
return not path.exists()
def convert(input_path: Path,
output_dir: Path, output_dir: Path,
input_ext: str, input_ext: str,
output_ext: str, output_ext: str,
conversion_func: Callable): conversion_func: Callable,
overwrite_existing: bool = False):
logger.info("Converting files in %s to %s", input_dir, output_dir) logger.info("Converting files in %s to %s", input_path, output_dir)
input_files = get_files_recursive(input_dir.resolve(), [input_ext]) if input_path.is_dir():
output_files = [_get_converted_path(f.relative_to(input_dir), input_files = get_files_recursive(input_path.resolve(), [input_ext])
else:
input_files = [input_path]
output_files = [_get_converted_path(f.relative_to(input_path),
output_dir, output_dir,
output_ext) for f in input_files] output_ext) for f in input_files]
conversion_pairs = [io_paths for io_paths in conversion_pairs = [io_paths for io_paths in
zip(input_files, output_files) if not io_paths[1].exists()] zip(input_files, output_files)
if _should_convert(overwrite_existing, io_paths[1])]
run_tasks(conversion_func, conversion_pairs) run_tasks(conversion_func, conversion_pairs)

View file

@ -0,0 +1,65 @@
import logging
from pathlib import Path
from functools import partial
import shutil
from .filesystem import for_each_file
logger = logging.getLogger(__name__)
def _remove_file(path: Path):
path.unlink()
def cli_file_remove(args):
input_path = args.path.resolve()
extension = args.ext
logger.info("Removing files with extension %s from %s",
extension, input_path)
for_each_file(input_path, [extension], _remove_file)
def _flatten(parent: Path, path: Path):
relative_path = path.relative_to(parent)
relative_dir = relative_path.parent
new_filename = "_".join(relative_dir.parts)
new_filename += "_" + path.name
new_path = parent / new_filename
shutil.move(path, new_path)
def cli_dir_flatten(args):
input_path = args.path.resolve()
logger.info("Flattening directory at: %s", input_path)
for_each_file(input_path, [], partial(_flatten, input_path))
def add_parser(subparsers):
parser = subparsers.add_parser("file")
parsers = parser.add_subparsers(required=True)
remove_parser = parsers.add_parser("remove")
remove_parser.add_argument(
"--path",
type=Path,
help="Path to directory to remove files from",
)
remove_parser.add_argument(
"--ext",
type=str,
help="Extension of file to remove",
)
remove_parser.set_defaults(func=cli_file_remove)
flatten_parser = parsers.add_parser("flatten")
flatten_parser.add_argument(
"--path",
type=Path,
help="Path to directory to flatten",
)
flatten_parser.set_defaults(func=cli_dir_flatten)

View file

@ -1,7 +1,11 @@
from pathlib import Path from pathlib import Path
import os import os
import sys import sys
from typing import Callable
def for_each_file(path: Path, extensions: list[str] | None, func: Callable):
for eachFile in get_files_recursive(path, extensions):
func(eachFile)
def get_files_recursive(path: Path, extensions: list[str] | None) -> list[Path]: def get_files_recursive(path: Path, extensions: list[str] | None) -> list[Path]:
if not extensions: if not extensions:

View file

35
src/jgutils/image/cli.py Normal file
View file

@ -0,0 +1,35 @@
from pathlib import Path
import os
from jgutils import converters
from jgutils.image.convert import pillow_convert
def cli_image_convert(args):
input_path = args.input.resolve()
if not input_path.is_dir():
output_dir = input_path.parent
else:
output_dir = input_path
converters.convert(input_path,
output_dir,
"HEIC",
"png",
pillow_convert,
True)
def add_parser(subparsers):
parser = subparsers.add_parser("image")
parsers = parser.add_subparsers(required=True)
convert_parser = parsers.add_parser("convert")
convert_parser.add_argument(
"--input",
type=Path,
default=Path(os.getcwd()),
help="Path to run the conversion from",
)
convert_parser.set_defaults(func=cli_image_convert)

View file

@ -0,0 +1,35 @@
from pathlib import Path
import logging
import sys
from PIL import Image
from pillow_heif import HeifImagePlugin
from PIL.ExifTags import TAGS
logger = logging.getLogger(__name__)
def pillow_convert(paths: tuple[Path]):
input_path, output_path = paths
logging.info("Converting %s to %s: ", input_path, output_path)
with Image.open(input_path) as im:
im.save(output_path, exif=im.getexif())
if __name__ == "__main__":
path = sys.argv[1]
image = Image.open(path)
print(image.info)
print(image.getxmp())
exifdata = image.getexif()
print(exifdata)
for tag_id in exifdata:
print(tag_id)
tag = TAGS.get(tag_id,tag_id)
print(tag)
data = exifdata.get(tag_id)
print(data)
# we decode bytes
if isinstance(data,bytes):
data = data.decode()
print(f'{tag:25}: {data}')

View file

@ -1,57 +1,24 @@
import argparse import argparse
import logging import logging
from pathlib import Path from pathlib import Path
import os
import jgutils.filesystem.cli
import jgutils.image.cli
import jgutils.music.cli
from jgutils import music
from jgutils.serialization import write_model
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def cli_music_read(args):
collection = music.collection.read(args.collection.resolve())
write_model(collection)
def cli_music_refresh(args):
music.refresh(args.collection.resolve(),
args.work_dir.resolve())
def main_cli(): def main_cli():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(required=True) subparsers = parser.add_subparsers(required=True)
music_parser = subparsers.add_parser("music") jgutils.filesystem.cli.add_parser(subparsers)
music_subparsers = music_parser.add_subparsers(required=True) jgutils.image.cli.add_parser(subparsers)
jgutils.music.cli.add_parser(subparsers)
music_read_parser = music_subparsers.add_parser("read")
music_read_parser.add_argument(
"--collection",
type=Path,
default=Path(),
help="Directory with input music files.",
)
music_read_parser.add_argument(
"--output_path",
type=Path,
default=Path() / "music_collection.json",
help="Path to save collection info json to.",
)
music_read_parser.set_defaults(func=cli_music_read)
music_refresh_parser = music_subparsers.add_parser("refresh")
music_refresh_parser.add_argument(
"--collection",
type=Path,
default=Path(),
help="Directory with the music collection.",
)
music_refresh_parser.add_argument(
"--work_dir",
type=Path,
default=Path(),
help="Directory for intermediate storage.",
)
music_refresh_parser.set_defaults(func=cli_music_refresh)
logging.basicConfig() logging.basicConfig()
logging.getLogger().setLevel(logging.INFO) logging.getLogger().setLevel(logging.INFO)

48
src/jgutils/music/cli.py Normal file
View file

@ -0,0 +1,48 @@
from pathlib import Path
from jgutils.music import collection
from jgutils.serialization import write_model
def cli_music_read(args):
collection = collection.read(args.collection.resolve())
write_model(collection)
def cli_music_refresh(args):
collection.refresh(args.collection.resolve(),
args.work_dir.resolve())
def add_parser(subparsers):
parser = subparsers.add_parser("music")
parsers = parser.add_subparsers(required=True)
read_parser = parsers.add_parser("read")
read_parser.add_argument(
"--collection",
type=Path,
default=Path(),
help="Directory with input music files.",
)
read_parser.add_argument(
"--output_path",
type=Path,
default=Path() / "music_collection.json",
help="Path to save collection info json to.",
)
read_parser.set_defaults(func=cli_music_read)
refresh_parser = parsers.add_parser("refresh")
refresh_parser.add_argument(
"--collection",
type=Path,
default=Path(),
help="Directory with the music collection.",
)
refresh_parser.add_argument(
"--work_dir",
type=Path,
default=Path(),
help="Directory for intermediate storage.",
)
refresh_parser.set_defaults(func=cli_music_refresh)

View file

@ -1,4 +0,0 @@
for f in $(find . -name '*.HEIC');
do echo "Converting $f";
/home/jgrogan/code/ImageMagick-7.1.1-35/utilities/magick mogrify -format png -quality 100% "$f";
done;