mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-06-05 02:28:00 +02:00
5d0e6c3a65
Apply fixes from a full-repo review (code, tests, docs). Security / correctness: - Clamp attacker-controlled PNG/caBX chunk lengths to the remaining file size in metadata.py and noai/c2pa.py (a malformed length no longer drives a multi-GB read); skipped chunks seek instead of read. - noai/isobmff.strip_c2pa_boxes is now fail-safe on a malformed box: return the original bytes with a warning instead of silently truncating the tail, so metadata --remove can no longer emit a corrupt file. - doubao_engine._fixed_alpha_map clamps the glyph box to the image (no crash on degenerate width-vs-height). - watermark_remover._run_region_hires gates the phaseCorrelate offset on response and magnitude (a spurious shift no longer garbles text) and drops the generator after a CPU fallback (no MPS/CPU device mismatch). Robustness: - gemini_engine, doubao_engine, region_eraser normalize grayscale and RGBA inputs to BGR at the engine entry points. - image_io.imwrite returns False on an unwritable path (matches cv2). - invisible_engine guards a None imread result before use. - trustmark_detector._decoder uses a double-checked threading lock. - ctrlregen.tiling.tile_positions raises on overlap >= tile. - humanizer chromatic shift no longer wraps opposite-edge pixels. - identify OpenAI caveat keyed on the normalized vendor, not a substring. - Remove the dead "visible --detect-threshold" CLI option. - publish.yml verifies the release tag matches the package version. Docs: - README strength 0.05 to 0.10; .env.example HF_TOKEN marked optional; doubao_capture README updated to reverse-alpha-only; CLAUDE.md synced with the new behaviors and the batch command. Tests: new test_security_clamp.py for the read clamp and isobmff fail-safe; erase CLI coverage; integrity-clash rule 2 end-to-end; multi-tag EXIF survival and cross-format strip guards; channel/size, tiling, humanizer, and imwrite regressions. Full suite 493 passed, 2 skipped; ruff and pyright src/ clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
73 lines
2.5 KiB
Python
73 lines
2.5 KiB
Python
import numpy as np
|
|
|
|
from remove_ai_watermarks.humanizer import apply_analog_humanizer
|
|
|
|
|
|
def test_humanizer_does_not_modify_original_if_disabled():
|
|
img = np.zeros((100, 100, 3), dtype=np.uint8)
|
|
img[50, 50] = [100, 150, 200]
|
|
org_img = img.copy()
|
|
|
|
# grain=0, shift=0 means disabled — result should match original.
|
|
result = apply_analog_humanizer(img, grain_intensity=0.0, chromatic_shift=0)
|
|
assert np.array_equal(result, org_img)
|
|
|
|
|
|
def test_chromatic_shift():
|
|
# Only green channel is centered, red/blue should shift.
|
|
img = np.zeros((5, 5, 3), dtype=np.uint8)
|
|
img[2, 2] = [255, 255, 255] # B, G, R
|
|
|
|
# shift=1
|
|
result = apply_analog_humanizer(img, grain_intensity=0.0, chromatic_shift=1)
|
|
|
|
# G (index 1) stays at [2,2]
|
|
assert result[2, 2, 1] == 255
|
|
# B (index 0) shifted right (+1 axis 1) -> [2, 3]
|
|
assert result[2, 3, 0] == 255
|
|
# R (index 2) shifted left (-1 axis 1) -> [2, 1]
|
|
assert result[2, 1, 2] == 255
|
|
|
|
|
|
def test_grain_intensity():
|
|
# Gray image
|
|
img = np.full((100, 100, 3), 128, dtype=np.uint8)
|
|
|
|
# Add strong noise
|
|
result = apply_analog_humanizer(img, grain_intensity=10.0, chromatic_shift=0)
|
|
|
|
# Image should no longer be purely 128
|
|
unique_vals = np.unique(result)
|
|
assert len(unique_vals) > 5
|
|
|
|
# Mean should roughly be 128
|
|
assert 126 < np.mean(result) < 130
|
|
|
|
|
|
def test_invalid_shape():
|
|
# Missing color channel
|
|
img = np.zeros((100, 100), dtype=np.uint8)
|
|
img[0, 0] = 50
|
|
result = apply_analog_humanizer(img)
|
|
assert np.array_equal(img, result)
|
|
|
|
|
|
def test_chromatic_shift_does_not_wrap_opposite_edge():
|
|
# On a horizontal gradient (dark left, bright right), a circular np.roll
|
|
# would wrap the bright right edge into the R channel's left border and the
|
|
# dark left edge into the B channel's right border, producing a colored
|
|
# fringe. After the fix the border columns must replicate their own edge.
|
|
ramp = np.linspace(0, 255, 64, dtype=np.uint8)
|
|
gray = np.broadcast_to(ramp, (32, 64))
|
|
img = np.stack([gray, gray, gray], axis=2).copy() # B, G, R
|
|
|
|
shift = 3
|
|
result = apply_analog_humanizer(img, grain_intensity=0.0, chromatic_shift=shift)
|
|
|
|
# B (index 0) rolled right -> its left border must stay dark (near 0),
|
|
# NOT wrap the bright right edge.
|
|
assert result[:, :shift, 0].max() < 60
|
|
# R (index 2) rolled left -> its right border must stay bright (near 255),
|
|
# NOT wrap the dark left edge.
|
|
assert result[:, -shift:, 2].min() > 195
|