import json import logging from typing import List, Optional from core.models import Template logger = logging.getLogger(__name__) _TEMPLATES_FILENAME = "templates.json" # --------------------------------------------------------------------------- # 内置模板(代码定义,不可删除/修改) # --------------------------------------------------------------------------- BUILTIN_TEMPLATES: List[Template] = [ Template( name="正方形模板", x_ratio=0.40, y_ratio=0.40, width_ratio=0.25, height_ratio=0.25, rotation=0.0, type="builtin", ), Template( name="纵向长方形模板", x_ratio=0.435, y_ratio=0.385, width_ratio=0.20, height_ratio=0.30, rotation=0.0, type="builtin", ), Template( name="横向长方形模板", x_ratio=0.385, y_ratio=0.435, width_ratio=0.30, height_ratio=0.20, rotation=0.0, type="builtin", ), Template( name="左胸小号模板", x_ratio=0.55, y_ratio=0.30, width_ratio=0.16, height_ratio=0.16, rotation=0.0, type="builtin", ), ] # --------------------------------------------------------------------------- # 内部辅助 # --------------------------------------------------------------------------- def _templates_file(): from services.file_service import get_config_path return get_config_path(_TEMPLATES_FILENAME) def _validate_entry(data) -> Optional[Template]: """Parse one raw dict into a Template; return None and log on any error.""" try: name = str(data["name"]).strip() if not name: raise ValueError("name is empty") w = float(data["width_ratio"]) h = float(data["height_ratio"]) if w <= 0 or h <= 0: raise ValueError("width_ratio and height_ratio must be > 0") return Template( name=name, x_ratio=float(data.get("x_ratio", 0.0)), y_ratio=float(data.get("y_ratio", 0.0)), width_ratio=w, height_ratio=h, rotation=float(data.get("rotation", 0.0)), type="custom", ) except (KeyError, ValueError, TypeError) as exc: logger.warning("Skipping invalid template entry (%s): %s", exc, data) return None def _save_custom_templates(templates: List[Template]): """Write custom templates to JSON. Logs error on failure, does not raise.""" path = _templates_file() try: path.parent.mkdir(parents=True, exist_ok=True) payload = { "templates": [ { "name": t.name, "x_ratio": t.x_ratio, "y_ratio": t.y_ratio, "width_ratio": t.width_ratio, "height_ratio": t.height_ratio, "rotation": t.rotation, } for t in templates ] } with open(str(path), "w", encoding="utf-8") as f: json.dump(payload, f, ensure_ascii=False, indent=2) logger.info("Saved %d custom template(s) to %s", len(templates), path) except OSError as exc: logger.error("Failed to save templates: %s", exc) # --------------------------------------------------------------------------- # 公开 API # --------------------------------------------------------------------------- def get_builtin_templates() -> List[Template]: """Return a copy of the built-in template list.""" return list(BUILTIN_TEMPLATES) def load_custom_templates() -> List[Template]: """Load user-defined templates from JSON. Returns [] on missing file. Skips corrupt individual entries so a single bad record never blocks the rest. """ path = _templates_file() if not path.exists(): return [] try: with open(str(path), encoding="utf-8") as f: data = json.load(f) except (json.JSONDecodeError, OSError) as exc: logger.warning("Cannot read templates.json (%s), ignoring.", exc) return [] if not isinstance(data, dict) or "templates" not in data: logger.warning("templates.json has unexpected structure, ignoring.") return [] results = [] for entry in data["templates"]: t = _validate_entry(entry) if t is not None: results.append(t) logger.info("Loaded %d custom template(s) from %s", len(results), path) return results def get_all_templates() -> List[Template]: """Return builtin templates followed by custom templates.""" return get_builtin_templates() + load_custom_templates() def add_template(template: Template): """Add or overwrite a custom template, then persist. If a custom template with the same name already exists it is replaced. Built-in templates cannot be added via this function. """ template.type = "custom" customs = load_custom_templates() customs = [t for t in customs if t.name != template.name] customs.append(template) _save_custom_templates(customs) logger.info("Template added: %s", template.name) def rename_template(old_name: str, new_name: str): """Rename a custom template and persist. Raises ValueError if *old_name* is not found or *new_name* is empty. """ new_name = new_name.strip() if not new_name: raise ValueError("New template name cannot be empty") customs = load_custom_templates() found = False for t in customs: if t.name == old_name: t.name = new_name found = True break if not found: raise ValueError("Custom template not found: {}".format(old_name)) _save_custom_templates(customs) logger.info("Template renamed: %s -> %s", old_name, new_name) def delete_template(name: str): """Delete a custom template by name and persist. Raises ValueError if *name* is not found among custom templates. """ customs = load_custom_templates() filtered = [t for t in customs if t.name != name] if len(filtered) == len(customs): raise ValueError("Custom template not found: {}".format(name)) _save_custom_templates(filtered) logger.info("Template deleted: %s", name)