Add EXIF Editor, consolidate channel key resolution

EXIF Editor (Library → CLI → API → WebUI):
- src/stegasoo/utils.py: read_image_exif(), write_image_exif()
- CLI: stegasoo tools exif [--clear|--set Field=Value]
- API: /api/tools/exif, /api/tools/exif/update, /api/tools/exif/clear
- WebUI: EXIF Editor tab with inline editing, clear all, save/download

Architectural consolidation:
- Moved resolve_channel_key() to src/stegasoo/channel.py (was duplicated in 3 frontends)
- Added get_channel_response_info() for consistent API/WebUI responses
- Frontends now use thin wrappers that translate exceptions

DCT improvements:
- Added will_fit_by_mode() pre-check to WebUI encode (fail fast)
- Suggests LSB mode when DCT capacity exceeded

Dependencies:
- Added piexif>=1.1.0 for EXIF editing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Aaron D. Lee
2026-01-04 01:16:33 -05:00
parent 5df9b9dac8
commit 01f0173dd4
9 changed files with 807 additions and 97 deletions

View File

@@ -49,7 +49,6 @@ from stegasoo import (
generate_credentials,
get_channel_status,
has_argon2,
has_channel_key,
has_dct_support,
set_channel_key,
validate_channel_key,
@@ -406,11 +405,7 @@ def _resolve_channel_key(channel_key: str | None) -> str | None:
"""
Resolve channel key from API parameter.
Args:
channel_key: API parameter value
- None: Use server-configured key (auto mode)
- "": Public mode (no channel key)
- "XXXX-...": Explicit key
Wrapper around library's resolve_channel_key with HTTP exception handling.
Returns:
Resolved channel key to pass to encode/decode
@@ -418,44 +413,27 @@ def _resolve_channel_key(channel_key: str | None) -> str | None:
Raises:
HTTPException: If key format is invalid
"""
if channel_key is None:
# Auto mode - use server config
return None
from stegasoo.channel import resolve_channel_key
if channel_key == "":
# Public mode
return ""
# Explicit key - validate format
if not validate_channel_key(channel_key):
raise HTTPException(
400, "Invalid channel key format. Expected: XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX"
)
return channel_key
try:
return resolve_channel_key(channel_key)
except (ValueError, FileNotFoundError) as e:
raise HTTPException(400, str(e))
def _get_channel_info(channel_key: str | None) -> tuple[str, str | None]:
"""
Get channel mode and fingerprint for response.
Uses library's get_channel_response_info for consistent formatting.
Returns:
(mode, fingerprint) tuple
"""
if channel_key == "":
return "public", None
from stegasoo.channel import get_channel_response_info
if channel_key is not None:
# Explicit key
fingerprint = f"{channel_key[:4]}-••••-••••-••••-••••-••••-••••-{channel_key[-4:]}"
return "private", fingerprint
# Auto mode - check server config
if has_channel_key():
status = get_channel_status()
return "private", status.get("fingerprint")
return "public", None
info = get_channel_response_info(channel_key)
return info["mode"], info.get("fingerprint")
# ============================================================================