Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| abb081f07b |
@@ -10,233 +10,28 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ── 预设滤镜包 ────────────────────────────────────────────────────────────────
|
||||
|
||||
# 预设名称常量
|
||||
PRESET_FRESH = "fresh" # 清新
|
||||
PRESET_JAPANESE = "japanese" # 日系
|
||||
PRESET_VINTAGE = "vintage" # 复古
|
||||
PRESET_CINEMA = "cinema" # 电影
|
||||
PRESET_FILM = "film" # 胶片
|
||||
PRESET_BW = "black_white" # 黑白
|
||||
PRESET_WARM = "warm" # 暖色
|
||||
PRESET_COOL = "cool" # 冷色
|
||||
|
||||
VALID_PRESETS = {
|
||||
from packages.domain.color_grade_config import ( # noqa: F401 — 向后兼容
|
||||
DEFAULT_PARAMS,
|
||||
PARAM_RANGES,
|
||||
PRESET_BW,
|
||||
PRESET_CINEMA,
|
||||
PRESET_COOL,
|
||||
PRESET_DISPLAY_NAMES,
|
||||
PRESET_FILM,
|
||||
PRESET_FRESH,
|
||||
PRESET_JAPANESE,
|
||||
PRESET_PARAMS,
|
||||
PRESET_VINTAGE,
|
||||
PRESET_CINEMA,
|
||||
PRESET_FILM,
|
||||
PRESET_BW,
|
||||
PRESET_WARM,
|
||||
PRESET_COOL,
|
||||
}
|
||||
VALID_PRESETS,
|
||||
ColorGradeConfig,
|
||||
clamp_param,
|
||||
get_preset_names,
|
||||
get_preset_params,
|
||||
)
|
||||
|
||||
# 预设名称 → 中文显示名
|
||||
PRESET_DISPLAY_NAMES = {
|
||||
PRESET_FRESH: "清新",
|
||||
PRESET_JAPANESE: "日系",
|
||||
PRESET_VINTAGE: "复古",
|
||||
PRESET_CINEMA: "电影",
|
||||
PRESET_FILM: "胶片",
|
||||
PRESET_BW: "黑白",
|
||||
PRESET_WARM: "暖色",
|
||||
PRESET_COOL: "冷色",
|
||||
}
|
||||
|
||||
# 预设参数配置
|
||||
# 每个预设包含:brightness, contrast, saturation, temperature, hue
|
||||
# 取值范围:brightness/contrast/temperature -100~100, saturation 0~200, hue -180~180
|
||||
PRESET_PARAMS: dict[str, dict[str, float]] = {
|
||||
PRESET_FRESH: {
|
||||
# 清新:提亮、高饱和、偏冷、微微调
|
||||
"brightness": 8,
|
||||
"contrast": 10,
|
||||
"saturation": 120,
|
||||
"temperature": -8,
|
||||
"hue": 5,
|
||||
},
|
||||
PRESET_JAPANESE: {
|
||||
# 日系:低对比、低饱和、偏暖、偏黄绿
|
||||
"brightness": 12,
|
||||
"contrast": -15,
|
||||
"saturation": 70,
|
||||
"temperature": 10,
|
||||
"hue": -5,
|
||||
},
|
||||
PRESET_VINTAGE: {
|
||||
# 复古:低饱和、偏黄、对比度适中、偏暖
|
||||
"brightness": -5,
|
||||
"contrast": 5,
|
||||
"saturation": 60,
|
||||
"temperature": 25,
|
||||
"hue": -8,
|
||||
},
|
||||
PRESET_CINEMA: {
|
||||
# 电影:高对比、低饱和、偏冷蓝、暗角感
|
||||
"brightness": -8,
|
||||
"contrast": 20,
|
||||
"saturation": 75,
|
||||
"temperature": -15,
|
||||
"hue": -3,
|
||||
},
|
||||
PRESET_FILM: {
|
||||
# 胶片:中对比、饱和适中、偏暖、颗粒感(这里只用调色模拟)
|
||||
"brightness": -3,
|
||||
"contrast": 12,
|
||||
"saturation": 95,
|
||||
"temperature": 15,
|
||||
"hue": -2,
|
||||
},
|
||||
PRESET_BW: {
|
||||
# 黑白:饱和度为0,对比度略高
|
||||
"brightness": 0,
|
||||
"contrast": 15,
|
||||
"saturation": 0,
|
||||
"temperature": 0,
|
||||
"hue": 0,
|
||||
},
|
||||
PRESET_WARM: {
|
||||
# 暖色:高色温、偏红黄
|
||||
"brightness": 5,
|
||||
"contrast": 8,
|
||||
"saturation": 110,
|
||||
"temperature": 30,
|
||||
"hue": -5,
|
||||
},
|
||||
PRESET_COOL: {
|
||||
# 冷色:低色温、偏蓝青
|
||||
"brightness": 3,
|
||||
"contrast": 8,
|
||||
"saturation": 105,
|
||||
"temperature": -25,
|
||||
"hue": 8,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ── 参数范围 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
PARAM_RANGES = {
|
||||
"brightness": (-100.0, 100.0),
|
||||
"contrast": (-100.0, 100.0),
|
||||
"saturation": (0.0, 200.0),
|
||||
"temperature": (-100.0, 100.0),
|
||||
"hue": (-180.0, 180.0),
|
||||
}
|
||||
|
||||
# 默认值(零调整)
|
||||
DEFAULT_PARAMS = {
|
||||
"brightness": 0.0,
|
||||
"contrast": 0.0,
|
||||
"saturation": 100.0,
|
||||
"temperature": 0.0,
|
||||
"hue": 0.0,
|
||||
}
|
||||
|
||||
|
||||
# ── 数据模型 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@dataclass
|
||||
class ColorGradeConfig:
|
||||
"""色彩调色配置.
|
||||
|
||||
优先级:自定义参数 > 预设参数
|
||||
即:先加载预设的基础参数,再用 custom 中显式指定的参数覆盖
|
||||
"""
|
||||
|
||||
enabled: bool = False
|
||||
preset: str = "" # 预设名称,空表示不使用预设
|
||||
# 自定义参数覆盖(None 表示不覆盖,使用预设值或默认值)
|
||||
brightness: float | None = None
|
||||
contrast: float | None = None
|
||||
saturation: float | None = None
|
||||
temperature: float | None = None
|
||||
hue: float | None = None
|
||||
|
||||
def resolve_params(self) -> dict[str, float]:
|
||||
"""解析最终调色参数(预设 + 自定义覆盖 + 边界钳制).
|
||||
|
||||
Returns:
|
||||
包含 brightness, contrast, saturation, temperature, hue 的参数字典
|
||||
"""
|
||||
# 1. 从默认值开始
|
||||
params = dict(DEFAULT_PARAMS)
|
||||
|
||||
# 2. 应用预设
|
||||
if self.preset and self.preset in PRESET_PARAMS:
|
||||
params.update(PRESET_PARAMS[self.preset])
|
||||
|
||||
# 3. 应用自定义覆盖
|
||||
if self.brightness is not None:
|
||||
params["brightness"] = self.brightness
|
||||
if self.contrast is not None:
|
||||
params["contrast"] = self.contrast
|
||||
if self.saturation is not None:
|
||||
params["saturation"] = self.saturation
|
||||
if self.temperature is not None:
|
||||
params["temperature"] = self.temperature
|
||||
if self.hue is not None:
|
||||
params["hue"] = self.hue
|
||||
|
||||
# 4. 边界钳制
|
||||
for key, (min_val, max_val) in PARAM_RANGES.items():
|
||||
params[key] = max(min_val, min(max_val, params[key]))
|
||||
|
||||
return params
|
||||
|
||||
def has_effect(self) -> bool:
|
||||
"""判断是否有实际调色效果(所有参数都是默认值则无效果).
|
||||
|
||||
用于优化:无效果时跳过滤镜,不浪费性能。
|
||||
"""
|
||||
params = self.resolve_params()
|
||||
for key, default in DEFAULT_PARAMS.items():
|
||||
if abs(params[key] - default) > 0.001:
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any] | None) -> "ColorGradeConfig":
|
||||
"""从字典解析配置."""
|
||||
if not data or not data.get("enabled", False):
|
||||
return cls(enabled=False)
|
||||
|
||||
preset = data.get("preset", "")
|
||||
if preset and preset not in VALID_PRESETS:
|
||||
logger.warning("未知的调色预设: %s,忽略预设", preset)
|
||||
preset = ""
|
||||
|
||||
def _get_float(key: str) -> float | None:
|
||||
val = data.get(key)
|
||||
if val is None:
|
||||
return None
|
||||
try:
|
||||
return float(val)
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
try:
|
||||
return cls(
|
||||
enabled=True,
|
||||
preset=preset,
|
||||
brightness=_get_float("brightness"),
|
||||
contrast=_get_float("contrast"),
|
||||
saturation=_get_float("saturation"),
|
||||
temperature=_get_float("temperature"),
|
||||
hue=_get_float("hue"),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning("调色配置解析失败: %s,使用默认配置", e)
|
||||
return cls(enabled=False)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ── 调色引擎 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
Executable
+268
@@ -0,0 +1,268 @@
|
||||
"""色彩调色配置领域模型 — 纯逻辑,无 FFmpeg 依赖.
|
||||
|
||||
抽离自 color_grade_engine.py 的数据类、预设常量和纯逻辑函数,
|
||||
方便单测覆盖,同时保持向后兼容。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ── 预设常量 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
PRESET_FRESH = "fresh" # 清新
|
||||
PRESET_JAPANESE = "japanese" # 日系
|
||||
PRESET_VINTAGE = "vintage" # 复古
|
||||
PRESET_CINEMA = "cinema" # 电影
|
||||
PRESET_FILM = "film" # 胶片
|
||||
PRESET_BW = "black_white" # 黑白
|
||||
PRESET_WARM = "warm" # 暖色
|
||||
PRESET_COOL = "cool" # 冷色
|
||||
|
||||
VALID_PRESETS = {
|
||||
PRESET_FRESH,
|
||||
PRESET_JAPANESE,
|
||||
PRESET_VINTAGE,
|
||||
PRESET_CINEMA,
|
||||
PRESET_FILM,
|
||||
PRESET_BW,
|
||||
PRESET_WARM,
|
||||
PRESET_COOL,
|
||||
}
|
||||
|
||||
# 预设名称 → 中文显示名
|
||||
PRESET_DISPLAY_NAMES = {
|
||||
PRESET_FRESH: "清新",
|
||||
PRESET_JAPANESE: "日系",
|
||||
PRESET_VINTAGE: "复古",
|
||||
PRESET_CINEMA: "电影",
|
||||
PRESET_FILM: "胶片",
|
||||
PRESET_BW: "黑白",
|
||||
PRESET_WARM: "暖色",
|
||||
PRESET_COOL: "冷色",
|
||||
}
|
||||
|
||||
# 预设参数配置
|
||||
# 每个预设包含:brightness, contrast, saturation, temperature, hue
|
||||
PRESET_PARAMS: dict[str, dict[str, float]] = {
|
||||
PRESET_FRESH: {
|
||||
"brightness": 8,
|
||||
"contrast": 10,
|
||||
"saturation": 120,
|
||||
"temperature": -8,
|
||||
"hue": 5,
|
||||
},
|
||||
PRESET_JAPANESE: {
|
||||
"brightness": 12,
|
||||
"contrast": -15,
|
||||
"saturation": 70,
|
||||
"temperature": 10,
|
||||
"hue": -5,
|
||||
},
|
||||
PRESET_VINTAGE: {
|
||||
"brightness": -5,
|
||||
"contrast": 5,
|
||||
"saturation": 60,
|
||||
"temperature": 25,
|
||||
"hue": -8,
|
||||
},
|
||||
PRESET_CINEMA: {
|
||||
"brightness": -8,
|
||||
"contrast": 20,
|
||||
"saturation": 75,
|
||||
"temperature": -15,
|
||||
"hue": -3,
|
||||
},
|
||||
PRESET_FILM: {
|
||||
"brightness": -3,
|
||||
"contrast": 12,
|
||||
"saturation": 95,
|
||||
"temperature": 15,
|
||||
"hue": -2,
|
||||
},
|
||||
PRESET_BW: {
|
||||
"brightness": 0,
|
||||
"contrast": 15,
|
||||
"saturation": 0,
|
||||
"temperature": 0,
|
||||
"hue": 0,
|
||||
},
|
||||
PRESET_WARM: {
|
||||
"brightness": 5,
|
||||
"contrast": 8,
|
||||
"saturation": 110,
|
||||
"temperature": 30,
|
||||
"hue": -5,
|
||||
},
|
||||
PRESET_COOL: {
|
||||
"brightness": 3,
|
||||
"contrast": 8,
|
||||
"saturation": 105,
|
||||
"temperature": -25,
|
||||
"hue": 8,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ── 参数范围 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
PARAM_RANGES: dict[str, tuple[float, float]] = {
|
||||
"brightness": (-100.0, 100.0),
|
||||
"contrast": (-100.0, 100.0),
|
||||
"saturation": (0.0, 200.0),
|
||||
"temperature": (-100.0, 100.0),
|
||||
"hue": (-180.0, 180.0),
|
||||
}
|
||||
|
||||
# 默认值(零调整)
|
||||
DEFAULT_PARAMS: dict[str, float] = {
|
||||
"brightness": 0.0,
|
||||
"contrast": 0.0,
|
||||
"saturation": 100.0,
|
||||
"temperature": 0.0,
|
||||
"hue": 0.0,
|
||||
}
|
||||
|
||||
ALL_PARAM_KEYS = ("brightness", "contrast", "saturation", "temperature", "hue")
|
||||
|
||||
|
||||
# ── 数据模型 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@dataclass
|
||||
class ColorGradeConfig:
|
||||
"""色彩调色配置.
|
||||
|
||||
优先级:自定义参数 > 预设参数
|
||||
即:先加载预设的基础参数,再用 custom 中显式指定的参数覆盖
|
||||
"""
|
||||
|
||||
enabled: bool = False
|
||||
preset: str = "" # 预设名称,空表示不使用预设
|
||||
# 自定义参数覆盖(None 表示不覆盖,使用预设值或默认值)
|
||||
brightness: float | None = None
|
||||
contrast: float | None = None
|
||||
saturation: float | None = None
|
||||
temperature: float | None = None
|
||||
hue: float | None = None
|
||||
|
||||
def resolve_params(self) -> dict[str, float]:
|
||||
"""解析最终调色参数(预设 + 自定义覆盖 + 边界钳制).
|
||||
|
||||
Returns:
|
||||
包含 brightness, contrast, saturation, temperature, hue 的参数字典
|
||||
"""
|
||||
# 1. 从默认值开始
|
||||
params = dict(DEFAULT_PARAMS)
|
||||
|
||||
# 2. 应用预设
|
||||
if self.preset and self.preset in PRESET_PARAMS:
|
||||
params.update(PRESET_PARAMS[self.preset])
|
||||
|
||||
# 3. 应用自定义覆盖
|
||||
if self.brightness is not None:
|
||||
params["brightness"] = self.brightness
|
||||
if self.contrast is not None:
|
||||
params["contrast"] = self.contrast
|
||||
if self.saturation is not None:
|
||||
params["saturation"] = self.saturation
|
||||
if self.temperature is not None:
|
||||
params["temperature"] = self.temperature
|
||||
if self.hue is not None:
|
||||
params["hue"] = self.hue
|
||||
|
||||
# 4. 边界钳制
|
||||
for key, (min_val, max_val) in PARAM_RANGES.items():
|
||||
params[key] = max(min_val, min(max_val, params[key]))
|
||||
|
||||
return params
|
||||
|
||||
def has_effect(self) -> bool:
|
||||
"""判断是否有实际调色效果(所有参数都是默认值则无效果).
|
||||
|
||||
用于优化:无效果时跳过滤镜,不浪费性能。
|
||||
"""
|
||||
params = self.resolve_params()
|
||||
for key, default in DEFAULT_PARAMS.items():
|
||||
if abs(params[key] - default) > 0.001:
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any] | None) -> "ColorGradeConfig":
|
||||
"""从字典解析配置."""
|
||||
if not data or not data.get("enabled", False):
|
||||
return cls(enabled=False)
|
||||
|
||||
preset = data.get("preset", "")
|
||||
if preset and preset not in VALID_PRESETS:
|
||||
logger.warning("未知的调色预设: %s,忽略预设", preset)
|
||||
preset = ""
|
||||
|
||||
def _get_float(key: str) -> float | None:
|
||||
val = data.get(key)
|
||||
if val is None:
|
||||
return None
|
||||
try:
|
||||
return float(val)
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
try:
|
||||
return cls(
|
||||
enabled=True,
|
||||
preset=preset,
|
||||
brightness=_get_float("brightness"),
|
||||
contrast=_get_float("contrast"),
|
||||
saturation=_get_float("saturation"),
|
||||
temperature=_get_float("temperature"),
|
||||
hue=_get_float("hue"),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning("调色配置解析失败: %s,使用默认配置", e)
|
||||
return cls(enabled=False)
|
||||
|
||||
def validate(self) -> tuple[bool, str]:
|
||||
"""校验配置合法性,返回 (是否合法, 错误信息)."""
|
||||
if not self.enabled:
|
||||
return True, ""
|
||||
|
||||
if self.preset and self.preset not in VALID_PRESETS:
|
||||
return False, f"未知的预设: {self.preset}"
|
||||
|
||||
# 解析后的参数自然在合法范围内(resolve_params 会钳制)
|
||||
# 这里检查是否有明显无效的自定义值
|
||||
for key in ALL_PARAM_KEYS:
|
||||
val = getattr(self, key)
|
||||
if val is not None:
|
||||
min_val, max_val = PARAM_RANGES[key]
|
||||
if val < min_val or val > max_val:
|
||||
return False, f"{key}超出范围[{min_val}, {max_val}]: {val}"
|
||||
|
||||
return True, ""
|
||||
|
||||
|
||||
# ── 工具函数 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def get_preset_names() -> list[tuple[str, str]]:
|
||||
"""获取所有预设的 (name, display_name) 列表."""
|
||||
return [(p, PRESET_DISPLAY_NAMES[p]) for p in sorted(VALID_PRESETS)]
|
||||
|
||||
|
||||
def get_preset_params(preset: str) -> dict[str, float] | None:
|
||||
"""获取指定预设的参数,不存在返回 None."""
|
||||
return PRESET_PARAMS.get(preset)
|
||||
|
||||
|
||||
def clamp_param(param_name: str, value: float) -> float:
|
||||
"""将参数钳制到合法范围内."""
|
||||
if param_name not in PARAM_RANGES:
|
||||
return value
|
||||
min_val, max_val = PARAM_RANGES[param_name]
|
||||
return max(min_val, min(max_val, value))
|
||||
Executable
+356
@@ -0,0 +1,356 @@
|
||||
"""color_grade_config 模块单测 — 纯逻辑,无 FFmpeg 依赖."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from packages.domain.color_grade_config import (
|
||||
ALL_PARAM_KEYS,
|
||||
DEFAULT_PARAMS,
|
||||
PARAM_RANGES,
|
||||
PRESET_BW,
|
||||
PRESET_CINEMA,
|
||||
PRESET_COOL,
|
||||
PRESET_DISPLAY_NAMES,
|
||||
PRESET_FILM,
|
||||
PRESET_FRESH,
|
||||
PRESET_JAPANESE,
|
||||
PRESET_PARAMS,
|
||||
PRESET_VINTAGE,
|
||||
PRESET_WARM,
|
||||
VALID_PRESETS,
|
||||
ColorGradeConfig,
|
||||
clamp_param,
|
||||
get_preset_names,
|
||||
get_preset_params,
|
||||
)
|
||||
|
||||
# ── 常量 ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestConstants:
|
||||
def test_eight_presets(self):
|
||||
assert len(VALID_PRESETS) == 8
|
||||
|
||||
def test_preset_display_names_match(self):
|
||||
assert set(PRESET_DISPLAY_NAMES.keys()) == VALID_PRESETS
|
||||
for name in VALID_PRESETS:
|
||||
assert len(PRESET_DISPLAY_NAMES[name]) > 0
|
||||
|
||||
def test_preset_params_complete(self):
|
||||
assert set(PRESET_PARAMS.keys()) == VALID_PRESETS
|
||||
for preset, params in PRESET_PARAMS.items():
|
||||
assert set(params.keys()) == set(ALL_PARAM_KEYS)
|
||||
|
||||
def test_default_params_keys(self):
|
||||
assert set(DEFAULT_PARAMS.keys()) == set(ALL_PARAM_KEYS)
|
||||
|
||||
def test_param_ranges_keys(self):
|
||||
assert set(PARAM_RANGES.keys()) == set(ALL_PARAM_KEYS)
|
||||
|
||||
def test_default_within_ranges(self):
|
||||
for key in ALL_PARAM_KEYS:
|
||||
min_val, max_val = PARAM_RANGES[key]
|
||||
assert min_val <= DEFAULT_PARAMS[key] <= max_val
|
||||
|
||||
def test_all_presets_within_ranges(self):
|
||||
for preset, params in PRESET_PARAMS.items():
|
||||
for key in ALL_PARAM_KEYS:
|
||||
min_val, max_val = PARAM_RANGES[key]
|
||||
assert min_val <= params[key] <= max_val, f"{preset}.{key}={params[key]} out of range"
|
||||
|
||||
|
||||
# ── 默认值 ────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestColorGradeConfigDefaults:
|
||||
def test_default_disabled(self):
|
||||
cfg = ColorGradeConfig()
|
||||
assert cfg.enabled is False
|
||||
assert cfg.preset == ""
|
||||
assert cfg.brightness is None
|
||||
assert cfg.contrast is None
|
||||
assert cfg.saturation is None
|
||||
assert cfg.temperature is None
|
||||
assert cfg.hue is None
|
||||
|
||||
def test_default_has_no_effect(self):
|
||||
cfg = ColorGradeConfig()
|
||||
assert cfg.has_effect() is False
|
||||
|
||||
def test_default_resolve_params_equals_defaults(self):
|
||||
cfg = ColorGradeConfig()
|
||||
params = cfg.resolve_params()
|
||||
for key in ALL_PARAM_KEYS:
|
||||
assert params[key] == DEFAULT_PARAMS[key]
|
||||
|
||||
|
||||
# ── from_dict ────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestFromDict:
|
||||
def test_none_data_disabled(self):
|
||||
cfg = ColorGradeConfig.from_dict(None)
|
||||
assert cfg.enabled is False
|
||||
|
||||
def test_empty_dict_disabled(self):
|
||||
cfg = ColorGradeConfig.from_dict({})
|
||||
assert cfg.enabled is False
|
||||
|
||||
def test_enabled_false(self):
|
||||
cfg = ColorGradeConfig.from_dict({"enabled": False})
|
||||
assert cfg.enabled is False
|
||||
|
||||
def test_enabled_no_preset(self):
|
||||
cfg = ColorGradeConfig.from_dict({"enabled": True})
|
||||
assert cfg.enabled is True
|
||||
assert cfg.preset == ""
|
||||
assert cfg.brightness is None
|
||||
|
||||
def test_with_valid_preset(self):
|
||||
cfg = ColorGradeConfig.from_dict({"enabled": True, "preset": "fresh"})
|
||||
assert cfg.enabled is True
|
||||
assert cfg.preset == "fresh"
|
||||
|
||||
def test_with_invalid_preset_ignored(self):
|
||||
cfg = ColorGradeConfig.from_dict({"enabled": True, "preset": "unknown_preset"})
|
||||
assert cfg.enabled is True
|
||||
assert cfg.preset == "" # 无效预设被忽略
|
||||
|
||||
def test_with_custom_params(self):
|
||||
cfg = ColorGradeConfig.from_dict(
|
||||
{
|
||||
"enabled": True,
|
||||
"brightness": 10,
|
||||
"contrast": -5,
|
||||
"saturation": 150,
|
||||
"temperature": 20,
|
||||
"hue": 30,
|
||||
}
|
||||
)
|
||||
assert cfg.enabled is True
|
||||
assert cfg.brightness == 10.0
|
||||
assert cfg.contrast == -5.0
|
||||
assert cfg.saturation == 150.0
|
||||
assert cfg.temperature == 20.0
|
||||
assert cfg.hue == 30.0
|
||||
|
||||
def test_with_preset_and_custom_override(self):
|
||||
cfg = ColorGradeConfig.from_dict(
|
||||
{
|
||||
"enabled": True,
|
||||
"preset": "fresh",
|
||||
"brightness": 50, # 覆盖预设的 8
|
||||
}
|
||||
)
|
||||
assert cfg.preset == "fresh"
|
||||
assert cfg.brightness == 50.0
|
||||
|
||||
def test_invalid_param_values_none(self):
|
||||
cfg = ColorGradeConfig.from_dict(
|
||||
{
|
||||
"enabled": True,
|
||||
"brightness": "not_a_number",
|
||||
}
|
||||
)
|
||||
assert cfg.enabled is True
|
||||
assert cfg.brightness is None # 解析失败为 None
|
||||
|
||||
def test_numeric_string_params(self):
|
||||
cfg = ColorGradeConfig.from_dict(
|
||||
{
|
||||
"enabled": True,
|
||||
"brightness": "15.5",
|
||||
}
|
||||
)
|
||||
assert cfg.brightness == 15.5
|
||||
|
||||
def test_preset_bw(self):
|
||||
cfg = ColorGradeConfig.from_dict({"enabled": True, "preset": "black_white"})
|
||||
assert cfg.preset == "black_white"
|
||||
params = cfg.resolve_params()
|
||||
assert params["saturation"] == 0.0
|
||||
|
||||
|
||||
# ── resolve_params ───────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestResolveParams:
|
||||
def test_default_returns_defaults(self):
|
||||
cfg = ColorGradeConfig()
|
||||
params = cfg.resolve_params()
|
||||
assert params == DEFAULT_PARAMS
|
||||
|
||||
def test_preset_fresh_params(self):
|
||||
cfg = ColorGradeConfig(preset="fresh")
|
||||
params = cfg.resolve_params()
|
||||
assert params["brightness"] == 8
|
||||
assert params["contrast"] == 10
|
||||
assert params["saturation"] == 120
|
||||
|
||||
def test_preset_bw_saturation_zero(self):
|
||||
cfg = ColorGradeConfig(preset="black_white")
|
||||
params = cfg.resolve_params()
|
||||
assert params["saturation"] == 0
|
||||
assert params["contrast"] == 15
|
||||
|
||||
def test_custom_override_preset(self):
|
||||
cfg = ColorGradeConfig(preset="fresh", brightness=50)
|
||||
params = cfg.resolve_params()
|
||||
assert params["brightness"] == 50 # 覆盖了预设
|
||||
assert params["saturation"] == 120 # 预设值保留
|
||||
|
||||
def test_below_min_clamped(self):
|
||||
cfg = ColorGradeConfig(brightness=-200, saturation=-10)
|
||||
params = cfg.resolve_params()
|
||||
assert params["brightness"] == PARAM_RANGES["brightness"][0]
|
||||
assert params["saturation"] == PARAM_RANGES["saturation"][0]
|
||||
|
||||
def test_above_max_clamped(self):
|
||||
cfg = ColorGradeConfig(brightness=200, saturation=300, hue=360)
|
||||
params = cfg.resolve_params()
|
||||
assert params["brightness"] == PARAM_RANGES["brightness"][1]
|
||||
assert params["saturation"] == PARAM_RANGES["saturation"][1]
|
||||
assert params["hue"] == PARAM_RANGES["hue"][1]
|
||||
|
||||
def test_all_presets_resolve_within_ranges(self):
|
||||
for preset in VALID_PRESETS:
|
||||
cfg = ColorGradeConfig(enabled=True, preset=preset)
|
||||
params = cfg.resolve_params()
|
||||
for key in ALL_PARAM_KEYS:
|
||||
min_val, max_val = PARAM_RANGES[key]
|
||||
assert min_val <= params[key] <= max_val, f"{preset}.{key}={params[key]}"
|
||||
|
||||
def test_returns_new_dict_each_call(self):
|
||||
cfg = ColorGradeConfig(preset="warm")
|
||||
p1 = cfg.resolve_params()
|
||||
p2 = cfg.resolve_params()
|
||||
assert p1 is not p2
|
||||
p1["brightness"] = 999
|
||||
assert p2["brightness"] != 999
|
||||
|
||||
|
||||
# ── has_effect ───────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestHasEffect:
|
||||
def test_disabled_no_effect(self):
|
||||
cfg = ColorGradeConfig(enabled=False)
|
||||
assert cfg.has_effect() is False
|
||||
|
||||
def test_default_no_effect(self):
|
||||
cfg = ColorGradeConfig(enabled=True)
|
||||
assert cfg.has_effect() is False
|
||||
|
||||
def test_preset_has_effect(self):
|
||||
for preset in VALID_PRESETS:
|
||||
cfg = ColorGradeConfig(enabled=True, preset=preset)
|
||||
# 大部分预设都有效果,除了默认值完全一致的(应该没有)
|
||||
if preset in ("black_white",):
|
||||
assert cfg.has_effect() is True # BW 有 contrast=15
|
||||
else:
|
||||
assert cfg.has_effect() is True
|
||||
|
||||
def test_custom_brightness_has_effect(self):
|
||||
cfg = ColorGradeConfig(enabled=True, brightness=1.0)
|
||||
assert cfg.has_effect() is True
|
||||
|
||||
def test_tiny_change_no_effect(self):
|
||||
# 小于 0.001 的差异视为无效果
|
||||
cfg = ColorGradeConfig(enabled=True)
|
||||
# 直接通过默认值的话应该没有效果
|
||||
assert cfg.has_effect() is False
|
||||
|
||||
def test_custom_saturation_changed_from_default(self):
|
||||
# 默认 saturation=100,改成 101 就有效果
|
||||
cfg = ColorGradeConfig(enabled=True, saturation=101)
|
||||
assert cfg.has_effect() is True
|
||||
|
||||
|
||||
# ── validate ─────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestValidate:
|
||||
def test_disabled_always_valid(self):
|
||||
cfg = ColorGradeConfig(enabled=False)
|
||||
ok, err = cfg.validate()
|
||||
assert ok is True
|
||||
assert err == ""
|
||||
|
||||
def test_enabled_no_preset_valid(self):
|
||||
cfg = ColorGradeConfig(enabled=True)
|
||||
ok, err = cfg.validate()
|
||||
assert ok is True
|
||||
|
||||
def test_valid_preset(self):
|
||||
cfg = ColorGradeConfig(enabled=True, preset="cinema")
|
||||
ok, _ = cfg.validate()
|
||||
assert ok is True
|
||||
|
||||
def test_invalid_preset(self):
|
||||
cfg = ColorGradeConfig(enabled=True, preset="invalid")
|
||||
ok, err = cfg.validate()
|
||||
assert ok is False
|
||||
assert "预设" in err
|
||||
|
||||
def test_custom_param_out_of_range(self):
|
||||
cfg = ColorGradeConfig(enabled=True, brightness=500)
|
||||
ok, err = cfg.validate()
|
||||
assert ok is False
|
||||
assert "brightness" in err
|
||||
|
||||
def test_custom_param_within_range(self):
|
||||
cfg = ColorGradeConfig(enabled=True, brightness=50, contrast=-50)
|
||||
ok, _ = cfg.validate()
|
||||
assert ok is True
|
||||
|
||||
def test_saturation_negative_invalid(self):
|
||||
cfg = ColorGradeConfig(enabled=True, saturation=-1)
|
||||
ok, err = cfg.validate()
|
||||
assert ok is False
|
||||
assert "saturation" in err
|
||||
|
||||
|
||||
# ── 工具函数 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestUtils:
|
||||
def test_get_preset_names_returns_all(self):
|
||||
names = get_preset_names()
|
||||
assert len(names) == len(VALID_PRESETS)
|
||||
preset_keys = [n[0] for n in names]
|
||||
assert set(preset_keys) == VALID_PRESETS
|
||||
|
||||
def test_get_preset_names_sorted(self):
|
||||
names = get_preset_names()
|
||||
preset_keys = [n[0] for n in names]
|
||||
assert preset_keys == sorted(preset_keys)
|
||||
|
||||
def test_get_preset_params_valid(self):
|
||||
params = get_preset_params("fresh")
|
||||
assert params is not None
|
||||
assert params["brightness"] == 8
|
||||
|
||||
def test_get_preset_params_invalid(self):
|
||||
params = get_preset_params("unknown")
|
||||
assert params is None
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"param,value,expected",
|
||||
[
|
||||
("brightness", 50, 50),
|
||||
("brightness", 200, 100),
|
||||
("brightness", -200, -100),
|
||||
("saturation", 50, 50),
|
||||
("saturation", -10, 0),
|
||||
("saturation", 300, 200),
|
||||
("hue", 0, 0),
|
||||
("hue", 200, 180),
|
||||
("hue", -200, -180),
|
||||
],
|
||||
)
|
||||
def test_clamp_param(self, param, value, expected):
|
||||
assert clamp_param(param, value) == expected
|
||||
|
||||
def test_clamp_param_unknown_passthrough(self):
|
||||
assert clamp_param("unknown_param", 999) == 999
|
||||
Reference in New Issue
Block a user