Add offline mosaic EXIF tagging (stitch --write-exif, tag_mosaic_exif CLI)
- spruce.exif: tag_mosaic_jpeg_for_scan_dir, resolve_machine_label_for_scan_dir; ProcessingSoftware for tile-stitched mosaics - spruce.settings: load_config(require_credentials=False) for config without login - scripts/tag_mosaic_exif.py and tests; stitch script --write-exif path
This commit is contained in:
@@ -26,6 +26,9 @@ if str(_REPO_ROOT) not in sys.path:
|
||||
|
||||
from PIL import Image, ImageChops
|
||||
|
||||
from spruce.exif import tag_mosaic_jpeg_for_scan_dir
|
||||
from spruce.settings import DEFAULT_CONFIG, load_config
|
||||
|
||||
TILE_FILENAME_RE = re.compile(r"tile_r(\d+)_c(\d+)\.jpg$", re.IGNORECASE)
|
||||
|
||||
|
||||
@@ -281,6 +284,20 @@ def main() -> None:
|
||||
default=95,
|
||||
help="JPEG quality for output (default: 95).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--write-exif",
|
||||
action="store_true",
|
||||
help=(
|
||||
"After saving, write mosaic EXIF using metadata.json and config "
|
||||
"(implies override if write_exif is false in config). JPEG output only."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--config",
|
||||
default=DEFAULT_CONFIG,
|
||||
metavar="FILE",
|
||||
help=f"YAML config for EXIF machine_metadata (default: {DEFAULT_CONFIG})",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--tile-gap",
|
||||
type=int,
|
||||
@@ -339,6 +356,20 @@ def main() -> None:
|
||||
f"grid {nx}x{ny}, tile_gap={args.tile_gap})"
|
||||
)
|
||||
|
||||
if args.write_exif:
|
||||
if out.suffix.lower() not in (".jpg", ".jpeg"):
|
||||
raise SystemExit("--write-exif requires a .jpg or .jpeg output path.")
|
||||
cfg_path = Path(args.config).expanduser()
|
||||
if not cfg_path.is_file():
|
||||
raise SystemExit(f"Config not found: {cfg_path}")
|
||||
config = load_config(str(cfg_path), require_credentials=False)
|
||||
try:
|
||||
ok = tag_mosaic_jpeg_for_scan_dir(scan_dir, out, config, force=True)
|
||||
except ValueError as exc:
|
||||
raise SystemExit(str(exc)) from exc
|
||||
if not ok:
|
||||
raise SystemExit("EXIF tagging failed (see log).")
|
||||
|
||||
if args.compare_mosaic:
|
||||
compare_mosaics(canvas, scan_dir / "mosaic.jpg", fit=args.fit)
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Write mosaic EXIF into a JPEG using metadata.json and config machine_metadata.
|
||||
|
||||
Usage:
|
||||
python scripts/tag_mosaic_exif.py /path/to/scan_dir
|
||||
python scripts/tag_mosaic_exif.py /path/to/scan_dir --jpeg mosaic.jpg --machine "BW1-6 [AMR-19]"
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
_REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
if str(_REPO_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(_REPO_ROOT))
|
||||
|
||||
from spruce.exif import tag_mosaic_jpeg_for_scan_dir
|
||||
from spruce.settings import DEFAULT_CONFIG, load_config
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s %(levelname)-8s %(message)s",
|
||||
datefmt="%H:%M:%S",
|
||||
)
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Insert RootView-style mosaic EXIF (piexif, no re-encode) using "
|
||||
"scan_dir/metadata.json and machine_metadata from config."
|
||||
)
|
||||
)
|
||||
parser.add_argument(
|
||||
"scan_dir",
|
||||
type=Path,
|
||||
help="Directory containing metadata.json (…/<machine>/<date>/<scan_id>/)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--jpeg",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="JPEG to tag (default: <scan_dir>/mosaic_reconstructed.jpg)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--config",
|
||||
default=DEFAULT_CONFIG,
|
||||
metavar="FILE",
|
||||
help=f"YAML config (default: {DEFAULT_CONFIG})",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--machine",
|
||||
metavar="LABEL",
|
||||
default=None,
|
||||
help='RootView machine label, e.g. "BW1-6 [AMR-19]" (skip archive slug inference)',
|
||||
)
|
||||
parser.add_argument(
|
||||
"--force",
|
||||
action="store_true",
|
||||
help="Tag even when write_exif is false in config",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--processing-software",
|
||||
default=None,
|
||||
metavar="STR",
|
||||
help="Override ProcessingSoftware EXIF string",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
scan_dir = args.scan_dir.expanduser().resolve()
|
||||
if not scan_dir.is_dir():
|
||||
sys.exit(f"Not a directory: {scan_dir}")
|
||||
|
||||
jpeg = args.jpeg
|
||||
if jpeg is None:
|
||||
jpeg = scan_dir / "mosaic_reconstructed.jpg"
|
||||
else:
|
||||
jpeg = jpeg.expanduser().resolve()
|
||||
|
||||
if not jpeg.is_file():
|
||||
sys.exit(f"JPEG not found: {jpeg}")
|
||||
|
||||
cfg_path = Path(args.config).expanduser()
|
||||
if not cfg_path.is_file():
|
||||
sys.exit(f"Config not found: {cfg_path}")
|
||||
|
||||
config = load_config(str(cfg_path), require_credentials=False)
|
||||
|
||||
try:
|
||||
ok = tag_mosaic_jpeg_for_scan_dir(
|
||||
scan_dir,
|
||||
jpeg,
|
||||
config,
|
||||
machine_label=args.machine,
|
||||
processing_software=args.processing_software,
|
||||
force=args.force,
|
||||
)
|
||||
except ValueError as exc:
|
||||
sys.exit(str(exc))
|
||||
|
||||
if not ok:
|
||||
sys.exit("EXIF tagging was skipped or failed (see log).")
|
||||
log.info("EXIF written: %s", jpeg)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user