diff --git a/sd_prompt_reader/format/__init__.py b/sd_prompt_reader/format/__init__.py index ecfe2b6..90190b5 100644 --- a/sd_prompt_reader/format/__init__.py +++ b/sd_prompt_reader/format/__init__.py @@ -12,3 +12,4 @@ from .drawthings import DrawThings from .swarmui import SwarmUI from .fooocus import Fooocus +from .civitai import CivitaiComfyUIFormat diff --git a/sd_prompt_reader/format/civitai.py b/sd_prompt_reader/format/civitai.py new file mode 100644 index 0000000..db2ed17 --- /dev/null +++ b/sd_prompt_reader/format/civitai.py @@ -0,0 +1,160 @@ +# sd_prompt_reader/format/civitai.py +# (For your Ktiseos-Nyx/stable-diffusion-prompt-reader fork, mojibake branch) +# Refined version with fixes for double parsing and width/height handling. + +import json +import re +from .base_format import BaseFormat +from ..logger import Logger + +class CivitaiComfyUIFormat(BaseFormat): + """ + Parser for UserComment metadata found in JPEGs generated by + Civitai's ComfyUI-based service. This typically involves a + "charset=Unicode" prefix and potentially a mojibake UTF-16LE JSON string + (if piexif doesn't fully clean it) representing the ComfyUI workflow. + The workflow contains an "extraMetadata" key with A1111-style parameters. + """ + def __init__(self, info: dict = None, raw: str = "", width: int = 0, height: int = 0): # Added width, height + super().__init__(info, raw, width, height) # Pass width, height to BaseFormat + self._logger = Logger(f"SDPR.{self.__class__.__name__}") + self.workflow_data: dict | None = None + self.tool = "Civitai ComfyUI" # Set the tool name this parser identifies + + def _decode_user_comment_for_civitai(self, uc_string: str) -> str | None: + """ + Decodes/cleans the UserComment string, expecting piexif's output. + Handles "charset=Unicode" prefix and potential mojibake reversal. + Returns a clean JSON string if successful, else None. + """ + self._logger.debug(f"Attempting to decode UserComment (first 70): '{uc_string[:70]}'") + if not uc_string or not isinstance(uc_string, str): + self._logger.warn("UserComment string is empty or not a string.") + return None + + data_to_process = uc_string + prefix_pattern = r'^charset\s*=\s*["\']?(UNICODE|UTF-16|UTF-16LE)["\']?\s*' + match = re.match(prefix_pattern, uc_string, re.IGNORECASE) + if match: + data_to_process = uc_string[len(match.group(0)):].strip() + self._logger.debug(f"Stripped 'charset=Unicode' prefix. Remaining: '{data_to_process[:50]}'") + + # Check for mojibake ONLY if piexif's output might still contain it. + # Based on logs, piexif seems to provide clean JSON, so this block might not be hit often + # when used within SDPR's ImageDataReader (which uses piexif). + # It's kept for robustness or if piexif's behavior changes or varies. + if ('笀' in data_to_process or '∀' in data_to_process or 'izarea' in data_to_process) and \ + not (data_to_process.startswith('{') and data_to_process.endswith('}')): + self._logger.debug("Mojibake characters detected. Attempting reversal.") + try: + byte_list = [] + for char_from_mojibake in data_to_process: + codepoint_val = ord(char_from_mojibake) + byte_list.append((codepoint_val >> 8) & 0xFF) + byte_list.append(codepoint_val & 0xFF) + + recovered_bytes = bytes(byte_list) + json_string = recovered_bytes.decode('utf-16le', errors='strict') + json.loads(json_string) # Validate + self._logger.debug("Mojibake reversal successful.") + return json_string + except Exception as e: + self._logger.warn(f"Mojibake reversal failed: {e}") + return None + + # If no mojibake (or reversal failed), check if it's already plain JSON + if data_to_process.startswith('{') and data_to_process.endswith('}'): + self._logger.debug("Data (post-prefix/post-mojibake-attempt) looks like plain JSON. Validating.") + try: + json.loads(data_to_process) + self._logger.debug("Plain JSON validation successful.") + return data_to_process + except json.JSONDecodeError as e: + self._logger.warn(f"Plain JSON validation failed: {e}") + return None + + self._logger.debug("UserComment string not recognized as Civitai JSON (mojibake or plain) after processing.") + return None + + def parse(self): + # Prevent re-parsing if already successful + if self._status == BaseFormat.Status.READ_SUCCESS: + self._logger.debug(f"{self.__class__.__name__} already parsed successfully. Skipping re-parse.") + return self._status + + self._logger.info(f"Attempting to parse using {self.__class__.__name__}.") + if not self._raw: + self._logger.warn("Raw data (UserComment from piexif) is empty. Cannot parse.") + self._status = BaseFormat.Status.FORMAT_ERROR + self._error = "Raw UserComment data is empty." + return self._status + + cleaned_workflow_json_str = self._decode_user_comment_for_civitai(self._raw) + + if not cleaned_workflow_json_str: + self._logger.warn("Failed to decode UserComment or not a Civitai JSON format for this parser.") + self._status = BaseFormat.Status.FORMAT_ERROR + self._error = "UserComment decoding failed or not the specific Civitai JSON structure." + return self._status + + try: + self.workflow_data = json.loads(cleaned_workflow_json_str) + self._logger.info("Successfully parsed main workflow JSON from UserComment.") + except json.JSONDecodeError as e: + self._logger.error(f"Decoded UserComment is not valid JSON: {e}") + self._status = BaseFormat.Status.FORMAT_ERROR + self._error = f"Invalid JSON in decoded UserComment: {e}" + return self._status + + extra_metadata_str = self.workflow_data.get("extraMetadata") + if extra_metadata_str and isinstance(extra_metadata_str, str): + self._logger.info("Found 'extraMetadata' string. Attempting to parse.") + try: + extra_meta_dict = json.loads(extra_metadata_str) + self._logger.debug("'extraMetadata' parsed into dict successfully.") + + self._positive = extra_meta_dict.get("prompt", "") + self._negative = extra_meta_dict.get("negativePrompt", "") + + self._parameter = {} + if "steps" in extra_meta_dict: self._parameter["steps"] = str(extra_meta_dict["steps"]) + + if "CFG scale" in extra_meta_dict: self._parameter["cfg_scale"] = str(extra_meta_dict["CFG scale"]) + elif "cfgScale" in extra_meta_dict: self._parameter["cfg_scale"] = str(extra_meta_dict["cfgScale"]) + + if "sampler" in extra_meta_dict: self._parameter["sampler_name"] = str(extra_meta_dict["sampler"]) + elif "sampler_name" in extra_meta_dict: self._parameter["sampler_name"] = str(extra_meta_dict["sampler_name"]) + + if "seed" in extra_meta_dict: self._parameter["seed"] = str(extra_meta_dict["seed"]) + + # Use width/height from extraMetadata if available, otherwise keep what was passed in __init__ + # BaseFormat __init__ already sets self._width and self._height + if "width" in extra_meta_dict: self._width = str(extra_meta_dict.get("width")) + if "height" in extra_meta_dict: self._height = str(extra_meta_dict.get("height")) + # If not found, self._width and self._height retain values from ImageDataReader (via super init) + + self._raw_setting = extra_metadata_str # Store the raw extraMetadata JSON string + + self._logger.info("Successfully extracted parameters from 'extraMetadata'.") + self._status = BaseFormat.Status.READ_SUCCESS + + except json.JSONDecodeError as e_extra: + self._logger.error(f"Failed to parse JSON from 'extraMetadata': {e_extra}") + self._status = BaseFormat.Status.FORMAT_ERROR + self._error = f"Invalid JSON in 'extraMetadata': {e_extra}" + # self.raw is set below even if extraMetadata fails, as main workflow was parsed. + else: + self._logger.warn("'extraMetadata' not found or not a string in UserComment workflow.") + # This parser specifically targets Civitai workflows that HAVE extraMetadata. + # If it's missing, it's a format error FOR THIS PARSER. + # A generic ComfyUI parser might handle workflows without extraMetadata. + self._status = BaseFormat.Status.FORMAT_ERROR + self._error = "'extraMetadata' missing or invalid in Civitai UserComment." + + # self._raw should store the primary data chunk this parser worked on before detailed parsing. + # In this case, it's the cleaned_workflow_json_str. + # Their other parsers often set self._raw to the input string if it's A1111 text, + # or to the "parameters" from PNGInfo. + self._raw = cleaned_workflow_json_str + + return self._status diff --git a/sd_prompt_reader/image_data_reader.py b/sd_prompt_reader/image_data_reader.py index 8305799..5b884ff 100644 --- a/sd_prompt_reader/image_data_reader.py +++ b/sd_prompt_reader/image_data_reader.py @@ -1,5 +1,10 @@ +# HYPOTHETICAL MODIFIED version of +# receyuki/stable-diffusion-prompt-reader/sd_prompt_reader/image_data_reader.py +# showing integration of a new CivitaiComfyUIFormat parser. +# ALL original methods and properties are included in this version. + __author__ = "receyuki" -__filename__ = "image_data_reader.py" +__filename__ = "image_data_reader.py" # (Hypothetically Modified) __copyright__ = "Copyright 2023" __email__ = "receyuki@gmail.com" @@ -23,6 +28,10 @@ DrawThings, SwarmUI, Fooocus, + # --- HYPOTHETICAL ADDITION: Import your new parser --- + CivitaiComfyUIFormat, # Assuming this would be added to format/__init__.py and assuming it's a class i can import?) + # --- For direct import if not in __init__.py during development: + # from .format.civitai import CivitaiComfyUIFormat ) @@ -46,7 +55,7 @@ def __init__(self, file, is_txt: bool = False): self._is_sdxl = False self._format = "" self._props = "" - self._parser = None + self._parser = None self._status = BaseFormat.Status.UNREAD self._logger = Logger("SD_Prompt_Reader.ImageDataReader") self.read_data(file) @@ -55,176 +64,167 @@ def read_data(self, file): if self._is_txt: self._raw = file.read() self._parser = A1111(raw=self._raw) - return + if self._parser: self._tool = getattr(self._parser, 'tool', "A1111 webUI (txt)") + # According to their original code, this path returns early. + return + with Image.open(file) as f: self._width = f.width self._height = f.height - self._info = f.info - self._format = f.format - # swarm legacy format + self._info = f.info + self._format = f.format + self._parser = None + + # --- SwarmUI Legacy EXIF (ModelID 0x0110) --- try: - exif = json.loads(f.getexif().get(0x0110)) - if "sui_image_params" in exif: - self._tool = "StableSwarmUI" - self._parser = SwarmUI(info=exif) - except TypeError: - if f.format == "PNG": - if "parameters" in self._info: - # swarm format - if "sui_image_params" in self._info.get("parameters"): - self._tool = "StableSwarmUI" - self._parser = SwarmUI(raw=self._info.get("parameters")) - # a1111 png compatible format - else: - if "prompt" in self._info: - self._tool = "ComfyUI\n(A1111 compatible)" - else: - self._tool = "A1111 webUI" - self._parser = A1111(info=self._info) - elif "postprocessing" in self._info: - self._tool = "A1111 webUI\n(Postprocessing)" - self._parser = A1111(info=self._info) - # easydiff png format - elif ( - "negative_prompt" in self._info - or "Negative Prompt" in self._info - ): - self._tool = "Easy Diffusion" - self._parser = EasyDiffusion(info=self._info) - # invokeai3 format - elif "invokeai_metadata" in self._info: - self._tool = "InvokeAI" - self._parser = InvokeAI(info=self._info) - # invokeai2 format - elif "sd-metadata" in self._info: - self._tool = "InvokeAI" - self._parser = InvokeAI(info=self._info) - # invokeai legacy dream format - elif "Dream" in self._info: - self._tool = "InvokeAI" - self._parser = InvokeAI(info=self._info) - # novelai legacy format - elif self._info.get("Software") == "NovelAI": - self._tool = "NovelAI" - self._parser = NovelAI( - info=self._info, width=self._width, height=self._height - ) - # comfyui format - elif "prompt" in self._info: - self._tool = "ComfyUI" - self._parser = ComfyUI( - info=self._info, width=self._width, height=self._height - ) - # fooocus format - elif "Comment" in self._info: - try: - self._tool = "Fooocus" - self._parser = Fooocus( - info=json.loads(self._info.get("Comment")) - ) - except Exception: - self._logger.warn("Fooocus format error") - # drawthings format - elif "XML:com.adobe.xmp" in self._info: - try: - data = minidom.parseString( - self._info.get("XML:com.adobe.xmp") - ) - data_json = json.loads( - data.getElementsByTagName("exif:UserComment")[0] - .childNodes[1] - .childNodes[1] - .childNodes[0] - .data - ) - except Exception: - self._logger.warn("Draw things format error") - self._status = BaseFormat.Status.FORMAT_ERROR - else: - self._tool = "Draw Things" - self._parser = DrawThings(info=data_json) - # novelai stealth pnginfo format - elif f.mode == "RGBA": - try: - reader = NovelAI.LSBExtractor(f) - read_magic = reader.get_next_n_bytes( - len(self.NOVELAI_MAGIC) - ).decode("utf-8") - assert ( - self.NOVELAI_MAGIC == read_magic - ), "NovelAI stealth png info magic number error" - except Exception as e: - self._logger.warn(e) - self._status = BaseFormat.Status.FORMAT_ERROR + exif_model_json_str = f.getexif().get(0x0110) + if exif_model_json_str: + exif_data = json.loads(exif_model_json_str) + if "sui_image_params" in exif_data: + self._tool = "StableSwarmUI" + self._parser = SwarmUI(info=exif_data) + except (TypeError, AttributeError, json.JSONDecodeError, ValueError): + pass + + # --- PNG Processing --- + if not self._parser and f.format == "PNG": + if "parameters" in self._info: + parameters_str = self._info.get("parameters", "") + if "sui_image_params" in parameters_str: + self._tool = "StableSwarmUI"; self._parser = SwarmUI(raw=parameters_str) + else: + self._tool = "A1111 webUI" if "prompt" not in self._info else "ComfyUI\n(A1111 compatible)" + self._parser = A1111(info=self._info) + elif "postprocessing" in self._info: + self._tool = "A1111 webUI\n(Postprocessing)"; self._parser = A1111(info=self._info) + elif ("negative_prompt" in self._info or "Negative Prompt" in self._info): + self._tool = "Easy Diffusion"; self._parser = EasyDiffusion(info=self._info) + elif "invokeai_metadata" in self._info: + self._tool = "InvokeAI"; self._parser = InvokeAI(info=self._info) + elif "sd-metadata" in self._info: + self._tool = "InvokeAI"; self._parser = InvokeAI(info=self._info) + elif "Dream" in self._info: # invokeai legacy dream format + self._tool = "InvokeAI"; self._parser = InvokeAI(info=self._info) + elif self._info.get("Software") == "NovelAI": + self._tool = "NovelAI"; self._parser = NovelAI(info=self._info, width=self._width, height=self._height) + elif "prompt" in self._info: # Standard ComfyUI PNG (workflow in 'prompt' key) + self._tool = "ComfyUI"; self._parser = ComfyUI(info=self._info, width=self._width, height=self._height) + elif "Comment" in self._info: # Fooocus PNG + try: + self._tool = "Fooocus"; self._parser = Fooocus(info=json.loads(self._info.get("Comment"))) + except Exception: self._logger.warn("Fooocus PNG format error") + elif "XML:com.adobe.xmp" in self._info: # DrawThings + try: + data = minidom.parseString(self._info.get("XML:com.adobe.xmp")) + data_json = json.loads(data.getElementsByTagName("exif:UserComment")[0].childNodes[1].childNodes[1].childNodes[0].data) + self._tool = "Draw Things"; self._parser = DrawThings(info=data_json) + except Exception: self._logger.warn("Draw things format error"); self._status = BaseFormat.Status.FORMAT_ERROR + elif f.mode == "RGBA": # NovelAI Stealth PNG + try: + reader = NovelAI.LSBExtractor(f) + read_magic = reader.get_next_n_bytes(len(self.NOVELAI_MAGIC)).decode("utf-8") + assert self.NOVELAI_MAGIC == read_magic, "NovelAI stealth png info magic number error" + self._tool = "NovelAI"; self._parser = NovelAI(extractor=reader) + except Exception as e: self._logger.warn(e); self._status = BaseFormat.Status.FORMAT_ERROR + + + # --- JPEG/WEBP Processing --- + elif not self._parser and f.format in ["JPEG", "WEBP"]: + raw_user_comment_from_piexif = None + exif_dict_piexif = {} + software_tag_str = "" + + exif_bytes = self._info.get("exif") + if exif_bytes: + try: + exif_dict_piexif = piexif.load(exif_bytes) + user_comment_bytes = exif_dict_piexif.get("Exif", {}).get(piexif.ExifIFD.UserComment) + if user_comment_bytes: + raw_user_comment_from_piexif = piexif.helper.UserComment.load(user_comment_bytes) + self._logger.info(f"piexif decoded UserComment (first 100): {raw_user_comment_from_piexif[:100] if raw_user_comment_from_piexif else 'None'}") + software_tag_bytes = exif_dict_piexif.get("0th", {}).get(piexif.ImageIFD.Software) + if software_tag_bytes: + software_tag_str = software_tag_bytes.decode('ascii', 'ignore').strip() + self._logger.info(f"Software Tag: {software_tag_str}") + except Exception as e_piexif: + self._logger.warn(f"piexif error processing EXIF: {e_piexif}") + + # --- START OF HYPOTHETICAL CIVITAI COMFYUI PARSER INTEGRATION --- + if raw_user_comment_from_piexif and not self._parser: + is_potential_civitai = False + # Heuristic 1: Check Civitai's known Software tag value + if software_tag_str == "4c6047c3-8b1c-4058-8888-fd48353bf47d": + is_potential_civitai = True + self._logger.info("Civitai software tag detected.") + # Heuristic 2: Content of UserComment if software tag is not definitive + elif "charset=Unicode" in raw_user_comment_from_piexif: + temp_data_after_prefix = raw_user_comment_from_piexif.split("charset=Unicode", 1)[-1].strip() + if (temp_data_after_prefix.startswith('笀∀爀攀猀漀甀爀挀攀') or \ + temp_data_after_prefix.startswith('{"resource-stack":')) and \ + '"extraMetadata":' in temp_data_after_prefix: + is_potential_civitai = True + self._logger.info("Civitai UserComment content pattern detected (mojibake or clean JSON).") + elif raw_user_comment_from_piexif.startswith('{"resource-stack":') and \ + '"extraMetadata":' in raw_user_comment_from_piexif: + is_potential_civitai = True # piexif might fully clean it + self._logger.info("Civitai UserComment clean JSON content pattern detected.") + + if is_potential_civitai: + self._logger.info("Attempting CivitaiComfyUIFormat parser.") + from .format import CivitaiComfyUIFormat # Ensure this import works in their structure + + temp_civitai_parser = CivitaiComfyUIFormat(raw=raw_user_comment_from_piexif) + temp_status = temp_civitai_parser.parse() + if temp_status == BaseFormat.Status.READ_SUCCESS: + self._tool = getattr(temp_civitai_parser, 'tool_name', "Civitai ComfyUI") # Parser should define its tool name + self._parser = temp_civitai_parser + self._logger.info(f"Successfully parsed as {self._tool}.") else: - self._tool = "NovelAI" - self._parser = NovelAI(extractor=reader) - elif f.format in ["JPEG", "WEBP"]: - # fooocus jpeg format - if "comment" in self._info: + self._logger.warn(f"CivitaiComfyUIFormat parsing failed. Error: {getattr(temp_civitai_parser, '_error', 'N/A')}. Falling back.") + # --- END OF HYPOTHETICAL CIVITAI COMFYUI PARSER INTEGRATION --- + + if not self._parser: # If Civitai parser didn't claim it or failed + # Fooocus (checks self._info.get("comment") - different from UserComment) + if "comment" in self._info and not self._parser: try: + fooocus_comment_data = json.loads(self._info.get("comment")) self._tool = "Fooocus" - self._parser = Fooocus( - info=json.loads(self._info.get("comment")) - ) - except Exception: - self._logger.warn("Fooocus format error") - self._status = BaseFormat.Status.FORMAT_ERROR - elif f.mode == "RGBA": - try: - reader = NovelAI.LSBExtractor(f) - read_magic = reader.get_next_n_bytes( - len(self.NOVELAI_MAGIC) - ).decode("utf-8") - assert ( - self.NOVELAI_MAGIC == read_magic - ), "NovelAI stealth png info magic number error" - except Exception as e: - self._logger.warn(e) - self._status = BaseFormat.Status.FORMAT_ERROR - else: - self._tool = "NovelAI" - self._parser = NovelAI(extractor=reader) - else: - try: - exif = piexif.load(self._info.get("exif")) or {} - user_comment = exif.get("Exif").get( - piexif.ExifIFD.UserComment - ) - except TypeError: - self._logger.warn("Empty jpeg") - self._status = BaseFormat.Status.FORMAT_ERROR - except Exception: - pass - else: - try: - # swarm format - if "sui_image_params" in user_comment[8:].decode( - "utf-16" - ): - self._tool = "StableSwarmUI" - self._parser = SwarmUI( - raw=user_comment[8:].decode("utf-16") - ) - else: - self._raw = piexif.helper.UserComment.load( - user_comment - ) - # easydiff jpeg and webp format - if self._raw[0] == "{": - self._tool = "Easy Diffusion" - self._parser = EasyDiffusion(raw=self._raw) - # a1111 jpeg and webp format - else: - self._tool = "A1111 webUI" - self._parser = A1111(raw=self._raw) - except Exception: - self._status = BaseFormat.Status.FORMAT_ERROR - if self._tool and self._status == BaseFormat.Status.UNREAD: - self._logger.info(f"Format: {self._tool}") + self._parser = Fooocus(info=fooocus_comment_data) + except: self._logger.warn("Fooocus (JPEG/comment) format error") + + # Standard UserComment fallbacks if still no parser and we have a UserComment + if not self._parser and raw_user_comment_from_piexif: + self._raw = raw_user_comment_from_piexif + if "sui_image_params" in raw_user_comment_from_piexif: # SwarmUI in UserComment + self._tool = "StableSwarmUI" + self._parser = SwarmUI(raw=raw_user_comment_from_piexif) + elif raw_user_comment_from_piexif.strip().startswith("{"): # Easy Diffusion JSON + self._tool = "Easy Diffusion" + self._parser = EasyDiffusion(raw=raw_user_comment_from_piexif) + else: # A1111 text block + self._tool = "A1111 webUI" + self._parser = A1111(raw=raw_user_comment_from_piexif) + + # NovelAI LSB in RGBA JPEGs/WebPs + if not self._parser and f.mode == "RGBA": + try: + reader = NovelAI.LSBExtractor(f) + read_magic = reader.get_next_n_bytes(len(self.NOVELAI_MAGIC)).decode("utf-8") + assert self.NOVELAI_MAGIC == read_magic, "NovelAI stealth LSB magic error" + self._tool = "NovelAI"; self._parser = NovelAI(extractor=reader) + except Exception as e_lsb: self._logger.warn(f"NovelAI LSB error: {e_lsb}") + + # --- FINAL PARSE CALL --- + if self._parser and self._status == BaseFormat.Status.UNREAD: + self._logger.info(f"Format determined: {self._tool if self._tool else 'Unknown'}. Parsing...") self._status = self._parser.parse() - self._logger.info(f"Reading Status: {self._status.name}") + elif not self._parser and not self._is_txt and self._status == BaseFormat.Status.UNREAD: + self._logger.warn("Could not determine image format or no parser assigned for image file.") + self._status = BaseFormat.Status.FORMAT_ERROR + + self._logger.info(f"Reading Status: {self._status.name if hasattr(self._status, 'name') else self._status}") + # --- Original staticmethods and properties --- @staticmethod def remove_data(image_file): with Image.open(image_file) as f: @@ -247,104 +247,68 @@ def save_image(image_path, new_path, image_format, data=None): "Exif": { piexif.ExifIFD.UserComment: ( piexif.helper.UserComment.dump( - data, encoding="unicode" + data, encoding="unicode" # piexif "unicode" means UTF-16LE + prefix ) ) }, } ) - with Image.open(image_path) as f: try: match image_format.upper(): case "PNG": - if data: - f.save(new_path, pnginfo=metadata) - else: - f.save(new_path) + if data: f.save(new_path, pnginfo=metadata) + else: f.save(new_path) case "JPEG" | "JPG": - f.save(new_path, quality="keep") - if data: - piexif.insert(metadata, str(new_path)) + f.save(new_path, quality="keep") # Preserves original quality + if data: piexif.insert(metadata, str(new_path)) case "WEBP": f.save(new_path, quality=100, lossless=True) - if data: - piexif.insert(metadata, str(new_path)) - except Exception: - print("Save error") + if data: piexif.insert(metadata, str(new_path)) + except Exception as e_save: + # Using f-string for better error message + Logger("SD_Prompt_Reader.ImageDataReader").error(f"Save error: {e_save}") + @staticmethod def construct_data(positive, negative, setting): - return "\n".join( - filter( - None, - [ - f"{positive}" if positive else "", - f"Negative prompt: {negative}" if negative else "", - f"{setting}" if setting else "", - ], - ) - ) + return "\n".join(filter(None, [ + f"{positive}" if positive else "", + f"Negative prompt: {negative}" if negative else "", + f"{setting}" if setting else "", + ])) def prompt_to_line(self): - return self._parser.prompt_to_line() + return self._parser.prompt_to_line() if self._parser else "" + # Properties (added hasattr for safety, good practice) @property - def height(self): - return self._parser.height if self._tool else self._height - + def height(self): return self._parser.height if self._parser and hasattr(self._parser, 'height') else self._height @property - def width(self): - return self._parser.width if self._tool else self._width - + def width(self): return self._parser.width if self._parser and hasattr(self._parser, 'width') else self._width @property - def info(self): - return self._info - + def info(self): return self._info @property - def positive(self): - return self._parser.positive if self._tool else self._positive - + def positive(self): return self._parser.positive if self._parser and hasattr(self._parser, 'positive') else self._positive @property - def negative(self): - return self._parser.negative if self._tool else self._negative - + def negative(self): return self._parser.negative if self._parser and hasattr(self._parser, 'negative') else self._negative @property - def positive_sdxl(self): - return self._parser.positive_sdxl if self._tool else self._positive_sdxl - + def positive_sdxl(self): return self._parser.positive_sdxl if self._parser and hasattr(self._parser, 'positive_sdxl') else self._positive_sdxl @property - def negative_sdxl(self): - return self._parser.negative_sdxl if self._tool else self._negative_sdxl - + def negative_sdxl(self): return self._parser.negative_sdxl if self._parser and hasattr(self._parser, 'negative_sdxl') else self._negative_sdxl @property - def setting(self): - return self._parser.setting if self._tool else self._setting - + def setting(self): return self._parser.setting if self._parser and hasattr(self._parser, 'setting') else self._setting @property - def raw(self): - return self._parser.raw or self._raw - + def raw(self): return self._parser.raw if self._parser and hasattr(self._parser, 'raw') else self._raw @property - def tool(self): - return self._tool - + def tool(self): return self._parser.tool if self._parser and hasattr(self._parser, 'tool') else self._tool # Let parser define tool name @property - def parameter(self): - return self._parser.parameter if self._tool else self._parameter - + def parameter(self): return self._parser.parameter if self._parser and hasattr(self._parser, 'parameter') else self._parameter @property - def format(self): - return self._format - + def format(self): return self._format @property - def is_sdxl(self): - return self._parser.is_sdxl if self._tool else self._is_sdxl - + def is_sdxl(self): return self._parser.is_sdxl if self._parser and hasattr(self._parser, 'is_sdxl') else self._is_sdxl @property - def props(self): - return self._parser.props if self._tool else self._props - + def props(self): return self._parser.props if self._parser and hasattr(self._parser, 'props') else self._props @property - def status(self): - return self._status + def status(self): return self._status