From c7d5bb5b42fb0c5015cf51c83002350fd592b6fa Mon Sep 17 00:00:00 2001 From: Kebolder Date: Tue, 9 Dec 2025 11:21:52 -0600 Subject: [PATCH 01/11] Reverted the repo to be in sync with main * Added resize command for custom graph node useage --- ai_diffusion/client.py | 7 ++++++- ai_diffusion/comfy_client.py | 14 +++++++++++++- ai_diffusion/document.py | 7 +++++++ ai_diffusion/model.py | 15 +++++++++++++-- 4 files changed, 39 insertions(+), 4 deletions(-) diff --git a/ai_diffusion/client.py b/ai_diffusion/client.py index 5274fe40a..5f8ed93b6 100644 --- a/ai_diffusion/client.py +++ b/ai_diffusion/client.py @@ -38,12 +38,17 @@ class TextOutput(NamedTuple): mime: str +class ResizeCommand(NamedTuple): + width: int + height: int + + class SharedWorkflow(NamedTuple): publisher: str workflow: dict -ClientOutput = dict | SharedWorkflow | TextOutput +ClientOutput = dict | SharedWorkflow | TextOutput | ResizeCommand class ClientMessage(NamedTuple): diff --git a/ai_diffusion/comfy_client.py b/ai_diffusion/comfy_client.py index 846a2e022..248c1b3f8 100644 --- a/ai_diffusion/comfy_client.py +++ b/ai_diffusion/comfy_client.py @@ -12,7 +12,7 @@ from .api import WorkflowInput from .client import Client, CheckpointInfo, ClientMessage, ClientEvent, DeviceInfo, ClientModels -from .client import SharedWorkflow, TranslationPackage, ClientFeatures, TextOutput +from .client import SharedWorkflow, TranslationPackage, ClientFeatures, TextOutput, ResizeCommand from .client import Quantization, MissingResources, filter_supported_styles, loras_to_upload from .comfy_workflow import ComfyObjectInfo from .files import FileFormat @@ -918,6 +918,18 @@ def _extract_text_output(job_id: str, msg: dict): text = payload.get("text") name = payload.get("name") mime = payload.get("content-type", mime) + # Special case: Krita canvas resize command produced by a tooling node + if mime == "application/x-krita-command" and isinstance(text, str): + try: + data = json.loads(text) + if data.get("action") == "resize_canvas": + width = int(data.get("width", 0)) + height = int(data.get("height", 0)) + if width > 0 and height > 0: + cmd = ResizeCommand(width, height) + return ClientMessage(ClientEvent.output, job_id, result=cmd) + except Exception as e: + log.warning(f"Failed to process Krita command output: {e}") elif isinstance(payload, str): text = payload name = f"Node {key}" diff --git a/ai_diffusion/document.py b/ai_diffusion/document.py index 60647eeaf..173c81f25 100644 --- a/ai_diffusion/document.py +++ b/ai_diffusion/document.py @@ -50,6 +50,10 @@ def get_image( def resize(self, extent: Extent): raise NotImplementedError + def resize_canvas(self, width: int, height: int): + """Resize the underlying canvas if supported by the implementation.""" + pass + def annotate(self, key: str, value: QByteArray): pass @@ -230,6 +234,9 @@ def resize(self, extent: Extent): res = self._doc.resolution() self._doc.scaleImage(extent.width, extent.height, res, res, "Bilinear") + def resize_canvas(self, width: int, height: int): + self._doc.resizeImage(0, 0, width, height) + def annotate(self, key: str, value: QByteArray): self._doc.setAnnotation(f"ai_diffusion/{key}", f"AI Diffusion Plugin: {key}", value) diff --git a/ai_diffusion/model.py b/ai_diffusion/model.py index 1e3ade128..cf1a5931e 100644 --- a/ai_diffusion/model.py +++ b/ai_diffusion/model.py @@ -23,7 +23,7 @@ from .settings import settings from .network import NetworkError from .image import Extent, Image, Mask, Bounds, DummyImage -from .client import Client, ClientMessage, ClientEvent, ClientOutput, is_style_supported +from .client import Client, ClientMessage, ClientEvent, ClientOutput, is_style_supported, ResizeCommand from .client import filter_supported_styles, resolve_arch from .custom_workflow import CustomWorkspace, WorkflowCollection, CustomGenerationMode from .document import Document, KritaDocument @@ -582,7 +582,10 @@ def handle_message(self, message: ClientMessage): self.progress_kind = ProgressKind.upload self.progress = message.progress elif message.event is ClientEvent.output: - self.custom.show_output(message.result) + if isinstance(message.result, ResizeCommand): + self._apply_resize_command(message.result, job) + else: + self.custom.show_output(message.result) elif message.event is ClientEvent.finished: if message.error: # successful jobs may have encountered some warnings self.report_error(Error.from_string(message.error, ErrorKind.warning)) @@ -622,6 +625,14 @@ def _finish_job(self, job: Job, event: ClientEvent): self.jobs.notify_cancelled(job) self.progress = 0 + def _apply_resize_command(self, cmd: ResizeCommand, job: Job): + try: + self.document.resize_canvas(cmd.width, cmd.height) + bounds = job.params.bounds + job.params.bounds = Bounds(bounds.x, bounds.y, cmd.width, cmd.height) + except Exception as e: + log.warning(f"Failed to resize canvas from custom workflow: {e}") + def update_preview(self): if selection := self.jobs.selection: self.show_preview(selection[0].job, selection[0].image) From 43ae6698fa68b03dba9cc37d4dd5f9b95e1eb957 Mon Sep 17 00:00:00 2001 From: Kebolder Date: Tue, 9 Dec 2025 11:34:54 -0600 Subject: [PATCH 02/11] Ruff format fix (forgot) --- ai_diffusion/model.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ai_diffusion/model.py b/ai_diffusion/model.py index cf1a5931e..af239e0db 100644 --- a/ai_diffusion/model.py +++ b/ai_diffusion/model.py @@ -23,7 +23,14 @@ from .settings import settings from .network import NetworkError from .image import Extent, Image, Mask, Bounds, DummyImage -from .client import Client, ClientMessage, ClientEvent, ClientOutput, is_style_supported, ResizeCommand +from .client import ( + Client, + ClientMessage, + ClientEvent, + ClientOutput, + is_style_supported, + ResizeCommand, +) from .client import filter_supported_styles, resolve_arch from .custom_workflow import CustomWorkspace, WorkflowCollection, CustomGenerationMode from .document import Document, KritaDocument From e49218a89feb4ae2921ecc58fc05fe8538b0144c Mon Sep 17 00:00:00 2001 From: Kebolder Date: Wed, 10 Dec 2025 18:33:17 -0600 Subject: [PATCH 03/11] Updated the model.py to support using the "Krita output" node instead --- ai_diffusion/model.py | 84 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) diff --git a/ai_diffusion/model.py b/ai_diffusion/model.py index af239e0db..092ccc2fd 100644 --- a/ai_diffusion/model.py +++ b/ai_diffusion/model.py @@ -139,6 +139,7 @@ def __init__(self, document: Document, connection: Connection, workflows: Workfl self._doc = document self._connection = connection self._layer: Layer | None = None + self._preview_canvas_original_extent: Extent | None = None self.generate_seed() self.jobs = JobQueue() self.regions = RootRegion(self) @@ -633,10 +634,85 @@ def _finish_job(self, job: Job, event: ClientEvent): self.progress = 0 def _apply_resize_command(self, cmd: ResizeCommand, job: Job): + """Record a requested canvas resize for this job. + + The actual resize is applied when showing a preview or applying the result. + """ try: - self.document.resize_canvas(cmd.width, cmd.height) bounds = job.params.bounds job.params.bounds = Bounds(bounds.x, bounds.y, cmd.width, cmd.height) + job.params.metadata["resize_canvas"] = {"width": cmd.width, "height": cmd.height} + except Exception as e: + log.warning(f"Failed to store resize command from custom workflow: {e}") + + def _get_resize_for_params(self, params: JobParams) -> tuple[int, int] | None: + resize = params.metadata.get("resize_canvas") + if not isinstance(resize, dict): + return None + + try: + width = int(resize.get("width", 0)) + height = int(resize.get("height", 0)) + except Exception: + return None + + if width <= 0 or height <= 0: + return None + + return width, height + + def _preview_canvas_size(self, params: JobParams): + """Resize canvas for preview, remembering original size so it can be reverted. + + This only affects temporary previews. The canvas is reverted when + there are no history selections (see hide_preview). + """ + resize = self._get_resize_for_params(params) + if resize is None: + return + + width, height = resize + extent = self.document.extent + if extent.width == width and extent.height == height: + return + if self._preview_canvas_original_extent is None: + self._preview_canvas_original_extent = extent + + try: + self.document.resize_canvas(width, height) + except Exception as e: + log.warning(f"Failed to resize canvas for preview from custom workflow: {e}") + + def _reset_preview_canvas_size(self): + """Revert canvas size after preview, if it was resized just for preview.""" + if self._preview_canvas_original_extent is None: + return + + original = self._preview_canvas_original_extent + self._preview_canvas_original_extent = None + + try: + self.document.resize_canvas(original.width, original.height) + except Exception as e: + log.warning(f"Failed to revert canvas size after preview: {e}") + + def _apply_canvas_resize_for_params(self, params: JobParams): + """Resize the canvas permanently if a resize was requested in metadata. + + Once applied, the resize hint is cleared so future previews don't + treat it as a pending temporary resize. + """ + resize = self._get_resize_for_params(params) + if resize is None: + return + + width, height = resize + + try: + extent = self.document.extent + if extent.width != width or extent.height != height: + self.document.resize_canvas(width, height) + params.metadata.pop("resize_canvas", None) except Exception as e: log.warning(f"Failed to resize canvas from custom workflow: {e}") @@ -652,6 +728,8 @@ def show_preview(self, job_id: str, index: int, name_prefix="Preview"): if job.kind is JobKind.animation: return # don't show animation preview on canvas (it's slow and clumsy) + self._preview_canvas_size(job.params) + name = f"[{name_prefix}] {trim_text(job.params.name, 77)}" image = job.results[index] bounds = job.params.bounds @@ -674,6 +752,7 @@ def hide_preview(self, delete_layer=False): self._layer = None else: self._layer.hide() + self._reset_preview_canvas_size() def apply_result( self, @@ -683,6 +762,9 @@ def apply_result( region_behavior=ApplyRegionBehavior.layer_group, prefix="", ): + self._apply_canvas_resize_for_params(params) + self._preview_canvas_original_extent = None + bounds = Bounds(*params.bounds.offset, *image.extent) if len(params.regions) == 0 or region_behavior is ApplyRegionBehavior.none: if behavior is ApplyBehavior.replace: From 5264dd1bed6cb5f5a80e5314eeef425d58a5daf4 Mon Sep 17 00:00:00 2001 From: Kebolder Date: Thu, 11 Dec 2025 12:49:50 -0600 Subject: [PATCH 04/11] Majorly simplified the resizing into a command. --- .gitignore | 3 +- ai_diffusion/jobs.py | 18 ++++++++- ai_diffusion/model.py | 89 ++++++------------------------------------- 3 files changed, 30 insertions(+), 80 deletions(-) diff --git a/.gitignore b/.gitignore index 96b8a5d0e..e1eb48aaf 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,5 @@ venv .DS_Store bin lib -pyvenv.cfg \ No newline at end of file +pyvenv.cfg +release_notes.md \ No newline at end of file diff --git a/ai_diffusion/jobs.py b/ai_diffusion/jobs.py index 2f091c66c..495e5ce89 100644 --- a/ai_diffusion/jobs.py +++ b/ai_diffusion/jobs.py @@ -6,7 +6,7 @@ from typing import Any, NamedTuple, TYPE_CHECKING from PyQt5.QtCore import QObject, pyqtSignal -from .image import Bounds, ImageCollection +from .image import Bounds, Extent, ImageCollection from .settings import settings from .style import Style from .util import ensure @@ -55,6 +55,7 @@ class JobParams: has_mask: bool = False frame: tuple[int, int, int] = (0, 0, 0) animation_id: str = "" + resize_canvas: Extent | None = None @staticmethod def from_dict(data: dict[str, Any]): @@ -69,6 +70,21 @@ def from_dict(data: dict[str, Any]): _move_field(data, "style", data["metadata"]) _move_field(data, "sampler", data["metadata"]) _move_field(data, "checkpoint", data["metadata"]) + if "resize_canvas" in data and data["resize_canvas"] is not None: + resize = data["resize_canvas"] + try: + if isinstance(resize, (list, tuple)) and len(resize) == 2: + data["resize_canvas"] = Extent(int(resize[0]), int(resize[1])) + elif isinstance(resize, dict): + width = int(resize.get("width", 0)) + height = int(resize.get("height", 0)) + data["resize_canvas"] = Extent(width, height) + elif isinstance(resize, Extent): + data["resize_canvas"] = resize + else: + data["resize_canvas"] = None + except Exception: + data["resize_canvas"] = None return JobParams(**data) @classmethod diff --git a/ai_diffusion/model.py b/ai_diffusion/model.py index 092ccc2fd..977e5a979 100644 --- a/ai_diffusion/model.py +++ b/ai_diffusion/model.py @@ -139,7 +139,6 @@ def __init__(self, document: Document, connection: Connection, workflows: Workfl self._doc = document self._connection = connection self._layer: Layer | None = None - self._preview_canvas_original_extent: Extent | None = None self.generate_seed() self.jobs = JobQueue() self.regions = RootRegion(self) @@ -641,81 +640,10 @@ def _apply_resize_command(self, cmd: ResizeCommand, job: Job): try: bounds = job.params.bounds job.params.bounds = Bounds(bounds.x, bounds.y, cmd.width, cmd.height) - job.params.metadata["resize_canvas"] = {"width": cmd.width, "height": cmd.height} + job.params.resize_canvas = Extent(cmd.width, cmd.height) except Exception as e: log.warning(f"Failed to store resize command from custom workflow: {e}") - def _get_resize_for_params(self, params: JobParams) -> tuple[int, int] | None: - resize = params.metadata.get("resize_canvas") - if not isinstance(resize, dict): - return None - - try: - width = int(resize.get("width", 0)) - height = int(resize.get("height", 0)) - except Exception: - return None - - if width <= 0 or height <= 0: - return None - - return width, height - - def _preview_canvas_size(self, params: JobParams): - """Resize canvas for preview, remembering original size so it can be reverted. - - This only affects temporary previews. The canvas is reverted when - there are no history selections (see hide_preview). - """ - resize = self._get_resize_for_params(params) - if resize is None: - return - - width, height = resize - extent = self.document.extent - if extent.width == width and extent.height == height: - return - if self._preview_canvas_original_extent is None: - self._preview_canvas_original_extent = extent - - try: - self.document.resize_canvas(width, height) - except Exception as e: - log.warning(f"Failed to resize canvas for preview from custom workflow: {e}") - - def _reset_preview_canvas_size(self): - """Revert canvas size after preview, if it was resized just for preview.""" - if self._preview_canvas_original_extent is None: - return - - original = self._preview_canvas_original_extent - self._preview_canvas_original_extent = None - - try: - self.document.resize_canvas(original.width, original.height) - except Exception as e: - log.warning(f"Failed to revert canvas size after preview: {e}") - - def _apply_canvas_resize_for_params(self, params: JobParams): - """Resize the canvas permanently if a resize was requested in metadata. - - Once applied, the resize hint is cleared so future previews don't - treat it as a pending temporary resize. - """ - resize = self._get_resize_for_params(params) - if resize is None: - return - - width, height = resize - - try: - extent = self.document.extent - if extent.width != width or extent.height != height: - self.document.resize_canvas(width, height) - params.metadata.pop("resize_canvas", None) - except Exception as e: - log.warning(f"Failed to resize canvas from custom workflow: {e}") - def update_preview(self): if selection := self.jobs.selection: self.show_preview(selection[0].job, selection[0].image) @@ -728,8 +656,6 @@ def show_preview(self, job_id: str, index: int, name_prefix="Preview"): if job.kind is JobKind.animation: return # don't show animation preview on canvas (it's slow and clumsy) - self._preview_canvas_size(job.params) - name = f"[{name_prefix}] {trim_text(job.params.name, 77)}" image = job.results[index] bounds = job.params.bounds @@ -752,7 +678,6 @@ def hide_preview(self, delete_layer=False): self._layer = None else: self._layer.hide() - self._reset_preview_canvas_size() def apply_result( self, @@ -762,8 +687,16 @@ def apply_result( region_behavior=ApplyRegionBehavior.layer_group, prefix="", ): - self._apply_canvas_resize_for_params(params) - self._preview_canvas_original_extent = None + if params.resize_canvas is not None: + target = params.resize_canvas + try: + extent = self.document.extent + if extent.width != target.width or extent.height != target.height: + self.document.resize_canvas(target.width, target.height) + except Exception as e: + log.warning(f"Failed to resize canvas from custom workflow: {e}") + finally: + params.resize_canvas = None bounds = Bounds(*params.bounds.offset, *image.extent) if len(params.regions) == 0 or region_behavior is ApplyRegionBehavior.none: From 8451f481217bd61a42794260b13d57356c57bb2b Mon Sep 17 00:00:00 2001 From: Kebolder Date: Thu, 11 Dec 2025 13:32:58 -0600 Subject: [PATCH 05/11] Updated resize a bit more to support actual commands instead of the json output --- ai_diffusion/__init__.py | 11 ++++++++--- ai_diffusion/comfy_client.py | 27 +++++++++++++++++++++++++++ ai_diffusion/jobs.py | 35 +++++++++++++++++++---------------- ai_diffusion/model.py | 21 ++++++++++++--------- ai_diffusion/util.py | 6 +++++- 5 files changed, 71 insertions(+), 29 deletions(-) diff --git a/ai_diffusion/__init__.py b/ai_diffusion/__init__.py index e343c39b8..eba80db34 100644 --- a/ai_diffusion/__init__.py +++ b/ai_diffusion/__init__.py @@ -13,6 +13,11 @@ ) -# The following imports depend on the code running inside Krita, so the cannot be imported in tests. -if importlib.util.find_spec("krita"): - from .extension import AIToolsExtension as AIToolsExtension +# The following imports depend on the code running inside Krita, so they cannot be imported in tests. +_krita_spec = importlib.util.find_spec("krita") +if _krita_spec is not None: + origin = getattr(_krita_spec, "origin", "") or "" + # Avoid treating local helper modules named `krita.py` (e.g. tooling nodes) + # as the actual Krita application module. + if not origin.endswith("krita.py"): + from .extension import AIToolsExtension as AIToolsExtension diff --git a/ai_diffusion/comfy_client.py b/ai_diffusion/comfy_client.py index 248c1b3f8..80647fea3 100644 --- a/ai_diffusion/comfy_client.py +++ b/ai_diffusion/comfy_client.py @@ -422,6 +422,9 @@ async def _listen_websocket(self, websocket: websockets.ClientConnection): text_output = _extract_text_output(job.id, msg) if text_output is not None: await self._messages.put(text_output) + resize_cmd = _extract_resize_output(job.id, msg) + if resize_cmd is not None: + await self._messages.put(resize_cmd) pose_json = _extract_pose_json(msg) if pose_json is not None: result = pose_json @@ -939,3 +942,27 @@ def _extract_text_output(job_id: str, msg: dict): except Exception as e: log.warning(f"Error processing message, error={str(e)}, msg={msg}") return None + + +def _extract_resize_output(job_id: str, msg: dict): + """Extract a Krita canvas resize toggle encoded directly in the UI output.""" + try: + output = msg["data"]["output"] + if output is None: + return None + + resize = output.get("resize_canvas") + if isinstance(resize, list): + active = any(bool(item) for item in resize) + else: + active = bool(resize) + + if not active: + return None + + # Use a lightweight dict result; the Krita client will interpret this + # as "resize canvas to match image extent" on apply. + return ClientMessage(ClientEvent.output, job_id, result={"resize_canvas": True}) + except Exception as e: + log.warning(f"Error processing Krita resize output: {e}, msg={msg}") + return None diff --git a/ai_diffusion/jobs.py b/ai_diffusion/jobs.py index 495e5ce89..e0ff3a2a4 100644 --- a/ai_diffusion/jobs.py +++ b/ai_diffusion/jobs.py @@ -6,7 +6,7 @@ from typing import Any, NamedTuple, TYPE_CHECKING from PyQt5.QtCore import QObject, pyqtSignal -from .image import Bounds, Extent, ImageCollection +from .image import Bounds, ImageCollection from .settings import settings from .style import Style from .util import ensure @@ -55,7 +55,7 @@ class JobParams: has_mask: bool = False frame: tuple[int, int, int] = (0, 0, 0) animation_id: str = "" - resize_canvas: Extent | None = None + resize_canvas: bool = False @staticmethod def from_dict(data: dict[str, Any]): @@ -70,21 +70,24 @@ def from_dict(data: dict[str, Any]): _move_field(data, "style", data["metadata"]) _move_field(data, "sampler", data["metadata"]) _move_field(data, "checkpoint", data["metadata"]) - if "resize_canvas" in data and data["resize_canvas"] is not None: + if "resize_canvas" in data: resize = data["resize_canvas"] - try: - if isinstance(resize, (list, tuple)) and len(resize) == 2: - data["resize_canvas"] = Extent(int(resize[0]), int(resize[1])) - elif isinstance(resize, dict): - width = int(resize.get("width", 0)) - height = int(resize.get("height", 0)) - data["resize_canvas"] = Extent(width, height) - elif isinstance(resize, Extent): - data["resize_canvas"] = resize - else: - data["resize_canvas"] = None - except Exception: - data["resize_canvas"] = None + # Backwards compatibility: accept various legacy forms and coerce to bool. + if isinstance(resize, dict): + try: + w = int(resize.get("width", 0)) + h = int(resize.get("height", 0)) + data["resize_canvas"] = w > 0 and h > 0 + except Exception: + data["resize_canvas"] = False + elif isinstance(resize, (list, tuple)) and len(resize) == 2: + try: + w, h = int(resize[0]), int(resize[1]) + data["resize_canvas"] = w > 0 and h > 0 + except Exception: + data["resize_canvas"] = False + else: + data["resize_canvas"] = bool(resize) return JobParams(**data) @classmethod diff --git a/ai_diffusion/model.py b/ai_diffusion/model.py index 977e5a979..0bb7c1933 100644 --- a/ai_diffusion/model.py +++ b/ai_diffusion/model.py @@ -591,6 +591,8 @@ def handle_message(self, message: ClientMessage): elif message.event is ClientEvent.output: if isinstance(message.result, ResizeCommand): self._apply_resize_command(message.result, job) + elif isinstance(message.result, dict) and message.result.get("resize_canvas"): + job.params.resize_canvas = True else: self.custom.show_output(message.result) elif message.event is ClientEvent.finished: @@ -633,14 +635,14 @@ def _finish_job(self, job: Job, event: ClientEvent): self.progress = 0 def _apply_resize_command(self, cmd: ResizeCommand, job: Job): - """Record a requested canvas resize for this job. + """Legacy: record a requested canvas resize for this job. - The actual resize is applied when showing a preview or applying the result. + Newer workflows use a simple resize toggle in the UI output instead. """ try: bounds = job.params.bounds job.params.bounds = Bounds(bounds.x, bounds.y, cmd.width, cmd.height) - job.params.resize_canvas = Extent(cmd.width, cmd.height) + job.params.resize_canvas = True except Exception as e: log.warning(f"Failed to store resize command from custom workflow: {e}") @@ -687,16 +689,17 @@ def apply_result( region_behavior=ApplyRegionBehavior.layer_group, prefix="", ): - if params.resize_canvas is not None: - target = params.resize_canvas + if params.resize_canvas: try: - extent = self.document.extent - if extent.width != target.width or extent.height != target.height: - self.document.resize_canvas(target.width, target.height) + extent = image.extent + target_width, target_height = extent.width, extent.height + current = self.document.extent + if current.width != target_width or current.height != target_height: + self.document.resize_canvas(target_width, target_height) except Exception as e: log.warning(f"Failed to resize canvas from custom workflow: {e}") finally: - params.resize_canvas = None + params.resize_canvas = False bounds = Bounds(*params.bounds.offset, *image.extent) if len(params.regions) == 0 or region_behavior is ApplyRegionBehavior.none: diff --git a/ai_diffusion/util.py b/ai_diffusion/util.py index 73fe9355e..4824fc10b 100644 --- a/ai_diffusion/util.py +++ b/ai_diffusion/util.py @@ -22,7 +22,11 @@ def _get_user_data_dir(): - if importlib.util.find_spec("krita") is None: + spec = importlib.util.find_spec("krita") + origin = getattr(spec, "origin", "") if spec is not None else "" + # Treat local helper modules named `krita.py` as "no Krita" so tests and + # non-Krita environments use a local appdata directory. + if spec is None or (origin and origin.endswith("krita.py")): dir = plugin_dir.parent / ".appdata" dir.mkdir(exist_ok=True) return dir From 798b1e44838bdccc332fa05bc5c7eece5aa1ee88 Mon Sep 17 00:00:00 2001 From: Kebolder Date: Thu, 11 Dec 2025 13:38:07 -0600 Subject: [PATCH 06/11] Removed old backwards code from jobs.py --- ai_diffusion/jobs.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/ai_diffusion/jobs.py b/ai_diffusion/jobs.py index e0ff3a2a4..e2b93b56a 100644 --- a/ai_diffusion/jobs.py +++ b/ai_diffusion/jobs.py @@ -70,24 +70,6 @@ def from_dict(data: dict[str, Any]): _move_field(data, "style", data["metadata"]) _move_field(data, "sampler", data["metadata"]) _move_field(data, "checkpoint", data["metadata"]) - if "resize_canvas" in data: - resize = data["resize_canvas"] - # Backwards compatibility: accept various legacy forms and coerce to bool. - if isinstance(resize, dict): - try: - w = int(resize.get("width", 0)) - h = int(resize.get("height", 0)) - data["resize_canvas"] = w > 0 and h > 0 - except Exception: - data["resize_canvas"] = False - elif isinstance(resize, (list, tuple)) and len(resize) == 2: - try: - w, h = int(resize[0]), int(resize[1]) - data["resize_canvas"] = w > 0 and h > 0 - except Exception: - data["resize_canvas"] = False - else: - data["resize_canvas"] = bool(resize) return JobParams(**data) @classmethod From 25ad67d6416b1fae4294529638e8058c9efcf5b4 Mon Sep 17 00:00:00 2001 From: Kebolder Date: Mon, 15 Dec 2025 18:36:44 -0600 Subject: [PATCH 07/11] Updated resizing from node support. * Removed old text output JSON * Dropped the dict handling * Updated ResizeCommand to a bool now --- ai_diffusion/client.py | 11 +++++++++-- ai_diffusion/comfy_client.py | 18 +++--------------- ai_diffusion/model.py | 13 +++++-------- 3 files changed, 17 insertions(+), 25 deletions(-) diff --git a/ai_diffusion/client.py b/ai_diffusion/client.py index 5f8ed93b6..150ccd069 100644 --- a/ai_diffusion/client.py +++ b/ai_diffusion/client.py @@ -39,8 +39,15 @@ class TextOutput(NamedTuple): class ResizeCommand(NamedTuple): - width: int - height: int + """Request that Krita resizes the canvas when applying a generated result. + + - If `match_image_extent` is True, the canvas is resized to the generated image extent. + - Otherwise, `width` and `height` (when provided) specify the target canvas size. + """ + + match_image_extent: bool = False + width: int | None = None + height: int | None = None class SharedWorkflow(NamedTuple): diff --git a/ai_diffusion/comfy_client.py b/ai_diffusion/comfy_client.py index 80647fea3..b6f87ae07 100644 --- a/ai_diffusion/comfy_client.py +++ b/ai_diffusion/comfy_client.py @@ -921,18 +921,6 @@ def _extract_text_output(job_id: str, msg: dict): text = payload.get("text") name = payload.get("name") mime = payload.get("content-type", mime) - # Special case: Krita canvas resize command produced by a tooling node - if mime == "application/x-krita-command" and isinstance(text, str): - try: - data = json.loads(text) - if data.get("action") == "resize_canvas": - width = int(data.get("width", 0)) - height = int(data.get("height", 0)) - if width > 0 and height > 0: - cmd = ResizeCommand(width, height) - return ClientMessage(ClientEvent.output, job_id, result=cmd) - except Exception as e: - log.warning(f"Failed to process Krita command output: {e}") elif isinstance(payload, str): text = payload name = f"Node {key}" @@ -960,9 +948,9 @@ def _extract_resize_output(job_id: str, msg: dict): if not active: return None - # Use a lightweight dict result; the Krita client will interpret this - # as "resize canvas to match image extent" on apply. - return ClientMessage(ClientEvent.output, job_id, result={"resize_canvas": True}) + return ClientMessage( + ClientEvent.output, job_id, result=ResizeCommand(match_image_extent=True) + ) except Exception as e: log.warning(f"Error processing Krita resize output: {e}, msg={msg}") return None diff --git a/ai_diffusion/model.py b/ai_diffusion/model.py index 0bb7c1933..d3fa3068c 100644 --- a/ai_diffusion/model.py +++ b/ai_diffusion/model.py @@ -591,8 +591,6 @@ def handle_message(self, message: ClientMessage): elif message.event is ClientEvent.output: if isinstance(message.result, ResizeCommand): self._apply_resize_command(message.result, job) - elif isinstance(message.result, dict) and message.result.get("resize_canvas"): - job.params.resize_canvas = True else: self.custom.show_output(message.result) elif message.event is ClientEvent.finished: @@ -635,14 +633,13 @@ def _finish_job(self, job: Job, event: ClientEvent): self.progress = 0 def _apply_resize_command(self, cmd: ResizeCommand, job: Job): - """Legacy: record a requested canvas resize for this job. - - Newer workflows use a simple resize toggle in the UI output instead. - """ try: - bounds = job.params.bounds - job.params.bounds = Bounds(bounds.x, bounds.y, cmd.width, cmd.height) job.params.resize_canvas = True + if not cmd.match_image_extent: + if cmd.width is None or cmd.height is None: + return + bounds = job.params.bounds + job.params.bounds = Bounds(bounds.x, bounds.y, cmd.width, cmd.height) except Exception as e: log.warning(f"Failed to store resize command from custom workflow: {e}") From 7ca69efb31bb3614a08965b55f031906db0feb5c Mon Sep 17 00:00:00 2001 From: Kebolder Date: Mon, 15 Dec 2025 18:47:37 -0600 Subject: [PATCH 08/11] Updated again, vscode didn't publish this one? + fixed conflict --- ai_diffusion/client.py | 10 +--------- ai_diffusion/comfy_client.py | 6 ++---- ai_diffusion/model.py | 7 +------ 3 files changed, 4 insertions(+), 19 deletions(-) diff --git a/ai_diffusion/client.py b/ai_diffusion/client.py index 150ccd069..4ee578f46 100644 --- a/ai_diffusion/client.py +++ b/ai_diffusion/client.py @@ -39,15 +39,7 @@ class TextOutput(NamedTuple): class ResizeCommand(NamedTuple): - """Request that Krita resizes the canvas when applying a generated result. - - - If `match_image_extent` is True, the canvas is resized to the generated image extent. - - Otherwise, `width` and `height` (when provided) specify the target canvas size. - """ - - match_image_extent: bool = False - width: int | None = None - height: int | None = None + resize_canvas: bool = False class SharedWorkflow(NamedTuple): diff --git a/ai_diffusion/comfy_client.py b/ai_diffusion/comfy_client.py index b6f87ae07..e07089994 100644 --- a/ai_diffusion/comfy_client.py +++ b/ai_diffusion/comfy_client.py @@ -12,7 +12,7 @@ from .api import WorkflowInput from .client import Client, CheckpointInfo, ClientMessage, ClientEvent, DeviceInfo, ClientModels -from .client import SharedWorkflow, TranslationPackage, ClientFeatures, TextOutput, ResizeCommand +from .client import SharedWorkflow, TranslationPackage, ClientFeatures, ClientJobQueue, TextOutput, ResizeCommand from .client import Quantization, MissingResources, filter_supported_styles, loras_to_upload from .comfy_workflow import ComfyObjectInfo from .files import FileFormat @@ -948,9 +948,7 @@ def _extract_resize_output(job_id: str, msg: dict): if not active: return None - return ClientMessage( - ClientEvent.output, job_id, result=ResizeCommand(match_image_extent=True) - ) + return ClientMessage(ClientEvent.output, job_id, result=ResizeCommand(True)) except Exception as e: log.warning(f"Error processing Krita resize output: {e}, msg={msg}") return None diff --git a/ai_diffusion/model.py b/ai_diffusion/model.py index d3fa3068c..a5836e962 100644 --- a/ai_diffusion/model.py +++ b/ai_diffusion/model.py @@ -634,12 +634,7 @@ def _finish_job(self, job: Job, event: ClientEvent): def _apply_resize_command(self, cmd: ResizeCommand, job: Job): try: - job.params.resize_canvas = True - if not cmd.match_image_extent: - if cmd.width is None or cmd.height is None: - return - bounds = job.params.bounds - job.params.bounds = Bounds(bounds.x, bounds.y, cmd.width, cmd.height) + job.params.resize_canvas = bool(cmd.resize_canvas) except Exception as e: log.warning(f"Failed to store resize command from custom workflow: {e}") From 2e4cf064be9de63ac0bb305f3324f73b501c85cc Mon Sep 17 00:00:00 2001 From: Kebolder Date: Mon, 15 Dec 2025 18:54:06 -0600 Subject: [PATCH 09/11] I always forget to run ruff format EEEEEEEEE --- ai_diffusion/comfy_client.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ai_diffusion/comfy_client.py b/ai_diffusion/comfy_client.py index e07089994..336bc2301 100644 --- a/ai_diffusion/comfy_client.py +++ b/ai_diffusion/comfy_client.py @@ -12,7 +12,14 @@ from .api import WorkflowInput from .client import Client, CheckpointInfo, ClientMessage, ClientEvent, DeviceInfo, ClientModels -from .client import SharedWorkflow, TranslationPackage, ClientFeatures, ClientJobQueue, TextOutput, ResizeCommand +from .client import ( + SharedWorkflow, + TranslationPackage, + ClientFeatures, + ClientJobQueue, + TextOutput, + ResizeCommand, +) from .client import Quantization, MissingResources, filter_supported_styles, loras_to_upload from .comfy_workflow import ComfyObjectInfo from .files import FileFormat From af7ba33085b4e8c0b57107384218396a7b722c46 Mon Sep 17 00:00:00 2001 From: Kebolder Date: Sun, 21 Dec 2025 15:14:13 -0600 Subject: [PATCH 10/11] Updated sections as requested reverted the changes fro m local branch --- .gitignore | 3 +-- ai_diffusion/__init__.py | 12 +++--------- ai_diffusion/model.py | 18 +++--------------- ai_diffusion/util.py | 8 ++------ 4 files changed, 9 insertions(+), 32 deletions(-) diff --git a/.gitignore b/.gitignore index e1eb48aaf..96b8a5d0e 100644 --- a/.gitignore +++ b/.gitignore @@ -24,5 +24,4 @@ venv .DS_Store bin lib -pyvenv.cfg -release_notes.md \ No newline at end of file +pyvenv.cfg \ No newline at end of file diff --git a/ai_diffusion/__init__.py b/ai_diffusion/__init__.py index eba80db34..3e2662fb4 100644 --- a/ai_diffusion/__init__.py +++ b/ai_diffusion/__init__.py @@ -12,12 +12,6 @@ " https://github.com/Acly/krita-ai-diffusion/releases" ) - -# The following imports depend on the code running inside Krita, so they cannot be imported in tests. -_krita_spec = importlib.util.find_spec("krita") -if _krita_spec is not None: - origin = getattr(_krita_spec, "origin", "") or "" - # Avoid treating local helper modules named `krita.py` (e.g. tooling nodes) - # as the actual Krita application module. - if not origin.endswith("krita.py"): - from .extension import AIToolsExtension as AIToolsExtension +# The following imports depend on the code running inside Krita, so the cannot be imported in tests. +if importlib.util.find_spec("krita"): + from .extension import AIToolsExtension as AIToolsExtension \ No newline at end of file diff --git a/ai_diffusion/model.py b/ai_diffusion/model.py index a5836e962..3ea6fb52d 100644 --- a/ai_diffusion/model.py +++ b/ai_diffusion/model.py @@ -633,10 +633,7 @@ def _finish_job(self, job: Job, event: ClientEvent): self.progress = 0 def _apply_resize_command(self, cmd: ResizeCommand, job: Job): - try: - job.params.resize_canvas = bool(cmd.resize_canvas) - except Exception as e: - log.warning(f"Failed to store resize command from custom workflow: {e}") + job.params.resize_canvas = cmd.resize_canvas def update_preview(self): if selection := self.jobs.selection: @@ -681,17 +678,8 @@ def apply_result( region_behavior=ApplyRegionBehavior.layer_group, prefix="", ): - if params.resize_canvas: - try: - extent = image.extent - target_width, target_height = extent.width, extent.height - current = self.document.extent - if current.width != target_width or current.height != target_height: - self.document.resize_canvas(target_width, target_height) - except Exception as e: - log.warning(f"Failed to resize canvas from custom workflow: {e}") - finally: - params.resize_canvas = False + if params.resize_canvas and self.document.extent != image.extent: + self.document.resize_canvas(*image.extent) bounds = Bounds(*params.bounds.offset, *image.extent) if len(params.regions) == 0 or region_behavior is ApplyRegionBehavior.none: diff --git a/ai_diffusion/util.py b/ai_diffusion/util.py index 4824fc10b..db4e830fa 100644 --- a/ai_diffusion/util.py +++ b/ai_diffusion/util.py @@ -22,11 +22,7 @@ def _get_user_data_dir(): - spec = importlib.util.find_spec("krita") - origin = getattr(spec, "origin", "") if spec is not None else "" - # Treat local helper modules named `krita.py` as "no Krita" so tests and - # non-Krita environments use a local appdata directory. - if spec is None or (origin and origin.endswith("krita.py")): + if importlib.util.find_spec("krita") is None: dir = plugin_dir.parent / ".appdata" dir.mkdir(exist_ok=True) return dir @@ -196,4 +192,4 @@ def acquire_elements(l: list[QOBJECT]) -> list[QOBJECT]: for obj in l: if obj is not None: sip.transferback(obj) - return l + return l \ No newline at end of file From 0d2c703ab4f56dbe15a64dd532f695c886c806ba Mon Sep 17 00:00:00 2001 From: Kebolder Date: Sun, 21 Dec 2025 15:15:28 -0600 Subject: [PATCH 11/11] Ruff format --- ai_diffusion/__init__.py | 2 +- ai_diffusion/util.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ai_diffusion/__init__.py b/ai_diffusion/__init__.py index 3e2662fb4..98c95e69a 100644 --- a/ai_diffusion/__init__.py +++ b/ai_diffusion/__init__.py @@ -14,4 +14,4 @@ # The following imports depend on the code running inside Krita, so the cannot be imported in tests. if importlib.util.find_spec("krita"): - from .extension import AIToolsExtension as AIToolsExtension \ No newline at end of file + from .extension import AIToolsExtension as AIToolsExtension diff --git a/ai_diffusion/util.py b/ai_diffusion/util.py index db4e830fa..73fe9355e 100644 --- a/ai_diffusion/util.py +++ b/ai_diffusion/util.py @@ -192,4 +192,4 @@ def acquire_elements(l: list[QOBJECT]) -> list[QOBJECT]: for obj in l: if obj is not None: sip.transferback(obj) - return l \ No newline at end of file + return l