From 8772413ae8ec6525e8f3f5c5477e8cf6306729f2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 21 Feb 2026 16:17:16 +0000 Subject: [PATCH 1/4] Initial plan From 3f31c7d8b430fadfd4912d40fa0c2832f230de98 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 21 Feb 2026 16:22:13 +0000 Subject: [PATCH 2/4] Add invoke-readwatermark CLI command to decode invisible watermarks Co-authored-by: lstein <111189+lstein@users.noreply.github.com> --- invokeai/app/read_watermark.py | 34 +++++++++++++++++++ .../backend/image_util/invisible_watermark.py | 22 +++++++++++- pyproject.toml | 1 + 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 invokeai/app/read_watermark.py diff --git a/invokeai/app/read_watermark.py b/invokeai/app/read_watermark.py new file mode 100644 index 00000000000..9bc712d7780 --- /dev/null +++ b/invokeai/app/read_watermark.py @@ -0,0 +1,34 @@ +"""CLI command to decode invisible watermarks from Invoke-generated images.""" + +import argparse +import sys + +from PIL import Image + +from invokeai.backend.image_util.invisible_watermark import InvisibleWatermark + + +def read_watermark() -> None: + """Read and print invisible watermarks from a list of image files.""" + parser = argparse.ArgumentParser( + prog="invoke-readwatermark", + description="Decode invisible watermarks from Invoke-generated images.", + ) + parser.add_argument("images", nargs="+", metavar="IMAGE", help="Image file(s) to read watermarks from.") + parser.add_argument( + "--length", + type=int, + default=32, + metavar="BYTES", + help="Expected watermark length in bytes (default: %(default)s).", + ) + args = parser.parse_args() + + for path in args.images: + try: + image = Image.open(path) + except OSError as e: + print(f"{path}: error opening image: {e}", file=sys.stderr) + continue + watermark = InvisibleWatermark.decode_watermark(image, watermark_length=args.length) + print(f"{path}: {watermark}") diff --git a/invokeai/backend/image_util/invisible_watermark.py b/invokeai/backend/image_util/invisible_watermark.py index 5b0b2dbb5b1..8cef538349e 100644 --- a/invokeai/backend/image_util/invisible_watermark.py +++ b/invokeai/backend/image_util/invisible_watermark.py @@ -9,7 +9,7 @@ from PIL import Image import invokeai.backend.util.logging as logger -from invokeai.backend.image_util.imwatermark.vendor import WatermarkEncoder +from invokeai.backend.image_util.imwatermark.vendor import WatermarkDecoder, WatermarkEncoder class InvisibleWatermark: @@ -25,3 +25,23 @@ def add_watermark(cls, image: Image.Image, watermark_text: str) -> Image.Image: encoder.set_watermark("bytes", watermark_text.encode("utf-8")) bgr_encoded = encoder.encode(bgr, "dwtDct") return Image.fromarray(cv2.cvtColor(bgr_encoded, cv2.COLOR_BGR2RGB)).convert("RGBA") + + @classmethod + def decode_watermark(cls, image: Image.Image, watermark_length: int = 32) -> str: + """Decode an invisible watermark from an image. + + Args: + image: The PIL image to decode the watermark from. + watermark_length: The length of the watermark in bytes (default 32). + + Returns: + The decoded watermark text, or an empty string if decoding fails. + """ + bgr = cv2.cvtColor(np.array(image.convert("RGB")), cv2.COLOR_RGB2BGR) + decoder = WatermarkDecoder("bytes", watermark_length * 8) + try: + raw = decoder.decode(bgr, "dwtDct") + return raw.rstrip(b"\x00").decode("utf-8", errors="replace") + except (RuntimeError, ValueError, NameError) as e: + logger.debug("Failed to decode watermark: %s", e) + return "" diff --git a/pyproject.toml b/pyproject.toml index adfe5982baf..e6a2752075f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -159,6 +159,7 @@ explicit = true [project.scripts] "invokeai-web" = "invokeai.app.run_app:run_app" +"invoke-readwatermark" = "invokeai.app.read_watermark:read_watermark" [project.urls] "Homepage" = "https://invoke-ai.github.io/InvokeAI/" From 6a6699f26ff5b8d64fa1660c94122c9c9d10dc09 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Sat, 21 Feb 2026 11:27:11 -0500 Subject: [PATCH 3/4] fix(app): change location of the read_watermark module --- invokeai/app/{ => util}/read_watermark.py | 0 pyproject.toml | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename invokeai/app/{ => util}/read_watermark.py (100%) diff --git a/invokeai/app/read_watermark.py b/invokeai/app/util/read_watermark.py similarity index 100% rename from invokeai/app/read_watermark.py rename to invokeai/app/util/read_watermark.py diff --git a/pyproject.toml b/pyproject.toml index e6a2752075f..a7cf0d3a420 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -159,7 +159,7 @@ explicit = true [project.scripts] "invokeai-web" = "invokeai.app.run_app:run_app" -"invoke-readwatermark" = "invokeai.app.read_watermark:read_watermark" +"invoke-readwatermark" = "invokeai.app.util.read_watermark:read_watermark" [project.urls] "Homepage" = "https://invoke-ai.github.io/InvokeAI/" From 1c9ce54ff94e65ee08eafc8a96d40654dd86348a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 21 Feb 2026 16:47:03 +0000 Subject: [PATCH 4/4] Fix default watermark_length from 32 to 8 bytes to match default InvokeAI watermark text Co-authored-by: lstein <111189+lstein@users.noreply.github.com> --- invokeai/app/util/read_watermark.py | 4 ++-- invokeai/backend/image_util/invisible_watermark.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/invokeai/app/util/read_watermark.py b/invokeai/app/util/read_watermark.py index 9bc712d7780..5fe492d2b04 100644 --- a/invokeai/app/util/read_watermark.py +++ b/invokeai/app/util/read_watermark.py @@ -18,9 +18,9 @@ def read_watermark() -> None: parser.add_argument( "--length", type=int, - default=32, + default=8, metavar="BYTES", - help="Expected watermark length in bytes (default: %(default)s).", + help="Expected watermark length in bytes (default: %(default)s, matching the default 'InvokeAI' watermark text).", ) args = parser.parse_args() diff --git a/invokeai/backend/image_util/invisible_watermark.py b/invokeai/backend/image_util/invisible_watermark.py index 8cef538349e..6a87f4da47e 100644 --- a/invokeai/backend/image_util/invisible_watermark.py +++ b/invokeai/backend/image_util/invisible_watermark.py @@ -27,12 +27,12 @@ def add_watermark(cls, image: Image.Image, watermark_text: str) -> Image.Image: return Image.fromarray(cv2.cvtColor(bgr_encoded, cv2.COLOR_BGR2RGB)).convert("RGBA") @classmethod - def decode_watermark(cls, image: Image.Image, watermark_length: int = 32) -> str: + def decode_watermark(cls, image: Image.Image, watermark_length: int = 8) -> str: """Decode an invisible watermark from an image. Args: image: The PIL image to decode the watermark from. - watermark_length: The length of the watermark in bytes (default 32). + watermark_length: The length of the watermark in bytes (default 8, matching the default "InvokeAI" watermark text). Returns: The decoded watermark text, or an empty string if decoding fails.