Skip to content

Commit b6d2367

Browse files
committed
feat: add jpg and webp support, add exif data handling for metadata (lllyasviel#1863)
* feature: added flag, config and ui update for image extension change lllyasviel#1789 * moved function to config module * moved image extension to webui via async worker. Passing as parameter to log and get_current_html_path functions per feedback * check flag before displaying image extension radio button * disabled if image log flag is passed in * fix: add missing image_extension parameter to log call * refactor: change label * feat: add webp to image_extensions supported image extemsions: see https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html * feat: use consistent file name in gradio returns and uses filepaths instead of numpy image by saving to temp dir uses double the temp dir file storage on disk as it saves to temp dir and gradio temp dir when displaying the image, but reuses logged output image * feat: delete temp images after yielding to gradio * feat: use args temp path if given * chore: code cleanup, remove redundant if statement * feat: always show image_extension element this is now possible due to image extension support in gradio via lllyasviel#1932 * refactor: rename image_extension to image_file_extension * feat: use optimized jpg parameters when saving the image quality=95 optimize=True progressive=True * refactor: rename image_file_extension to output_format * feat: add exif handling * refactor: code cleanup, remove items from metadata output --------- Co-authored-by: Manuel Schmid <dev@mash1t.de> Co-authored-by: Manuel Schmid <9307310+mashb1t@users.noreply.github.com> Co-authored-by: Manuel Schmid <manuel.schmid@odt.net> Co-authored by: eddyizm <wtfisup@hotmail.com>
1 parent ba9eadb commit b6d2367

File tree

6 files changed

+104
-28
lines changed

6 files changed

+104
-28
lines changed

modules/async_worker.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import threading
2-
import os
32
from modules.patch import PatchSettings, patch_settings, patch_all
43

54
patch_all()
@@ -142,6 +141,7 @@ def handler(async_task):
142141
performance_selection = Performance(args.pop())
143142
aspect_ratios_selection = args.pop()
144143
image_number = args.pop()
144+
output_format = args.pop()
145145
image_seed = args.pop()
146146
sharpness = args.pop()
147147
guidance_scale = args.pop()
@@ -414,6 +414,7 @@ def handler(async_task):
414414

415415
progressbar(async_task, 3, 'Processing prompts ...')
416416
tasks = []
417+
417418
for i in range(image_number):
418419
if disable_seed_increment:
419420
task_seed = seed
@@ -553,7 +554,7 @@ def handler(async_task):
553554

554555
if direct_return:
555556
d = [('Upscale (Fast)', '2x')]
556-
uov_input_image_path = log(uov_input_image, d)
557+
uov_input_image_path = log(uov_input_image, d, output_format)
557558
yield_result(async_task, uov_input_image_path, do_not_show_finished_images=True)
558559
return
559560

@@ -863,7 +864,7 @@ def callback(step, x0, x, total_steps, y):
863864
d.append((f'LoRA {li + 1}', f'lora_combined_{li + 1}', f'{n} : {w}'))
864865

865866
d.append(('Version', 'version', 'Fooocus v' + fooocus_version.version))
866-
img_paths.append(log(x, d, metadata_parser))
867+
img_paths.append(log(x, d, metadata_parser, output_format))
867868

868869
yield_result(async_task, img_paths, do_not_show_finished_images=len(tasks) == 1 or disable_intermediate_results)
869870
except ldm_patched.modules.model_management.InterruptProcessingException as e:

modules/config.py

+5
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,11 @@ def get_config_item_or_set_default(key, default_value, validator, disable_empty_
306306
default_value=32,
307307
validator=lambda x: isinstance(x, int) and x >= 1
308308
)
309+
default_output_format = get_config_item_or_set_default(
310+
key='default_output_format',
311+
default_value='png',
312+
validator=lambda x: x in modules.flags.output_formats
313+
)
309314
default_image_number = get_config_item_or_set_default(
310315
key='default_image_number',
311316
default_value=2,

modules/flags.py

+2
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@
6767
cn_ip: (0.5, 0.6), cn_ip_face: (0.9, 0.75), cn_canny: (0.5, 1.0), cn_cpds: (0.5, 1.0)
6868
} # stop, weight
6969

70+
output_formats = ['png', 'jpg', 'webp']
71+
7072
inpaint_engine_versions = ['None', 'v1', 'v2.5', 'v2.6']
7173
inpaint_option_default = 'Inpaint or Outpaint (default)'
7274
inpaint_option_detail = 'Improve Detail (face, hand, eyes, etc.)'

modules/meta_parser.py

+61-5
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import gradio as gr
88
from PIL import Image
99

10+
import fooocus_version
1011
import modules.config
1112
import modules.sdxl_styles
1213
from modules.flags import MetadataScheme, Performance, Steps
@@ -181,13 +182,43 @@ def get_lora(key: str, fallback: str | None, source_dict: dict, results: list):
181182

182183
def get_sha256(filepath):
183184
global hash_cache
184-
185185
if filepath not in hash_cache:
186186
hash_cache[filepath] = calculate_sha256(filepath)
187187

188188
return hash_cache[filepath]
189189

190190

191+
def parse_meta_from_preset(preset_content):
192+
assert isinstance(preset_content, dict)
193+
preset_prepared = {}
194+
items = preset_content
195+
196+
for settings_key, meta_key in modules.config.possible_preset_keys.items():
197+
if settings_key == "default_loras":
198+
loras = getattr(modules.config, settings_key)
199+
if settings_key in items:
200+
loras = items[settings_key]
201+
for index, lora in enumerate(loras[:5]):
202+
preset_prepared[f'lora_combined_{index + 1}'] = ' : '.join(map(str, lora))
203+
elif settings_key == "default_aspect_ratio":
204+
if settings_key in items and items[settings_key] is not None:
205+
default_aspect_ratio = items[settings_key]
206+
width, height = default_aspect_ratio.split('*')
207+
else:
208+
default_aspect_ratio = getattr(modules.config, settings_key)
209+
width, height = default_aspect_ratio.split('×')
210+
height = height[:height.index(" ")]
211+
preset_prepared[meta_key] = (width, height)
212+
else:
213+
preset_prepared[meta_key] = items[settings_key] if settings_key in items and items[
214+
settings_key] is not None else getattr(modules.config, settings_key)
215+
216+
if settings_key == "default_styles" or settings_key == "default_aspect_ratio":
217+
preset_prepared[meta_key] = str(preset_prepared[meta_key])
218+
219+
return preset_prepared
220+
221+
191222
class MetadataParser(ABC):
192223
def __init__(self):
193224
self.raw_prompt: str = ''
@@ -213,7 +244,8 @@ def parse_json(self, metadata: dict | str) -> dict:
213244
def parse_string(self, metadata: dict) -> str:
214245
raise NotImplementedError
215246

216-
def set_data(self, raw_prompt, full_prompt, raw_negative_prompt, full_negative_prompt, steps, base_model_name, refiner_model_name, loras):
247+
def set_data(self, raw_prompt, full_prompt, raw_negative_prompt, full_negative_prompt, steps, base_model_name,
248+
refiner_model_name, loras):
217249
self.raw_prompt = raw_prompt
218250
self.full_prompt = full_prompt
219251
self.raw_negative_prompt = raw_negative_prompt
@@ -492,16 +524,28 @@ def get_metadata_parser(metadata_scheme: MetadataScheme) -> MetadataParser:
492524
raise NotImplementedError
493525

494526

495-
def read_info_from_image(filepath) -> tuple[str | None, dict, MetadataScheme | None]:
527+
def read_info_from_image(filepath) -> tuple[str | None, MetadataScheme | None]:
496528
with Image.open(filepath) as image:
497529
items = (image.info or {}).copy()
498530

499531
parameters = items.pop('parameters', None)
532+
metadata_scheme = items.pop('fooocus_scheme', None)
533+
exif = items.pop('exif', None)
534+
500535
if parameters is not None and is_json(parameters):
501536
parameters = json.loads(parameters)
537+
elif exif is not None:
538+
exif = image.getexif()
539+
# 0x9286 = UserComment
540+
parameters = exif.get(0x9286, None)
541+
# 0x927C = MakerNote
542+
metadata_scheme = exif.get(0x927C, None)
543+
544+
if is_json(parameters):
545+
parameters = json.loads(parameters)
502546

503547
try:
504-
metadata_scheme = MetadataScheme(items.pop('fooocus_scheme', None))
548+
metadata_scheme = MetadataScheme(metadata_scheme)
505549
except ValueError:
506550
metadata_scheme = None
507551

@@ -512,4 +556,16 @@ def read_info_from_image(filepath) -> tuple[str | None, dict, MetadataScheme | N
512556
if isinstance(parameters, str):
513557
metadata_scheme = MetadataScheme.A1111
514558

515-
return parameters, items, metadata_scheme
559+
return parameters, metadata_scheme
560+
561+
562+
def get_exif(metadata: str | None, metadata_scheme: str):
563+
exif = Image.Exif()
564+
# tags see see https://github.com/python-pillow/Pillow/blob/9.2.x/src/PIL/ExifTags.py
565+
# 0x9286 = UserComment
566+
exif[0x9286] = metadata
567+
# 0x0131 = Software
568+
exif[0x0131] = 'Fooocus v' + fooocus_version.version
569+
# 0x927C = MakerNote
570+
exif[0x927C] = metadata_scheme
571+
return exif

modules/private_logger.py

+20-12
Original file line numberDiff line numberDiff line change
@@ -7,34 +7,42 @@
77
from PIL import Image
88
from PIL.PngImagePlugin import PngInfo
99
from modules.util import generate_temp_filename
10-
from modules.meta_parser import MetadataParser
11-
from tempfile import gettempdir
10+
from modules.meta_parser import MetadataParser, get_exif
1211

1312
log_cache = {}
1413

1514

16-
def get_current_html_path():
15+
def get_current_html_path(output_format=None):
16+
output_format = output_format if output_format else modules.config.default_output_format
1717
date_string, local_temp_filename, only_name = generate_temp_filename(folder=modules.config.path_outputs,
18-
extension='png')
18+
extension=output_format)
1919
html_name = os.path.join(os.path.dirname(local_temp_filename), 'log.html')
2020
return html_name
2121

2222

23-
def log(img, metadata, metadata_parser: MetadataParser | None = None) -> str:
23+
def log(img, metadata, metadata_parser: MetadataParser | None = None, output_format=None) -> str:
2424
path_outputs = args_manager.args.temp_path if args_manager.args.disable_image_log else modules.config.path_outputs
25-
date_string, local_temp_filename, only_name = generate_temp_filename(folder=path_outputs, extension='png')
25+
output_format = output_format if output_format else modules.config.default_output_format
26+
date_string, local_temp_filename, only_name = generate_temp_filename(folder=path_outputs, extension=output_format)
2627
os.makedirs(os.path.dirname(local_temp_filename), exist_ok=True)
2728

2829
parsed_parameters = metadata_parser.parse_string(metadata) if metadata_parser is not None else ''
2930
image = Image.fromarray(img)
3031

31-
if parsed_parameters != '':
32-
pnginfo = PngInfo()
33-
pnginfo.add_text('parameters', parsed_parameters)
34-
pnginfo.add_text('fooocus_scheme', metadata_parser.get_scheme().value)
32+
if output_format == 'png':
33+
if parsed_parameters != '':
34+
pnginfo = PngInfo()
35+
pnginfo.add_text('parameters', parsed_parameters)
36+
pnginfo.add_text('fooocus_scheme', metadata_parser.get_scheme().value)
37+
else:
38+
pnginfo = None
39+
image.save(local_temp_filename, pnginfo=pnginfo)
40+
elif output_format == 'jpg':
41+
image.save(local_temp_filename, quality=95, optimize=True, progressive=True, exif=get_exif(parsed_parameters, metadata_parser.get_scheme().value) if metadata_parser else Image.Exif())
42+
elif output_format == 'webp':
43+
image.save(local_temp_filename, quality=95, lossless=False, exif=get_exif(parsed_parameters, metadata_parser.get_scheme().value) if metadata_parser else Image.Exif())
3544
else:
36-
pnginfo = None
37-
image.save(local_temp_filename, pnginfo=pnginfo)
45+
image.save(local_temp_filename)
3846

3947
if args_manager.args.disable_image_log:
4048
return local_temp_filename

webui.py

+12-8
Original file line numberDiff line numberDiff line change
@@ -224,15 +224,12 @@ def ip_advance_checked(x):
224224
metadata_import_button = gr.Button(value='Apply Metadata')
225225

226226
def trigger_metadata_preview(filepath):
227-
parameters, items, metadata_scheme = modules.meta_parser.read_info_from_image(filepath)
227+
parameters, metadata_scheme = modules.meta_parser.read_info_from_image(filepath)
228228

229229
results = {}
230230
if parameters is not None:
231231
results['parameters'] = parameters
232232

233-
if items:
234-
results['items'] = items
235-
236233
if isinstance(metadata_scheme, flags.MetadataScheme):
237234
results['metadata_scheme'] = metadata_scheme.value
238235

@@ -263,6 +260,11 @@ def trigger_metadata_preview(filepath):
263260
value=modules.config.default_aspect_ratio, info='width × height',
264261
elem_classes='aspect_ratios')
265262
image_number = gr.Slider(label='Image Number', minimum=1, maximum=modules.config.default_max_image_number, step=1, value=modules.config.default_image_number)
263+
264+
output_format = gr.Radio(label='Output Format',
265+
choices=modules.flags.output_formats,
266+
value=modules.config.default_output_format)
267+
266268
negative_prompt = gr.Textbox(label='Negative Prompt', show_label=True, placeholder="Type prompt here.",
267269
info='Describing what you do not want to see.', lines=2,
268270
elem_id='negative_prompt',
@@ -292,7 +294,7 @@ def update_history_link():
292294
if args_manager.args.disable_image_log:
293295
return gr.update(value='')
294296

295-
return gr.update(value=f'<a href="file={get_current_html_path()}" target="_blank">\U0001F4DA History Log</a>')
297+
return gr.update(value=f'<a href="file={get_current_html_path(output_format)}" target="_blank">\U0001F4DA History Log</a>')
296298

297299
history_link = gr.HTML()
298300
shared.gradio_root.load(update_history_link, outputs=history_link, queue=False, show_progress=False)
@@ -532,7 +534,9 @@ def model_refresh_clicked():
532534
adm_scaler_negative, refiner_switch, refiner_model, sampler_name,
533535
scheduler_name, adaptive_cfg, refiner_swap_method, negative_prompt, disable_intermediate_results
534536
], queue=False, show_progress=False)
535-
537+
538+
output_format.input(lambda x: gr.update(output_format=x), inputs=output_format)
539+
536540
advanced_checkbox.change(lambda x: gr.update(visible=x), advanced_checkbox, advanced_column,
537541
queue=False, show_progress=False) \
538542
.then(fn=lambda: None, _js='refresh_grid_delayed', queue=False, show_progress=False)
@@ -573,7 +577,7 @@ def inpaint_mode_change(mode):
573577
ctrls = [currentTask, generate_image_grid]
574578
ctrls += [
575579
prompt, negative_prompt, style_selections,
576-
performance_selection, aspect_ratios_selection, image_number, image_seed, sharpness, guidance_scale
580+
performance_selection, aspect_ratios_selection, image_number, output_format, image_seed, sharpness, guidance_scale
577581
]
578582

579583
ctrls += [base_model, refiner_model, refiner_switch] + lora_ctrls
@@ -622,7 +626,7 @@ def parse_meta(raw_prompt_txt, is_generating):
622626
load_parameter_button.click(modules.meta_parser.load_parameter_button_click, inputs=[prompt, state_is_generating], outputs=load_data_outputs, queue=False, show_progress=False)
623627

624628
def trigger_metadata_import(filepath, state_is_generating):
625-
parameters, items, metadata_scheme = modules.meta_parser.read_info_from_image(filepath)
629+
parameters, metadata_scheme = modules.meta_parser.read_info_from_image(filepath)
626630
if parameters is None:
627631
print('Could not find metadata in the image!')
628632
parsed_parameters = {}

0 commit comments

Comments
 (0)