Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Display improvements to PNG Info tab #16415

Open
wants to merge 20 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 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
56 changes: 51 additions & 5 deletions modules/extras.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,73 @@
import re
import shutil
import json

import html

import torch
import tqdm

from modules import shared, images, sd_models, sd_vae, sd_models_config, errors
from modules import shared, images, sd_models, sd_vae, sd_models_config, errors, png_parser
from modules.ui_common import plaintext_to_html
import gradio as gr
import safetensors.torch

def pnginfo_format_string(plain_text):
content = "<br>\n".join(html.escape(x) for x in str(plain_text).split('\n'))
return content

def pnginfo_format_setting(name, value):
cls_name = 'geninfo-setting-string' if value.startswith('"') else 'geninfo-setting-value'
return f"<span class='geninfo-setting-name'>{html.escape(name)}:</span> <span class='{cls_name}' onclick='uiCopyElementText(this)'>{html.escape(value)}</span>"

def pnginfo_format_quicklink(name):
return f"<span class='geninfo-quick-link' onclick='uiCopyPngInfo(this, \"{name}\")'>[{html.escape(name)}]</span>"

def run_pnginfo(image):
if image is None:
return '', '', ''

geninfo, items = images.read_info_from_image(image)
Copy link
Collaborator

@w-e-w w-e-w Oct 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another issue: items is discarded if parser.valid

these are contains extra information that might be added on by stuff such as post-processing on extras tab
image

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@w-e-w Thank you for taking the time to go over this. I will take the time to look over the changes and test. It will take a few days. Thank you again!

items = {**{'parameters': geninfo}, **items}

info = ''
for key, text in items.items():
info += f"""
parser = png_parser.PngParser(geninfo)
if parser.valid:
info += f"""
<div class='pnginfo-page'>
<p><b>parameters</b><br>
{pnginfo_format_quicklink("Copy")}&nbsp;{pnginfo_format_quicklink("Prompt")}"""
if parser.negative is not None:
info += f'&nbsp;{pnginfo_format_quicklink("Negative")}'
info += f"""&nbsp;{pnginfo_format_quicklink("Settings")}
</p>
<p id='pnginfo-positive'>{pnginfo_format_string(parser.positive)}</p>"""
if parser.negative is not None:
info += f"""
<p>
<span class='geninfo-setting-name'>Negative prompt:</span><br><span id='pnginfo-negative'>{pnginfo_format_string(parser.negative)}</span>
</p>
"""
if parser.settings is None:
info += f"{plaintext_to_html(str(parser.parameters))}"
else:
info += "<p id='pnginfo-settings'>"
first = True
for setting in parser.settings:
if first:
first = False
else:
info += ", "
info += pnginfo_format_setting(str(setting[0]), str(setting[1])+str(setting[2]))
info += "</p>"

if parser.extra is not None:
info += f"<p>{pnginfo_format_string(parser.extra)}</p>"

info += "</div>\n"
else:
items = {**{'parameters': geninfo}, **items}

for key, text in items.items():
info += f"""
<div class="infotext">
<p><b>{plaintext_to_html(str(key))}</b></p>
<p>{plaintext_to_html(str(text))}</p>
Expand Down
57 changes: 57 additions & 0 deletions modules/png_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import re

class PngParser:
re_top_level = None
re_top_level2 = None
re_extra_newline = None
re_parameters = None

def __init__(self, pnginfo_string):
PngParser.init_re()

self.valid = self.parse_pnginfo(pnginfo_string)

def parse_pnginfo(self, pnginfo_string):
try:
# separate positive, negative, and parameters
m = PngParser.re_top_level.search(pnginfo_string)
if m is None:
m = PngParser.re_top_level2.search(pnginfo_string)
if m is None:
return False
else:
self.positive = m.group(1)
self.negative = None
self.parameters = m.group(2)
else:
self.positive = m.group(1)
self.negative = m.group(2)
self.parameters = m.group(3)

self.extra = None
self.settings = None

# parse extra parameters (if they exist) by a newline outside of quotes
m = PngParser.re_extra_newline.search(self.parameters)
if m is not None:
s = m.span()
self.extra = self.parameters[s[1]:]
self.parameters = self.parameters[:s[0]]

# parse standard parameters
self.settings = PngParser.re_parameters.findall(self.parameters)
if self.settings is None:
return False
except Exception:
return False

return True

@classmethod
def init_re(cls):
if cls.re_top_level is None:
cls.re_top_level = re.compile(r'^(?P<positive>(?:.|\n)*)\nNegative prompt: (?P<negative>(?:.|\n)*)\n(?=Steps: )(?P<parameters>(?:.|\n)*)$')
cls.re_top_level2 = re.compile(r'^(?P<positive>(?:.|\n)*)\nSteps: (?P<parameters>(?:.|\n)*)$')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I broke the parser
let me see if I can fix it

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. when no Negative prompt, Stpes is not in the parameters group and so is not displayed and it's not copied when using copy buttons
  2. the regex pattern is using a dangerous assumption that Steps must exist and be is the first listed parameter

