"""Preset loader — reads JSON config files from the presets directory.""" from __future__ import annotations import json import logging from pathlib import Path from typing import Any logger = logging.getLogger(__name__) _PRESETS_DIR = Path(__file__).parent _VALID_SECTIONS = {"preprocessing", "vectorization", "postprocessing"} # In-memory cache — loaded once per process, cleared on reload(). _cache: dict[str, dict[str, Any]] = {} def _load_all() -> dict[str, dict[str, Any]]: """Scan the presets directory and load every .json file.""" presets: dict[str, dict[str, Any]] = {} for p in sorted(_PRESETS_DIR.glob("*.json")): try: data = json.loads(p.read_text()) name = data.get("name", p.stem) presets[name] = data except (json.JSONDecodeError, OSError) as exc: logger.warning("Skipping invalid preset file %s: %s", p, exc) return presets def all_presets() -> dict[str, dict[str, Any]]: """Return all presets, loading from disk on first call. Returns: Dict mapping preset name → full preset config dict. """ if not _cache: _cache.update(_load_all()) return dict(_cache) def get_preset(name: str) -> dict[str, Any] | None: """Return a single preset by name, or None if not found.""" presets = all_presets() return presets.get(name) def preset_names() -> list[str]: """Return sorted list of available preset names.""" return sorted(all_presets().keys()) def resolve_params( preset_name: str, user_params: dict[str, Any] | None = None, ) -> dict[str, Any]: """Resolve effective parameters by merging preset defaults with user overrides. The preset provides three sections: preprocessing, vectorization, postprocessing. User params are applied as a flat overlay — keys in user_params override matching keys from the preset at any level. Special keys: - ``mode``: if present in user_params, overrides ``vectorization.mode``. - ``epsilon``: if present in user_params, overrides ``postprocessing.epsilon``. Returns: Merged param dict with top-level keys for preprocessing, vectorization mode, vectorizer-specific params, and postprocessing epsilon. """ user = user_params or {} preset = get_preset(preset_name) if preset is None: # No preset found — fall back to raw user params. return { "preprocessing": {}, "vectorization_mode": user.get("mode", "potrace"), "vectorizer_params": {k: v for k, v in user.items() if k != "mode"}, "postprocessing": {"epsilon": float(user.get("epsilon", 1.0))}, } # -- Preprocessing: preset defaults + user overrides -- pre = dict(preset.get("preprocessing", {})) for key in list(pre.keys()): if key in user: pre[key] = user[key] # -- Vectorization mode -- vec_cfg = preset.get("vectorization", {}) mode = user.get("mode", vec_cfg.get("mode", "potrace")) # -- Vectorizer-specific params: preset defaults + user overrides -- vec_params = dict(vec_cfg.get(mode, vec_cfg.get("potrace", {}))) # Apply any user overrides that match vectorizer param names. for key in list(vec_params.keys()): if key in user: vec_params[key] = user[key] # -- Postprocessing -- post = dict(preset.get("postprocessing", {})) if "epsilon" in user: post["epsilon"] = float(user["epsilon"]) return { "preprocessing": pre, "vectorization_mode": mode, "vectorizer_params": vec_params, "postprocessing": post, } def reload() -> None: """Clear the preset cache and reload from disk.""" _cache.clear() _cache.update(_load_all())