Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion invokeai/app/services/boards/boards_default.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def create(
board_name: str,
) -> BoardDTO:
board_record = self.__invoker.services.board_records.save(board_name)
return board_record_to_dto(board_record, None, 0, 0, 0)
return board_record_to_dto(board_record, None, 0, 0)

def get_dto(self, board_id: str) -> BoardDTO:
board_record = self.__invoker.services.board_records.get(board_id)
Expand Down
34 changes: 34 additions & 0 deletions invokeai/app/util/read_watermark.py
Original file line number Diff line number Diff line change
@@ -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=8,
metavar="BYTES",
help="Expected watermark length in bytes (default: %(default)s, matching the default 'InvokeAI' watermark text).",
)
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}")
22 changes: 21 additions & 1 deletion invokeai/backend/image_util/invisible_watermark.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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 = 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 8, matching the default "InvokeAI" watermark text).

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 ""
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,8 @@ import { useCallback } from 'react';
*/
export const useIsUncommittedCanvasTextSessionActive = () => {
const canvasManager = useCanvasManagerSafe();
return useCallback(() => canvasManager?.tool.tools.text.$session.get() !== null, [canvasManager]);
return useCallback(
() => canvasManager !== null && canvasManager.tool.tools.text.$session.get() !== null,
[canvasManager]
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -134,19 +134,14 @@ export class CanvasGradientToolModule extends CanvasModuleBase {
};

if (settings.gradientType === 'linear') {
const bboxCenter = {
x: bboxInLayer.x + bboxInLayer.width / 2,
y: bboxInLayer.y + bboxInLayer.height / 2,
};
const cos = Math.cos(angle);
const sin = Math.sin(angle);
const halfWidth = (Math.abs(bboxInLayer.width * cos) + Math.abs(bboxInLayer.height * sin)) / 2;
const halfHeight = (Math.abs(bboxInLayer.width * sin) + Math.abs(bboxInLayer.height * cos)) / 2;
// Always render linear gradients on a rect that fully covers the bbox.
// Angle-dependent rect sizing can undershoot on non-square bboxes (e.g. 90deg on tall bboxes),
// leaving uncovered bands when the result is clipped back to bbox.
rect = {
x: bboxCenter.x - halfWidth,
y: bboxCenter.y - halfHeight,
width: Math.max(halfWidth * 2, 1),
height: Math.max(halfHeight * 2, 1),
x: bboxInLayer.x,
y: bboxInLayer.y,
width: Math.max(bboxInLayer.width, 1),
height: Math.max(bboxInLayer.height, 1),
};
}

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ explicit = true

[project.scripts]
"invokeai-web" = "invokeai.app.run_app:run_app"
"invoke-readwatermark" = "invokeai.app.util.read_watermark:read_watermark"

[project.urls]
"Homepage" = "https://invoke-ai.github.io/InvokeAI/"
Expand Down