mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-06-08 11:53:55 +02:00
Extract _target_size helper + regression-test native resolution (v0.5.4)
The native-vs-downscale decision in InvisibleEngine.remove_watermark (the issue #10/#15 fix: max_resolution=0 must not pre-downscale, since any downscale both loses quality and lets SynthID survive) had no test. Extract it into a pure helper invisible_engine._target_size(w, h, max_resolution) and cover it with tests/test_invisible_engine.py::TestTargetSize so a re-introduced forced downscale fails CI instead of silently regressing #15. Also: - Clamp the short side to >=1 in _target_size: extreme aspect ratios (e.g. 5000x3 with --max-resolution 1024) truncated it to 0 and crashed image.resize(). Pre-existing in the inline math; fixed now that it is a named, tested function. - Consolidate the two duplicated temp-file save blocks into one unconditional save (behavior unchanged: the EXIF-transposed image is still always persisted before WatermarkRemover reloads it by path), and drop the now-redundant `_tmp_path is not None` guard in finally. - Bump version 0.5.3 -> 0.5.4 (pyproject, __init__, uv.lock); document the helper as the regression guard in CLAUDE.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from remove_ai_watermarks.invisible_engine import InvisibleEngine, is_available
|
||||
from remove_ai_watermarks.invisible_engine import InvisibleEngine, _target_size, is_available
|
||||
|
||||
|
||||
class TestIsAvailable:
|
||||
@@ -26,3 +26,45 @@ class TestInvisibleEngineInit:
|
||||
|
||||
def test_ctrlregen_model_id(self):
|
||||
assert InvisibleEngine.CTRLREGEN_MODEL_ID == "yepengliu/ctrlregen"
|
||||
|
||||
|
||||
class TestTargetSize:
|
||||
"""Regression guard for the native-resolution decision (issues #10 / #15).
|
||||
|
||||
max_resolution=0 must NOT downscale -- the forced downscale->upscale
|
||||
round-trip was the quality loss in #10, and downscaling at all let SynthID
|
||||
survive in #15 (the native SDXL pass at strength ~0.05 is what defeats it).
|
||||
"""
|
||||
|
||||
def test_native_default_no_downscale(self):
|
||||
# The default (0) means native resolution: no resize, regardless of size.
|
||||
assert _target_size(4096, 4096, 0) is None
|
||||
assert _target_size(123, 456, 0) is None
|
||||
|
||||
def test_negative_cap_treated_as_native(self):
|
||||
assert _target_size(4096, 4096, -1) is None
|
||||
|
||||
def test_cap_below_long_side_downscales(self):
|
||||
# 2000x1000, cap 1024 -> long side scaled to 1024, aspect preserved.
|
||||
assert _target_size(2000, 1000, 1024) == (1024, 512)
|
||||
|
||||
def test_cap_uses_long_side_for_portrait(self):
|
||||
# Portrait: height is the long side, so it drives the ratio.
|
||||
assert _target_size(1000, 2000, 1024) == (512, 1024)
|
||||
|
||||
def test_cap_at_or_above_long_side_no_downscale(self):
|
||||
# Already within the cap (and exactly equal) -> no resize.
|
||||
assert _target_size(800, 600, 1024) is None
|
||||
assert _target_size(1024, 768, 1024) is None
|
||||
|
||||
def test_integer_truncation_matches_pil_call_site(self):
|
||||
# 1254x1254 (the gpt-image sample) capped at 1000: int(1254*1000/1254)=1000.
|
||||
assert _target_size(1254, 1254, 1000) == (1000, 1000)
|
||||
# Non-divisible ratio truncates toward zero like int() at the call site.
|
||||
assert _target_size(1000, 333, 500) == (500, 166)
|
||||
|
||||
def test_extreme_aspect_ratio_clamps_short_side_to_one(self):
|
||||
# 5000x3 capped at 1024: int(3 * 1024/5000) = 0 would crash resize();
|
||||
# the short side must clamp to 1, never 0.
|
||||
assert _target_size(5000, 3, 1024) == (1024, 1)
|
||||
assert _target_size(3, 5000, 1024) == (1, 1024)
|
||||
|
||||
Reference in New Issue
Block a user