2026-06-15 17:01:40 +08:00
|
|
|
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="正方形模板",
|
2026-06-16 15:11:56 +08:00
|
|
|
x_ratio=0.34, y_ratio=0.22,
|
|
|
|
|
width_ratio=0.32, height_ratio=0.32,
|
2026-06-15 17:01:40 +08:00
|
|
|
rotation=0.0, type="builtin",
|
|
|
|
|
),
|
|
|
|
|
Template(
|
|
|
|
|
name="纵向长方形模板",
|
2026-06-16 15:11:56 +08:00
|
|
|
x_ratio=0.35, y_ratio=0.16,
|
|
|
|
|
width_ratio=0.30, height_ratio=0.46,
|
2026-06-15 17:01:40 +08:00
|
|
|
rotation=0.0, type="builtin",
|
|
|
|
|
),
|
|
|
|
|
Template(
|
|
|
|
|
name="横向长方形模板",
|
2026-06-16 15:11:56 +08:00
|
|
|
x_ratio=0.27, y_ratio=0.26,
|
|
|
|
|
width_ratio=0.46, height_ratio=0.30,
|
2026-06-15 17:01:40 +08:00
|
|
|
rotation=0.0, type="builtin",
|
|
|
|
|
),
|
|
|
|
|
Template(
|
|
|
|
|
name="左胸小号模板",
|
2026-06-16 15:11:56 +08:00
|
|
|
x_ratio=0.50, y_ratio=0.22,
|
|
|
|
|
width_ratio=0.16, height_ratio=0.16,
|
2026-06-15 17:01:40 +08:00
|
|
|
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(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)
|