# cls.re_top_level2 = re.compile(r'^(?P<positive>(?:.|\n)*)\n(?=Steps: )(?P<parameters>(?:.|\n)*)$')
cls.re_extra_newline = re.compile(r'\n(?=(?:[^"]*"[^"]*")*[^"]*$)')
cls.re_parameters = re.compile(r'\s*(?P<param>[^:,]+):\s*(?P<quote>")?(?P<value>(?(2)(?:.)*?(?:(?<!\\)")|.*?))(?:\s*,\s*|$)')
63 changes: 63 additions & 0 deletions script.js
Original file line number Diff line number Diff line change
Expand Up @@ -212,3 +212,66 @@ function uiElementInSight(el) {

return isOnScreen;
}

function uiCopyElementAnimate(el) {
el.classList.remove('animate');
setTimeout(() => {
el.classList.add('animate');
}, 0);
setTimeout(() => {
el.classList.remove('animate');
}, 1100);
}

function uiCopyElementText(el) {
var text = el.innerText;
if (text.startsWith('"')) {
text = text.substring(1, text.length - 1).replaceAll('\\n', '\n');
}

navigator.clipboard.writeText(text);
uiCopyElementAnimate(el);
}

function uiCopyRawText(elid) {
var el = document.getElementById(elid);
if (el == null) {
return null;
}

return el.innerText;
}

function uiCopyPngInfo(el, mode) {
var text = null;

if (mode == "Prompt") {
text = uiCopyRawText("pnginfo-positive");
} else if (mode == "Negative") {
text = uiCopyRawText("pnginfo-negative");
} else if (mode == "Settings") {
text = uiCopyRawText("pnginfo-settings");
} else if (mode == "Copy") {
text = "";
var t2 = uiCopyRawText("pnginfo-positive");
if (t2 != null) {
text += t2;
}
t2 = uiCopyRawText("pnginfo-negative");
if (t2 != null) {
text += "\nNegative prompt: " + t2;
}
t2 = uiCopyRawText("pnginfo-settings");
if (t2 != null) {
text += "\n" + t2;
}
if (text == "") {
text = null;
}
}

if (text != null) {
navigator.clipboard.writeText(text);
uiCopyElementAnimate(el);
}
}
94 changes: 94 additions & 0 deletions style.css
Original file line number Diff line number Diff line change
Expand Up @@ -1665,3 +1665,97 @@ body.resizing .resize-handle {
visibility: visible;
width: auto;
}

/* PngInfo colors */
:root {
--pnginfo-value-color:var(--secondary-600);
--pnginfo-string-color:var(--primary-600);
--pnginfo-value-hover:var(--secondary-100);
--pnginfo-string-hover:var(--primary-100);
--pnginfo-copy-color:#22c922;
--pnginfo-copy-background:#a9cfa9;
}

.dark {
--pnginfo-value-color:var(--secondary-400);
--pnginfo-string-color:var(--primary-400);
--pnginfo-value-hover:var(--secondary-700);
--pnginfo-string-hover:var(--primary-700);
--pnginfo-copy-color:#a9cfa9;
--pnginfo-copy-background:#22c922;
}

.pnginfo-page {
overflow-wrap: break-word;
}

/* PngInfo styles */
.pnginfo-page p span.geninfo-setting-name {
font-weight: var(--weight-semibold);
}

.pnginfo-page p span.geninfo-setting-value {
color: var(--pnginfo-value-color);
cursor: pointer;
}

.pnginfo-page p span.geninfo-setting-value:hover {
background-color: var(--pnginfo-value-hover);
}

.pnginfo-page p span.geninfo-setting-string {
color: var(--pnginfo-string-color);
font-style: italic;
cursor: pointer;
}

.pnginfo-page p span.geninfo-setting-string:hover {
background-color: var(--pnginfo-string-hover);
}

.pnginfo-page p span.geninfo-quick-link {
color: var(--pnginfo-string-color);
cursor: pointer;
}

.pnginfo-page p span.geninfo-quick-link:hover {
background-color: var(--pnginfo-string-hover);
}

/* PngInfo animations */
@keyframes copyAnimationSettingValue {
0% {
color: var(--pnginfo-copy-color);
background-color: var(--pnginfo-copy-background);
}
100% {
color: var(--pnginfo-value-color);
background-color: unset;
}
}

span.geninfo-setting-value.animate {
-webkit-animation: copyAnimationSettingValue 1s 1;
animation: copyAnimationSettingValue 1s 1;
}

@keyframes copyAnimationSettingString {
0% {
color: var(--pnginfo-copy-color);
background-color: var(--pnginfo-copy-background);
}
100% {
color: var(--pnginfo-string-color);
background-color: unset;
}
}

span.geninfo-setting-string.animate {
-webkit-animation: copyAnimationSettingString 1s 1;
animation: copyAnimationSettingString 1s 1;
}

span.geninfo-quick-link.animate {
-webkit-animation: copyAnimationSettingString 1s 1;
animation: copyAnimationSettingString 1s 1;
}