diff --git a/README.md b/README.md index 64d342f..070c7fa 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,11 @@ Command-line tools for organising photos by date, managing RAW/JPG pairs, and op ## Supported formats -- RAW: .raf -- JPG: .jpg, .jpeg +- **RAW**: `.cr2`, `.cr3`, `.nef`, `.arw`, `.raf`, `.orf`, `.rw2`, `.dng`, `.pef`, `.srw`, `.x3f` +- JPG: `.jpg`, `.jpeg` + + +> ⚠️ **Note:** Detection is based on file extensions only (case-insensitive). Files with incorrect or missing extensions may not be handled correctly. ## Prerequisites - System Tools diff --git a/src/photo_tools/commands/clean_unpaired_raws.py b/src/photo_tools/commands/clean_unpaired_raws.py index cc9cc1c..0d06a0e 100644 --- a/src/photo_tools/commands/clean_unpaired_raws.py +++ b/src/photo_tools/commands/clean_unpaired_raws.py @@ -6,9 +6,6 @@ logger = logging.getLogger(__name__) -RAW_EXTENSIONS = {".raf"} -JPG_EXTENSIONS = {".jpg", ".jpeg"} - Reporter = Callable[[str, str], None] RawMatcher = Callable[[Path, list[Path]], bool] diff --git a/src/photo_tools/commands/keep_five_star_raws.py b/src/photo_tools/commands/keep_five_star_raws.py index 383fcc0..319d54c 100644 --- a/src/photo_tools/commands/keep_five_star_raws.py +++ b/src/photo_tools/commands/keep_five_star_raws.py @@ -7,8 +7,6 @@ logger = logging.getLogger(__name__) -RAW_EXTENSIONS = {".raf"} -JPG_EXTENSIONS = {".jpg", ".jpeg"} Reporter = Callable[[str, str], None] RawMatcher = Callable[[Path, list[Path]], bool] diff --git a/src/photo_tools/commands/optimise.py b/src/photo_tools/commands/optimise.py index eed0616..a298ba7 100644 --- a/src/photo_tools/commands/optimise.py +++ b/src/photo_tools/commands/optimise.py @@ -5,11 +5,11 @@ from PIL import Image from photo_tools.core.validation import validate_input_dir +from photo_tools.image.file_types import is_jpg from photo_tools.image.optimisation import optimise_jpeg, resize_to_max_width logger = logging.getLogger(__name__) -IMAGE_EXTENSIONS = {".jpg", ".jpeg"} MAX_WIDTH = 2500 MAX_FILE_SIZE_BYTES = 500 * 1024 @@ -34,10 +34,7 @@ def optimise( failed_count = 0 for file_path in input_path.iterdir(): - if not file_path.is_file(): - continue - - if file_path.suffix.lower() not in IMAGE_EXTENSIONS: + if not is_jpg(file_path): logger.debug("Skipping (not a supported image): %s", file_path.name) continue diff --git a/src/photo_tools/commands/organise_by_date.py b/src/photo_tools/commands/organise_by_date.py index 06a5e68..83576d5 100644 --- a/src/photo_tools/commands/organise_by_date.py +++ b/src/photo_tools/commands/organise_by_date.py @@ -4,16 +4,11 @@ from pathlib import Path from photo_tools.core.validation import validate_input_dir +from photo_tools.image.file_types import is_jpg, is_raw from photo_tools.image.metadata import get_image_date logger = logging.getLogger(__name__) -IMAGE_EXTENSIONS = { - ".jpg", - ".jpeg", - ".raf", -} - Reporter = Callable[[str, str], None] @@ -37,10 +32,7 @@ def organise_by_date( cleaned_suffix = suffix.strip() if suffix and suffix.strip() else None for file_path in input_path.iterdir(): - if not file_path.is_file(): - continue - - if file_path.suffix.lower() not in IMAGE_EXTENSIONS: + if not (is_jpg(file_path) or is_raw(file_path)): logger.debug("Skipping unsupported file: %s", file_path.name) continue @@ -85,7 +77,6 @@ def organise_by_date( report("info", f"Moved {file_path.name} -> {target_dir}") # Summary - if dry_run: report("summary", f"Dry run complete: would move {dry_run_count} file(s)") else: diff --git a/src/photo_tools/commands/separate_raws.py b/src/photo_tools/commands/separate_raws.py index b900da2..37686ef 100644 --- a/src/photo_tools/commands/separate_raws.py +++ b/src/photo_tools/commands/separate_raws.py @@ -4,10 +4,10 @@ from pathlib import Path from photo_tools.core.validation import validate_input_dir +from photo_tools.image.file_types import is_raw logger = logging.getLogger(__name__) -RAW_EXTENSIONS = {".raf"} OUTPUT_DIR = "raws" Reporter = Callable[[str, str], None] @@ -29,10 +29,7 @@ def separate_raws( skipped_existing_count = 0 for file_path in input_path.iterdir(): - if not file_path.is_file(): - continue - - if file_path.suffix.lower() not in RAW_EXTENSIONS: + if not is_raw(file_path): logger.debug("Skipping (not RAW): %s", file_path.name) continue diff --git a/src/photo_tools/image/file_types.py b/src/photo_tools/image/file_types.py new file mode 100644 index 0000000..13d4e9d --- /dev/null +++ b/src/photo_tools/image/file_types.py @@ -0,0 +1,25 @@ +from pathlib import Path + +RAW_EXTENSIONS = { + ".cr2", + ".cr3", + ".nef", + ".arw", + ".raf", + ".orf", + ".rw2", + ".dng", + ".pef", + ".srw", + ".x3f", +} + +JPG_EXTENSIONS = {".jpg", ".jpeg"} + + +def is_raw(file_path: Path) -> bool: + return file_path.is_file() and file_path.suffix.lower() in RAW_EXTENSIONS + + +def is_jpg(file_path: Path) -> bool: + return file_path.is_file() and file_path.suffix.lower() in JPG_EXTENSIONS diff --git a/src/photo_tools/image/raw_utils.py b/src/photo_tools/image/raw_utils.py index 485daef..4774ec2 100644 --- a/src/photo_tools/image/raw_utils.py +++ b/src/photo_tools/image/raw_utils.py @@ -4,11 +4,10 @@ from pathlib import Path from photo_tools.core.validation import validate_input_dir +from photo_tools.image.file_types import is_jpg, is_raw logger = logging.getLogger(__name__) -RAW_EXTENSIONS = {".raf"} -JPG_EXTENSIONS = {".jpg", ".jpeg"} Reporter = Callable[[str, str], None] RawMatcher = Callable[[Path, list[Path]], bool] @@ -34,17 +33,10 @@ def move_raws_by_rule( dry_run_count = 0 skipped_existing_count = 0 - jpg_files = [ - f - for f in jpg_path.iterdir() - if f.is_file() and f.suffix.lower() in JPG_EXTENSIONS - ] + jpg_files = [f for f in jpg_path.iterdir() if is_jpg(f)] for raw_file in raw_path.iterdir(): - if not raw_file.is_file(): - continue - - if raw_file.suffix.lower() not in RAW_EXTENSIONS: + if not is_raw(raw_file): continue if not should_move(raw_file, jpg_files): diff --git a/tests/image/test_file_types.py b/tests/image/test_file_types.py new file mode 100644 index 0000000..bb7a456 --- /dev/null +++ b/tests/image/test_file_types.py @@ -0,0 +1,118 @@ +from pathlib import Path + +import pytest + +from photo_tools.image.raw_utils import is_jpg, is_raw + + +@pytest.mark.parametrize( + ("filename"), + [ + "image.cr2", + "image.CR2", + "image.cr3", + "image.nef", + "image.NeF", + "image.arw", + "image.raf", + "image.orf", + "image.rw2", + "image.dng", + "image.pef", + "image.srw", + "image.x3f", + ], +) +def test_is_raw_returns_true_for_supported_raw_files( + tmp_path: Path, + filename: str, +) -> None: + file_path = tmp_path / filename + file_path.touch() + + assert is_raw(file_path) is True + + +@pytest.mark.parametrize( + ("filename"), + [ + "image.jpg", + "image.jpeg", + "image.png", + "image.txt", + "image", + ], +) +def test_is_raw_returns_false_for_non_raw_files( + tmp_path: Path, + filename: str, +) -> None: + file_path = tmp_path / filename + file_path.touch() + + assert is_raw(file_path) is False + + +def test_is_raw_returns_false_for_directory(tmp_path: Path) -> None: + dir_path = tmp_path / "image.cr2" + dir_path.mkdir() + + assert is_raw(dir_path) is False + + +def test_is_raw_returns_false_for_non_existing_path(tmp_path: Path) -> None: + file_path = tmp_path / "missing.cr2" + + assert is_raw(file_path) is False + + +@pytest.mark.parametrize( + ("filename"), + [ + "image.jpg", + "image.jpeg", + "image.JPG", + "image.JPEG", + ], +) +def test_is_jpg_returns_true_for_supported_jpg_files( + tmp_path: Path, + filename: str, +) -> None: + file_path = tmp_path / filename + file_path.touch() + + assert is_jpg(file_path) is True + + +@pytest.mark.parametrize( + ("filename"), + [ + "image.cr2", + "image.nef", + "image.png", + "image.txt", + "image", + ], +) +def test_is_jpg_returns_false_for_non_jpg_files( + tmp_path: Path, + filename: str, +) -> None: + file_path = tmp_path / filename + file_path.touch() + + assert is_jpg(file_path) is False + + +def test_is_jpg_returns_false_for_directory(tmp_path: Path) -> None: + dir_path = tmp_path / "image.jpg" + dir_path.mkdir() + + assert is_jpg(dir_path) is False + + +def test_is_jpg_returns_false_for_non_existing_path(tmp_path: Path) -> None: + file_path = tmp_path / "missing.jpg" + + assert is_jpg(file_path) is False