mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-10-18 15:31:06 +02:00
Let the TIFF geometry override a caller's raw-decode claim
Both readers in the imread stack apply the TIFF orientation tag (cv2's libtiff path under IMREAD_UNCHANGED too, and Pillow's TIFF plugin, identically), so the visible path's blanket orientation_applied=False carried the tag onto an already-turned raster. _read_display_tags now ignores the explicit claim for TIFF and decides on the IFD's stored size, which also keeps a hypothetical non-turning build correct: a stored-shape raster still carries the tag. Guarded at the api seam, not just imwrite. pre-commit: 1) maintain.sh - exit 2 at the uv-secure gate, the three triaged no-fix advisories in docs/development.md (rechecked 2026-09-08); ruff check+format clean, pyright src/ 0 errors, 2363 passed 3 skipped run separately; 2) /simplify - single pass, no fan-out available, nothing to fix (api-state plumbing rejected as disproportionate); 3) docs sync - identifiers live only in docs/module-internals.md, updated, no other refs; 4) CLAUDE.md - audited against the structure rule, no additions, revise-claude-md: no additions
This commit is contained in:
3 files changed
+36
-1
No files matched your search
@@ -2148,7 +2148,14 @@ Contracts:
|
||||
pixels on save. Callers that know the decode state pass
|
||||
`orientation_applied=False` for raw pixels and `True` for a transposed
|
||||
raster. Dimensions alone cannot distinguish mirrors or 180-degree rotation.
|
||||
The omitted value retains the compatibility shape heuristic. Display tags
|
||||
TIFF is the container exception: every reader in the stack (cv2's libtiff path
|
||||
under `IMREAD_UNCHANGED` too, and Pillow's TIFF plugin, identically) applies
|
||||
the orientation tag on decode, so `_read_display_tags` ignores the explicit
|
||||
claim for TIFF and decides on the IFD's stored `ImageWidth`/`ImageLength`
|
||||
against the raster, via `_stored_size` -- `Image.size` already reports the
|
||||
upright geometry there, which is what let a turned raster be tagged into a
|
||||
second rotation (issue #106). The omitted value retains the compatibility shape
|
||||
heuristic. Display tags
|
||||
are captured before encoding, including when source and destination match.
|
||||
|
||||
The metadata strip is the other half of that contract: Pillow cannot hold 16-bit
|
||||
|
||||
@@ -219,6 +219,14 @@ def _read_display_tags(
|
||||
in every case: it describes colour, not geometry. Failures read as "no tags":
|
||||
the restore is an enhancement, never a reason to fail a write.
|
||||
|
||||
TIFF is the exception on the explicit state: every reader in this module's
|
||||
stack applies the TIFF orientation tag on decode (cv2's libtiff path under
|
||||
``IMREAD_UNCHANGED`` too, and Pillow's TIFF plugin, identically), so a caller's
|
||||
blanket ``False`` -- which encodes cv2's JPEG/PNG/WebP contract -- is not true
|
||||
for TIFF. For TIFF sources the parameter is ignored and the geometry check
|
||||
decides against the IFD's stored size: a raster that no longer has the stored
|
||||
dimensions was turned by the decode and must not be tagged again (issue #106).
|
||||
|
||||
The stored size cannot be read straight off ``Image``: Pillow's TIFF reader
|
||||
reports the upright one, so it comes through :func:`_stored_size`.
|
||||
"""
|
||||
@@ -231,11 +239,14 @@ def _read_display_tags(
|
||||
with Image.open(source) as im:
|
||||
icc = im.info.get("icc_profile")
|
||||
orient = im.getexif().get(_ORIENT_EXIF_TAG)
|
||||
fmt = im.format
|
||||
stored_w, stored_h = _stored_size(im)
|
||||
except Exception:
|
||||
return None, None
|
||||
if not (isinstance(icc, bytes) and icc):
|
||||
icc = None
|
||||
if fmt == "TIFF":
|
||||
orientation_applied = None
|
||||
if orientation_applied is True:
|
||||
orient = None
|
||||
elif orientation_applied is False:
|
||||
|
||||
@@ -445,3 +445,20 @@ class TestTiffOrientation:
|
||||
out = tmp_path / "out.png"
|
||||
assert image_io.imwrite(out, raster, display_tags_from=src) is True
|
||||
assert self._out_orientation(out) is None
|
||||
|
||||
def test_the_visible_api_path_does_not_tag_a_turned_tiff(self, tmp_path: Path) -> None:
|
||||
# The visible write passes orientation_applied=False ("raw decode"), which is
|
||||
# a lie for TIFF: both readers turn the raster. _read_display_tags must ignore
|
||||
# that claim for TIFF and let the IFD geometry decide, or a CLI/library user
|
||||
# gets a turned raster TAGGED for the same turn again (issue #106).
|
||||
from PIL import Image
|
||||
|
||||
from remove_ai_watermarks import api
|
||||
|
||||
src = self._tagged(tmp_path / "src.tif", self.ORIENT)
|
||||
out = tmp_path / "out.png"
|
||||
_, removed = api.remove_visible(src, out, strip_metadata=False)
|
||||
assert removed == [] # plain red: no mark, the passthrough write is exercised
|
||||
with Image.open(out) as im:
|
||||
assert im.size == (64, 96) # the upright raster, not the stored landscape
|
||||
assert im.getexif().get(0x0112) is None # and no tag asking for another turn
|
||||
Reference in new issue
Block a user