Compare commits

...

66 Commits
cf ... v2.5.41a

Author SHA1 Message Date
324226f87d Merge pull request #1379 from easydiffusion/yaml-legacy-path
Handle the legacy yaml config path
2023-06-30 16:31:16 +05:30
3120b593c6 Handle the legacy yaml config path 2023-06-30 16:30:29 +05:30
d98e4772ac Merge pull request #1378 from easydiffusion/yaml-to-json
Allow main to switch back from yaml to json config files
2023-06-30 15:55:02 +05:30
cf87c34bef Allow main to switch back from yaml to json config files 2023-06-30 15:53:54 +05:30
656acafed3 Don't read config.yaml just yet in the main branch 2023-06-30 09:52:10 +05:30
5bc0d1f762 Merge pull request #1366 from easydiffusion/beta
Fix broken save settings
2023-06-26 15:35:34 +05:30
881fdc58ec debug logging 2023-06-26 15:34:25 +05:30
569431dc72 Merge pull request #1357 from JeLuF/savesettings
Fix saving of network settings
2023-06-26 15:22:35 +05:30
07e30ae4ad Merge pull request #1365 from easydiffusion/beta
Beta
2023-06-26 15:05:40 +05:30
c74be07c33 sdkit 1.0.112 - fix broken inpainting in low vram mode 2023-06-24 15:46:03 +05:30
887d871d26 changelog 2023-06-24 15:22:09 +05:30
4dd1a46efa sdkit 1.0.111 - don't apply a negative lora when testing a newly loaded SD model 2023-06-24 15:21:13 +05:30
eb301a67d4 changelog 2023-06-23 21:43:36 +05:30
d9bddffc42 sdkit 1.0.110 - don't offload latent upscaler to the CPU if not running on a GPU 2023-06-23 21:42:11 +05:30
a43bd2fd3b changelog 2023-06-20 10:50:28 +05:30
aac9acf068 sdkit 1.0.109 - auto-set fp32 attention precision in diffusers if required 2023-06-20 10:49:34 +05:30
65bb01892f remove old code 2023-06-19 21:58:58 +02:00
5b35c47360 Fix saving of network settings 2023-06-19 21:50:56 +02:00
4bf78521ce changelog 2023-06-19 19:58:59 +05:30
2a5b3040e2 sdkit 1.0.108 - potential fix for multi-gpu bug while rendering - the sampler instances weren't thread-local 2023-06-19 19:58:17 +05:30
2c4cd21c8f sdkit 1.0.107 - fix a bug where low VRAM usage mode wasn't working with multiple GPUs 2023-06-16 16:46:32 +05:30
8ced5b7199 Merge pull request #1344 from easydiffusion/beta
Beta
2023-06-13 17:08:46 +05:30
41d8847592 changelog 2023-06-13 13:39:58 +05:30
eb96bfe8a4 sdkit 1.0.106 - fix errors with multi-gpu in low vram mode 2023-06-13 13:39:23 +05:30
3037cceab3 Merge branch 'beta' of github.com:cmdr2/stable-diffusion-ui into beta 2023-06-12 17:22:29 +05:30
324ffdefba changelog 2023-06-12 16:58:11 +05:30
9a81d17d33 Fix for multi-gpu bug in codeformer 2023-06-12 16:57:36 +05:30
0ba9f0549e Merge pull request #1341 from JeLuF/beta
Set PYTHONNOUSERSITE=y in dev console
2023-06-12 14:48:00 +05:30
f83af28e42 Set PYTHONNOUSERSITE=y in dev console
Make behaviour consistent with on_env_start.sh
2023-06-11 21:12:22 +02:00
a2856b2b77 Update README.md 2023-06-08 16:47:50 +05:30
924fee394a A better way to make gfpgan show up at the top 2023-06-08 16:09:11 +05:30
e349fb1a23 fix 2023-06-08 15:55:36 +05:30
4f799a2bf0 Use gfpgan as the default model for face restoration 2023-06-08 15:52:41 +05:30
5398765fd7 Tighten the image editor (to reduce unnecessary empty space and reduce mouse travel) - Thanks @fdwr - #1307 2023-06-08 15:21:16 +05:30
48edce72a9 Log the version numbers of only a few important modules 2023-06-07 16:38:15 +05:30
267c7b85ea Use only realesrgan_x4 (not anime) for upscaling in codeformer 2023-06-07 16:37:44 +05:30
e23f66a697 Fix #1333 - listen_port isn't always present in the config file 2023-06-07 15:45:21 +05:30
9a0031c47b Don't copy check_models.py, it doesn't exist anymore 2023-06-07 15:21:16 +05:30
0d8e73b206 sdkit 1.0.104 - Not all pipelines have vae slicing 2023-06-07 15:10:57 +05:30
9486c03a89 Don't use the default SD model (if the desired model was not found), unless the UI is starting up 2023-06-06 17:10:38 +05:30
c09512bf12 dead code 2023-06-06 16:57:25 +05:30
05c2de9450 Fail with an error if the desired model (non-Stable Diffusion) wasn't found 2023-06-06 16:56:37 +05:30
6ae5cb28cf Set the default codeformer strength to 0.5 2023-06-06 16:37:17 +05:30
cf6c1add1d Merge branch 'beta' of github.com:cmdr2/stable-diffusion-ui into beta 2023-06-06 16:24:42 +05:30
d0184a1598 Allow changing the strength of the codeformer model (1 - fidelity); Improve the styling of the sub-settings 2023-06-06 16:16:21 +05:30
79d6ab9915 Update CHANGES.md 2023-06-06 15:22:27 +05:30
047390873c changelog; show labels next to the lora strength slider 2023-06-05 16:53:18 +05:30
4b36ca75cb Merge pull request #1313 from JeLuF/cloudflared
Share ED via Cloudflare's ArgoTunnel
2023-06-05 16:20:40 +05:30
f7c52b700e Merge pull request #1328 from ogmaresca/negative-lora-strength
Allow LoRA strengths between -2 and 2
2023-06-05 16:18:28 +05:30
c81d98ad0f Merge pull request #1325 from JeLuF/tildl
Tiled image download plugin
2023-06-05 16:17:23 +05:30
046c00d844 changelog 2023-06-05 16:13:41 +05:30
b14653cb9e sdkit 1.0.103 - Pin the versions of diffusers models used; Use cpu offloading for balanced and low while upscaling using latent upscaler 2023-06-05 16:11:48 +05:30
c72b287c82 Show a more helpful error message in the logs when the system runs out of RAM 2023-06-05 15:22:37 +05:30
a10aa92634 Fix a bug where the realesrgan model would get unloaded after the first request in a batch while using Codeformer with upscaling of faces 2023-06-05 15:08:57 +05:30
8a2c09c6de Fix for rabbit hole plugin 2023-06-05 09:00:50 +05:30
401fc30617 Allow LoRA strengths between -2 and 2 2023-06-03 14:54:17 -04:00
6ca7247c02 Enable face upscaling by default 2023-06-03 10:11:03 +05:30
1d5309decb changelog 2023-06-03 10:04:06 +05:30
ab0218050c Merge pull request #1322 from cmdr2/cf
CodeFormer
2023-06-03 09:55:21 +05:30
6dcf7539bb close window 2023-06-03 00:04:13 +02:00
51d52d3a07 Tiled image download plugin 2023-06-02 23:41:53 +02:00
3045f5211f Merge pull request #1321 from cmdr2/beta
Tiling and other bug fixes
2023-06-01 16:53:51 +05:30
9ce076eb0d Copy address button 2023-05-28 01:18:39 +02:00
2080d6e27b Share ED via Cloudflare's ArgoTunnel
Shares the Easy Diffusion instance via https://try.cloudflare.com/
2023-05-28 00:50:23 +02:00
41ecc822df Merge pull request #1305 from JeLuF/patch-27
Update "How to install and run.txt"
2023-05-26 15:25:31 +05:30
ce2a42ca13 Update "How to install and run.txt" 2023-05-25 20:18:19 +02:00
25 changed files with 693 additions and 88 deletions

View File

@ -22,6 +22,20 @@
Our focus continues to remain on an easy installation experience, and an easy user-interface. While still remaining pretty powerful, in terms of features and speed.
### Detailed changelog
* 2.5.41 - 24 Jun 2023 - (beta-only) Fix broken inpainting in low VRAM usage mode.
* 2.5.41 - 24 Jun 2023 - (beta-only) Fix a recent regression where the LoRA would not get applied when changing SD models.
* 2.5.41 - 23 Jun 2023 - Fix a regression where latent upscaler stopped working on PCs without a graphics card.
* 2.5.41 - 20 Jun 2023 - Automatically fix black images if fp32 attention precision is required in diffusers.
* 2.5.41 - 19 Jun 2023 - Another fix for multi-gpu rendering (in all VRAM usage modes).
* 2.5.41 - 13 Jun 2023 - Fix multi-gpu bug with "low" VRAM usage mode while generating images.
* 2.5.41 - 12 Jun 2023 - Fix multi-gpu bug with CodeFormer.
* 2.5.41 - 6 Jun 2023 - Allow changing the strength of CodeFormer, and slightly improved styling of the CodeFormer options.
* 2.5.41 - 5 Jun 2023 - Allow sharing an Easy Diffusion instance via https://try.cloudflare.com/ . You can find this option at the bottom of the Settings tab. Thanks @JeLuf.
* 2.5.41 - 5 Jun 2023 - Show an option to download for tiled images. Shows a button on the generated image. Creates larger images by tiling them with the image generated by Easy Diffusion. Thanks @JeLuf.
* 2.5.41 - 5 Jun 2023 - (beta-only) Allow LoRA strengths between -2 and 2. Thanks @ogmaresca.
* 2.5.40 - 5 Jun 2023 - Reduce the VRAM usage of Latent Upscaling when using "balanced" VRAM usage mode.
* 2.5.40 - 5 Jun 2023 - Fix the "realesrgan" key error when using CodeFormer with more than 1 image in a batch.
* 2.5.40 - 3 Jun 2023 - Added CodeFormer as another option for fixing faces and eyes. CodeFormer tends to perform better than GFPGAN for many images. Thanks @patriceac for the implementation, and for contacting the CodeFormer team (who were supportive of it being integrated into Easy Diffusion).
* 2.5.39 - 25 May 2023 - (beta-only) Seamless Tiling - make seamlessly tiled images, e.g. rock and grass textures. Thanks @JeLuf.
* 2.5.38 - 24 May 2023 - Better reporting of errors, and show an explanation if the user cannot disable the "Use CPU" setting.
* 2.5.38 - 23 May 2023 - Add Latent Upscaler as another option for upscaling images. Thanks @JeLuf for the implementation of the Latent Upscaler model.

View File

@ -5,10 +5,10 @@ If you haven't downloaded Stable Diffusion UI yet, please download from https://
After downloading, to install please follow these instructions:
For Windows:
- Please double-click the "Start Stable Diffusion UI.cmd" file inside the "stable-diffusion-ui" folder.
- Please double-click the "Easy-Diffusion-Windows.exe" file and follow the instructions.
For Linux:
- Please open a terminal, and go to the "stable-diffusion-ui" directory. Then run ./start.sh
- Please open a terminal, unzip the Easy-Diffusion-Linux.zip file and go to the "easy-diffusion" directory. Then run ./start.sh
That file will automatically install everything. After that it will start the Stable Diffusion interface in a web browser.
@ -21,4 +21,4 @@ If you have any problems, please:
3. Or, file an issue at https://github.com/cmdr2/stable-diffusion-ui/issues
Thanks
cmdr2 (and contributors to the project)
cmdr2 (and contributors to the project)

View File

@ -23,6 +23,7 @@ Click the download button for your operating system:
- Minimum 8 GB of system RAM.
- Atleast 25 GB of space on the hard disk.
The installer will take care of whatever is needed. If you face any problems, you can join the friendly [Discord community](https://discord.com/invite/u9yhsFmEkB) and ask for assistance.
## On Windows:

View File

@ -18,13 +18,15 @@ os_name = platform.system()
modules_to_check = {
"torch": ("1.11.0", "1.13.1", "2.0.0"),
"torchvision": ("0.12.0", "0.14.1", "0.15.1"),
"sdkit": "1.0.101",
"sdkit": "1.0.112",
"stable-diffusion-sdkit": "2.1.4",
"rich": "12.6.0",
"uvicorn": "0.19.0",
"fastapi": "0.85.1",
"pycloudflared": "0.2.0",
# "xformers": "0.0.16",
}
modules_to_log = ["torch", "torchvision", "sdkit", "stable-diffusion-sdkit"]
def version(module_name: str) -> str:
@ -89,7 +91,8 @@ def init():
traceback.print_exc()
fail(module_name)
print(f"{module_name}: {version(module_name)}")
if module_name in modules_to_log:
print(f"{module_name}: {version(module_name)}")
### utilities

View File

@ -39,6 +39,8 @@ if [ "$0" == "bash" ]; then
export PYTHONPATH="$(pwd)/stable-diffusion/env/lib/python3.8/site-packages"
fi
export PYTHONNOUSERSITE=y
which python
python --version

View File

@ -4,7 +4,7 @@ import sys
# The config file is in the same directory as this script
config_directory = os.path.dirname(__file__)
config_yaml = os.path.join(config_directory, "config.yaml")
# config_yaml = os.path.join(config_directory, "config.yaml")
config_json = os.path.join(config_directory, "config.json")
parser = argparse.ArgumentParser(description='Get values from config file')
@ -16,15 +16,16 @@ parser.add_argument('key', metavar='key', nargs='+',
args = parser.parse_args()
if os.path.isfile(config_yaml):
import yaml
with open(config_yaml, 'r') as configfile:
try:
config = yaml.safe_load(configfile)
except Exception as e:
print(e, file=sys.stderr)
config = {}
elif os.path.isfile(config_json):
# if os.path.isfile(config_yaml):
# import yaml
# with open(config_yaml, 'r') as configfile:
# try:
# config = yaml.safe_load(configfile)
# except Exception as e:
# print(e, file=sys.stderr)
# config = {}
# el
if os.path.isfile(config_json):
import json
with open(config_json, 'r') as configfile:
try:

View File

@ -67,7 +67,6 @@ if "%update_branch%"=="" (
@xcopy sd-ui-files\ui ui /s /i /Y /q
@copy sd-ui-files\scripts\on_sd_start.bat scripts\ /Y
@copy sd-ui-files\scripts\check_modules.py scripts\ /Y
@copy sd-ui-files\scripts\check_models.py scripts\ /Y
@copy sd-ui-files\scripts\get_config.py scripts\ /Y
@copy "sd-ui-files\scripts\Start Stable Diffusion UI.cmd" . /Y
@copy "sd-ui-files\scripts\Developer Console.cmd" . /Y

View File

@ -50,7 +50,6 @@ cp -Rf sd-ui-files/ui .
cp sd-ui-files/scripts/on_sd_start.sh scripts/
cp sd-ui-files/scripts/bootstrap.sh scripts/
cp sd-ui-files/scripts/check_modules.py scripts/
cp sd-ui-files/scripts/check_models.py scripts/
cp sd-ui-files/scripts/get_config.py scripts/
cp sd-ui-files/scripts/start.sh .
cp sd-ui-files/scripts/developer_console.sh .

View File

@ -5,7 +5,6 @@
@copy sd-ui-files\scripts\on_env_start.bat scripts\ /Y
@copy sd-ui-files\scripts\check_modules.py scripts\ /Y
@copy sd-ui-files\scripts\check_models.py scripts\ /Y
@copy sd-ui-files\scripts\get_config.py scripts\ /Y
if exist "%cd%\profile" (

View File

@ -4,7 +4,6 @@ cp sd-ui-files/scripts/functions.sh scripts/
cp sd-ui-files/scripts/on_env_start.sh scripts/
cp sd-ui-files/scripts/bootstrap.sh scripts/
cp sd-ui-files/scripts/check_modules.py scripts/
cp sd-ui-files/scripts/check_models.py scripts/
cp sd-ui-files/scripts/get_config.py scripts/
source ./scripts/functions.sh

View File

@ -100,7 +100,28 @@ def init():
def getConfig(default_val=APP_CONFIG_DEFAULTS):
try:
config_json_path = os.path.join(CONFIG_DIR, "config.json")
if not os.path.exists(config_json_path):
# compatibility with upcoming yaml changes, switching from beta to main
config_yaml_path = os.path.join(CONFIG_DIR, "..", "config.yaml")
# migrate the old config yaml location
config_legacy_yaml = os.path.join(CONFIG_DIR, "config.yaml")
if os.path.isfile(config_legacy_yaml):
shutil.move(config_legacy_yaml, config_yaml_path)
if os.path.exists(config_yaml_path):
try:
import yaml
with open(config_yaml_path, "r", encoding="utf-8") as f:
config = yaml.safe_load(f)
setConfig(config) # save to config.json
os.remove(config_yaml_path) # delete the yaml file
except:
log.warn(traceback.format_exc())
config = default_val
elif not os.path.exists(config_json_path):
config = default_val
else:
with open(config_json_path, "r", encoding="utf-8") as f:

View File

@ -60,7 +60,7 @@ def load_default_models(context: Context):
# init default model paths
for model_type in MODELS_TO_LOAD_ON_START:
context.model_paths[model_type] = resolve_model_to_use(model_type=model_type)
context.model_paths[model_type] = resolve_model_to_use(model_type=model_type, fail_if_not_found=False)
try:
load_model(
context,
@ -72,7 +72,12 @@ def load_default_models(context: Context):
del context.model_load_errors[model_type]
except Exception as e:
log.error(f"[red]Error while loading {model_type} model: {context.model_paths[model_type]}[/red]")
log.exception(e)
if "DefaultCPUAllocator: not enough memory" in str(e):
log.error(
f"[red]Your PC is low on system RAM. Please add some virtual memory (or swap space) by following the instructions at this link: https://www.ibm.com/docs/en/opw/8.2.0?topic=tuning-optional-increasing-paging-file-size-windows-computers[/red]"
)
else:
log.exception(e)
del context.model_paths[model_type]
context.model_load_errors[model_type] = str(e) # storing the entire Exception can lead to memory leaks
@ -85,7 +90,7 @@ def unload_all(context: Context):
del context.model_load_errors[model_type]
def resolve_model_to_use(model_name: str = None, model_type: str = None):
def resolve_model_to_use(model_name: str = None, model_type: str = None, fail_if_not_found: bool = True):
model_extensions = MODEL_EXTENSIONS.get(model_type, [])
default_models = DEFAULT_MODELS.get(model_type, [])
config = app.getConfig()
@ -108,7 +113,7 @@ def resolve_model_to_use(model_name: str = None, model_type: str = None):
return os.path.abspath(model_name + model_extension)
# Can't find requested model, check the default paths.
if model_type == "stable-diffusion":
if model_type == "stable-diffusion" and not fail_if_not_found:
for default_model in default_models:
default_model_path = os.path.join(model_dir, default_model["file_name"])
if os.path.exists(default_model_path):
@ -118,7 +123,8 @@ def resolve_model_to_use(model_name: str = None, model_type: str = None):
)
return default_model_path
return None
if model_name and fail_if_not_found:
raise Exception(f"Could not find the desired model {model_name}! Is it present in the {model_dir} folder?")
def reload_models_if_necessary(context: Context, task_data: TaskData):
@ -142,10 +148,12 @@ def reload_models_if_necessary(context: Context, task_data: TaskData):
if context.model_paths.get(model_type) != path
}
if task_data.codeformer_upscale_faces and "realesrgan" not in models_to_reload.keys():
models_to_reload["realesrgan"] = resolve_model_to_use(
DEFAULT_MODELS["realesrgan"][0]["file_name"], "realesrgan"
)
if task_data.codeformer_upscale_faces:
if "realesrgan" not in models_to_reload and "realesrgan" not in context.models:
default_realesrgan = DEFAULT_MODELS["realesrgan"][0]["file_name"]
models_to_reload["realesrgan"] = resolve_model_to_use(default_realesrgan, "realesrgan")
elif "realesrgan" in models_to_reload and models_to_reload["realesrgan"] is None:
del models_to_reload["realesrgan"] # don't unload realesrgan
if set_vram_optimizations(context) or set_clip_skip(context, task_data): # reload SD
models_to_reload["stable-diffusion"] = model_paths_in_req["stable-diffusion"]

View File

@ -7,10 +7,12 @@ from easydiffusion import device_manager
from easydiffusion.types import GenerateImageRequest
from easydiffusion.types import Image as ResponseImage
from easydiffusion.types import Response, TaskData, UserInitiatedStop
from easydiffusion.model_manager import DEFAULT_MODELS, resolve_model_to_use
from easydiffusion.utils import get_printable_request, log, save_images_to_disk
from sdkit import Context
from sdkit.filter import apply_filters
from sdkit.generate import generate_images
from sdkit.models import load_model
from sdkit.utils import (
diffusers_latent_samples_to_images,
gc,
@ -157,36 +159,51 @@ def filter_images(req: GenerateImageRequest, task_data: TaskData, images: list,
if user_stopped:
return images
filters_to_apply = []
filter_params = {}
if task_data.block_nsfw:
filters_to_apply.append("nsfw_checker")
if task_data.use_face_correction and "codeformer" in task_data.use_face_correction.lower():
filters_to_apply.append("codeformer")
images = apply_filters(context, "nsfw_checker", images)
filter_params["upscale_faces"] = task_data.codeformer_upscale_faces
if task_data.use_face_correction and "codeformer" in task_data.use_face_correction.lower():
default_realesrgan = DEFAULT_MODELS["realesrgan"][0]["file_name"]
prev_realesrgan_path = None
if task_data.codeformer_upscale_faces and default_realesrgan not in context.model_paths["realesrgan"]:
prev_realesrgan_path = context.model_paths["realesrgan"]
context.model_paths["realesrgan"] = resolve_model_to_use(default_realesrgan, "realesrgan")
load_model(context, "realesrgan")
try:
images = apply_filters(
context,
"codeformer",
images,
upscale_faces=task_data.codeformer_upscale_faces,
codeformer_fidelity=task_data.codeformer_fidelity,
)
finally:
if prev_realesrgan_path:
context.model_paths["realesrgan"] = prev_realesrgan_path
load_model(context, "realesrgan")
elif task_data.use_face_correction and "gfpgan" in task_data.use_face_correction.lower():
filters_to_apply.append("gfpgan")
images = apply_filters(context, "gfpgan", images)
if task_data.use_upscale:
if "realesrgan" in task_data.use_upscale.lower():
filters_to_apply.append("realesrgan")
images = apply_filters(context, "realesrgan", images, scale=task_data.upscale_amount)
elif task_data.use_upscale == "latent_upscaler":
filters_to_apply.append("latent_upscaler")
images = apply_filters(
context,
"latent_upscaler",
images,
scale=task_data.upscale_amount,
latent_upscaler_options={
"prompt": req.prompt,
"negative_prompt": req.negative_prompt,
"seed": req.seed,
"num_inference_steps": task_data.latent_upscaler_steps,
"guidance_scale": 0,
},
)
filter_params["latent_upscaler_options"] = {
"prompt": req.prompt,
"negative_prompt": req.negative_prompt,
"seed": req.seed,
"num_inference_steps": task_data.latent_upscaler_steps,
"guidance_scale": 0,
}
filter_params["scale"] = task_data.upscale_amount
if len(filters_to_apply) == 0:
return images
return apply_filters(context, filters_to_apply, images, **filter_params)
return images
def construct_response(images: list, seeds: list, task_data: TaskData, base_seed: int):

View File

@ -15,6 +15,7 @@ from fastapi import FastAPI, HTTPException
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel, Extra
from starlette.responses import FileResponse, JSONResponse, StreamingResponse
from pycloudflared import try_cloudflare
log.info(f"started in {app.SD_DIR}")
log.info(f"started at {datetime.datetime.now():%x %X}")
@ -113,6 +114,14 @@ def init():
def get_image(task_id: int, img_id: int):
return get_image_internal(task_id, img_id)
@server_api.post("/tunnel/cloudflare/start")
def start_cloudflare_tunnel(req: dict):
return start_cloudflare_tunnel_internal(req)
@server_api.post("/tunnel/cloudflare/stop")
def stop_cloudflare_tunnel(req: dict):
return stop_cloudflare_tunnel_internal(req)
@server_api.get("/")
def read_root():
return FileResponse(os.path.join(app.SD_UI_DIR, "index.html"), headers=NOCACHE_HEADERS)
@ -211,6 +220,8 @@ def ping_internal(session_id: str = None):
session = task_manager.get_cached_session(session_id, update_ttl=True)
response["tasks"] = {id(t): t.status for t in session.tasks}
response["devices"] = task_manager.get_devices()
if cloudflare.address != None:
response["cloudflare"] = cloudflare.address
return JSONResponse(response, headers=NOCACHE_HEADERS)
@ -322,3 +333,47 @@ def get_image_internal(task_id: int, img_id: int):
return StreamingResponse(img_data, media_type="image/jpeg")
except KeyError as e:
raise HTTPException(status_code=500, detail=str(e))
#---- Cloudflare Tunnel ----
class CloudflareTunnel:
def __init__(self):
config = app.getConfig()
self.urls = None
self.port = config.get("net", {}).get("listen_port")
def start(self):
if self.port:
self.urls = try_cloudflare(self.port)
def stop(self):
if self.urls:
try_cloudflare.terminate(self.port)
self.urls = None
@property
def address(self):
if self.urls:
return self.urls.tunnel
else:
return None
cloudflare = CloudflareTunnel()
def start_cloudflare_tunnel_internal(req: dict):
try:
cloudflare.start()
log.info(f"- Started cloudflare tunnel. Using address: {cloudflare.address}")
return JSONResponse({"address":cloudflare.address})
except Exception as e:
log.error(str(e))
log.error(traceback.format_exc())
return HTTPException(status_code=500, detail=str(e))
def stop_cloudflare_tunnel_internal(req: dict):
try:
cloudflare.stop()
except Exception as e:
log.error(str(e))
log.error(traceback.format_exc())
return HTTPException(status_code=500, detail=str(e))

View File

@ -52,6 +52,7 @@ class TaskData(BaseModel):
stream_image_progress_interval: int = 5
clip_skip: bool = False
codeformer_upscale_faces: bool = False
codeformer_fidelity: float = 0.5
class MergeRequest(BaseModel):

View File

@ -30,7 +30,7 @@
<h1>
<img id="logo_img" src="/media/images/icon-512x512.png" >
Easy Diffusion
<small>v2.5.39 <span id="updateBranchLabel"></span></small>
<small>v2.5.41 <span id="updateBranchLabel"></span></small>
</h1>
</div>
<div id="server-status">
@ -227,7 +227,10 @@
</td></tr>
<tr id="lora_alpha_container" class="pl-5">
<td><label for="lora_alpha_slider">LoRA Strength:</label></td>
<td> <input id="lora_alpha_slider" name="lora_alpha_slider" class="editor-slider" value="50" type="range" min="0" max="100"> <input id="lora_alpha" name="lora_alpha" size="4" pattern="^[0-9\.]+$" onkeypress="preventNonNumericalInput(event)"><br/></td>
<td>
<small>-2</small> <input id="lora_alpha_slider" name="lora_alpha_slider" class="editor-slider" value="50" type="range" min="-200" max="200"> <small>2</small> &nbsp;
<input id="lora_alpha" name="lora_alpha" size="4" pattern="^-?[0-9]*\.?[0-9]*$" onkeypress="preventNonNumericalInput(event)"><br/>
</td>
</tr>
<tr class="pl-5"><td><label for="hypernetwork_model">Hypernetwork:</label></td><td>
<input id="hypernetwork_model" type="text" spellcheck="false" autocomplete="off" class="model-filter" data-path="" />
@ -263,11 +266,12 @@
<div><ul>
<li><b class="settings-subheader">Render Settings</b></li>
<li class="pl-5"><input id="stream_image_progress" name="stream_image_progress" type="checkbox"> <label for="stream_image_progress">Show a live preview <small>(uses more VRAM, slower images)</small></label></li>
<li class="pl-5">
<li class="pl-5" id="use_face_correction_container">
<input id="use_face_correction" name="use_face_correction" type="checkbox"> <label for="use_face_correction">Fix incorrect faces and eyes</label> <div style="display:inline-block;"><input id="gfpgan_model" type="text" spellcheck="false" autocomplete="off" class="model-filter" data-path="" /></div>
<div id="codeformer_settings" class="displayNone sub-settings">
<input id="codeformer_upscale_faces" name="codeformer_upscale_faces" type="checkbox"><label for="codeformer_upscale_faces">Upscale Faces <small>(improves the resolution of faces)</small></label>
</div>
<table id="codeformer_settings" class="displayNone sub-settings">
<tr class="pl-5"><td><label for="codeformer_fidelity_slider">Strength:</label></td><td><input id="codeformer_fidelity_slider" name="codeformer_fidelity_slider" class="editor-slider" value="5" type="range" min="0" max="10"> <input id="codeformer_fidelity" name="codeformer_fidelity" size="4" pattern="^[0-9\.]+$" onkeypress="preventNonNumericalInput(event)"></td></tr>
<tr class="pl-5"><td><label for="codeformer_upscale_faces">Upscale Faces:</label></td><td><input id="codeformer_upscale_faces" name="codeformer_upscale_faces" type="checkbox" checked> <label><small>(improves the resolution of faces)</small></label></td></tr>
</table>
</li>
<li class="pl-5">
<input id="use_upscale" name="use_upscale" type="checkbox"> <label for="use_upscale">Scale up by</label>
@ -281,9 +285,9 @@
<option value="RealESRGAN_x4plus_anime_6B">RealESRGAN_x4plus_anime_6B</option>
<option value="latent_upscaler">Latent Upscaler 2x</option>
</select>
<div id="latent_upscaler_settings" class="displayNone sub-settings">
<label for="latent_upscaler_steps_slider">Upscaling Steps:</label></td><td> <input id="latent_upscaler_steps_slider" name="latent_upscaler_steps_slider" class="editor-slider" value="10" type="range" min="1" max="50"> <input id="latent_upscaler_steps" name="latent_upscaler_steps" size="4" pattern="^[0-9\.]+$" onkeypress="preventNonNumericalInput(event)">
</div>
<table id="latent_upscaler_settings" class="displayNone sub-settings">
<tr class="pl-5"><td><label for="latent_upscaler_steps_slider">Upscaling Steps:</label></td><td><input id="latent_upscaler_steps_slider" name="latent_upscaler_steps_slider" class="editor-slider" value="10" type="range" min="1" max="50"> <input id="latent_upscaler_steps" name="latent_upscaler_steps" size="4" pattern="^[0-9\.]+$" onkeypress="preventNonNumericalInput(event)"></td></tr>
</table>
</li>
<li class="pl-5"><input id="show_only_filtered_image" name="show_only_filtered_image" type="checkbox" checked> <label for="show_only_filtered_image">Show only the corrected/upscaled image</label></li>
</ul></div>
@ -361,10 +365,16 @@
<div id="tab-content-settings" class="tab-content">
<div id="system-settings" class="tab-content-inner">
<h1>System Settings</h1>
<div class="parameters-table"></div>
<div class="parameters-table" id="system-settings-table"></div>
<br/>
<button id="save-system-settings-btn" class="primaryButton">Save</button>
<br/><br/>
<div id="share-easy-diffusion">
<h3><i class="fa fa-user-group"></i> Share Easy Diffusion</h3>
<div class="parameters-table" id="system-settings-network-table">
</div>
</div>
<br/><br/>
<div>
<h3><i class="fa fa-microchip icon"></i> System Info</h3>
<div id="system-info">
@ -539,7 +549,8 @@ async function init() {
SD.init({
events: {
statusChange: setServerStatus,
idle: onIdle
idle: onIdle,
ping: tunnelUpdate
}
})

View File

@ -69,13 +69,15 @@
}
.parameters-table > div:first-child {
border-radius: 12px 12px 0px 0px;
border-top-left-radius: 12px;
border-top-right-radius: 12px;
}
.parameters-table > div:last-child {
border-radius: 0px 0px 12px 12px;
border-bottom-left-radius: 12px;
border-bottom-right-radius: 12px;
}
.parameters-table .fa-fire {
color: #F7630C;
}
}

View File

@ -96,7 +96,7 @@
.editor-controls-center {
/* background: var(--background-color2); */
flex: 1;
flex: 0;
display: flex;
justify-content: center;
align-items: center;
@ -105,6 +105,8 @@
.editor-controls-center > div {
position: relative;
background: black;
margin: 20pt;
margin-top: 40pt;
}
.editor-controls-center canvas {
@ -164,8 +166,10 @@
margin: var(--popup-margin);
padding: var(--popup-padding);
min-height: calc(99h - (2 * var(--popup-margin)));
max-width: none;
max-width: fit-content;
min-width: fit-content;
margin-left: auto;
margin-right: auto;
}
.image-editor-popup h1 {

View File

@ -1309,6 +1309,29 @@ body.wait-pause {
padding-left: 5pt;
}
#cloudflare-address {
background-color: var(--background-color3);
padding: 6px;
border-radius: var(--input-border-radius);
border: var(--input-border-size) solid var(--input-border-color);
margin-top: 0.2em;
margin-bottom: 0.2em;
display: inline-block;
}
#copy-cloudflare-address {
padding: 4px 8px;
margin-left: 0.5em;
}
.expandedSettingRow {
background: var(--background-color1);
width: 95%;
border-radius: 4pt;
margin-top: 5pt;
margin-bottom: 3pt;
}
/* TOAST NOTIFICATIONS */
.toast-notification {
position: fixed;

View File

@ -186,6 +186,7 @@
const EVENT_TASK_START = "taskStart"
const EVENT_TASK_END = "taskEnd"
const EVENT_TASK_ERROR = "task_error"
const EVENT_PING = "ping"
const EVENT_UNEXPECTED_RESPONSE = "unexpectedResponse"
const EVENTS_TYPES = [
EVENT_IDLE,
@ -196,6 +197,7 @@
EVENT_TASK_START,
EVENT_TASK_END,
EVENT_TASK_ERROR,
EVENT_PING,
EVENT_UNEXPECTED_RESPONSE,
]
@ -240,6 +242,7 @@
setServerStatus("error", "offline")
return false
}
// Set status
switch (serverState.status) {
case ServerStates.init:
@ -261,6 +264,7 @@
break
}
serverState.time = Date.now()
await eventSource.fireEvent(EVENT_PING, serverState)
return true
} catch (e) {
console.error(e)

View File

@ -87,13 +87,15 @@ let promptStrengthField = document.querySelector("#prompt_strength")
let samplerField = document.querySelector("#sampler_name")
let samplerSelectionContainer = document.querySelector("#samplerSelection")
let useFaceCorrectionField = document.querySelector("#use_face_correction")
let gfpganModelField = new ModelDropdown(document.querySelector("#gfpgan_model"), ["codeformer", "gfpgan"])
let gfpganModelField = new ModelDropdown(document.querySelector("#gfpgan_model"), ["gfpgan", "codeformer"], "", false)
let useUpscalingField = document.querySelector("#use_upscale")
let upscaleModelField = document.querySelector("#upscale_model")
let upscaleAmountField = document.querySelector("#upscale_amount")
let latentUpscalerSettings = document.querySelector("#latent_upscaler_settings")
let latentUpscalerStepsSlider = document.querySelector("#latent_upscaler_steps_slider")
let latentUpscalerStepsField = document.querySelector("#latent_upscaler_steps")
let codeformerFidelitySlider = document.querySelector("#codeformer_fidelity_slider")
let codeformerFidelityField = document.querySelector("#codeformer_fidelity")
let stableDiffusionModelField = new ModelDropdown(document.querySelector("#stable_diffusion_model"), "stable-diffusion")
let clipSkipField = document.querySelector("#clip_skip")
let tilingField = document.querySelector("#tiling")
@ -1266,6 +1268,7 @@ function getCurrentUserRequest() {
if (gfpganModelField.value.includes("codeformer")) {
newTask.reqBody.codeformer_upscale_faces = document.querySelector("#codeformer_upscale_faces").checked
newTask.reqBody.codeformer_fidelity = 1 - parseFloat(codeformerFidelityField.value)
}
}
if (useUpscalingField.checked) {
@ -1588,8 +1591,10 @@ function onFixFaceModelChange() {
let codeformerSettings = document.querySelector("#codeformer_settings")
if (gfpganModelField.value === "codeformer" && !gfpganModelField.disabled) {
codeformerSettings.classList.remove("displayNone")
codeformerSettings.classList.add("expandedSettingRow")
} else {
codeformerSettings.classList.add("displayNone")
codeformerSettings.classList.remove("expandedSettingRow")
}
}
gfpganModelField.addEventListener("change", onFixFaceModelChange)
@ -1610,9 +1615,11 @@ function onUpscaleModelChange() {
upscale4x.disabled = true
upscaleAmountField.value = "2"
latentUpscalerSettings.classList.remove("displayNone")
latentUpscalerSettings.classList.add("expandedSettingRow")
} else {
upscale4x.disabled = false
latentUpscalerSettings.classList.add("displayNone")
latentUpscalerSettings.classList.remove("expandedSettingRow")
}
}
upscaleModelField.addEventListener("change", onUpscaleModelChange)
@ -1627,6 +1634,27 @@ document.onkeydown = function(e) {
}
}
/********************* CodeFormer Fidelity **************************/
function updateCodeformerFidelity() {
codeformerFidelityField.value = codeformerFidelitySlider.value / 10
codeformerFidelityField.dispatchEvent(new Event("change"))
}
function updateCodeformerFidelitySlider() {
if (codeformerFidelityField.value < 0) {
codeformerFidelityField.value = 0
} else if (codeformerFidelityField.value > 1) {
codeformerFidelityField.value = 1
}
codeformerFidelitySlider.value = codeformerFidelityField.value * 10
codeformerFidelitySlider.dispatchEvent(new Event("change"))
}
codeformerFidelitySlider.addEventListener("input", updateCodeformerFidelity)
codeformerFidelityField.addEventListener("input", updateCodeformerFidelitySlider)
updateCodeformerFidelity()
/********************* Latent Upscaler Steps **************************/
function updateLatentUpscalerSteps() {
latentUpscalerStepsField.value = latentUpscalerStepsSlider.value
@ -1725,10 +1753,10 @@ function updateLoraAlpha() {
}
function updateLoraAlphaSlider() {
if (loraAlphaField.value < 0) {
loraAlphaField.value = 0
} else if (loraAlphaField.value > 1) {
loraAlphaField.value = 1
if (loraAlphaField.value < -2) {
loraAlphaField.value = -2
} else if (loraAlphaField.value > 2) {
loraAlphaField.value = 2
}
loraAlphaSlider.value = loraAlphaField.value * 100
@ -1979,6 +2007,38 @@ resumeBtn.addEventListener("click", function() {
document.body.classList.remove("wait-pause")
})
function tunnelUpdate(event) {
if ("cloudflare" in event) {
document.getElementById("cloudflare-off").classList.add("displayNone")
document.getElementById("cloudflare-on").classList.remove("displayNone")
cloudflareAddressField.innerHTML = event.cloudflare
document.getElementById("toggle-cloudflare-tunnel").innerHTML = "Stop"
} else {
document.getElementById("cloudflare-on").classList.add("displayNone")
document.getElementById("cloudflare-off").classList.remove("displayNone")
document.getElementById("toggle-cloudflare-tunnel").innerHTML = "Start"
}
}
document.getElementById("toggle-cloudflare-tunnel").addEventListener("click", async function() {
let command = "stop"
if (document.getElementById("toggle-cloudflare-tunnel").innerHTML == "Start") {
command = "start"
}
showToast(`Cloudflare tunnel ${command} initiated. Please wait.`)
let res = await fetch("/tunnel/cloudflare/" + command, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({}),
})
res = await res.json()
console.log(`Cloudflare tunnel ${command} result:`, res)
})
/* Pause function */
document.querySelectorAll(".tab").forEach(linkTabContents)

View File

@ -11,6 +11,12 @@ var ParameterType = {
custom: "custom",
}
/**
* Element shortcuts
*/
let parametersTable = document.querySelector("#system-settings-table")
let networkParametersTable = document.querySelector("#system-settings-network-table")
/**
* JSDoc style
* @typedef {object} Parameter
@ -186,6 +192,7 @@ var PARAMETERS = [
icon: "fa-network-wired",
default: true,
saveInAppConfig: true,
table: networkParametersTable,
},
{
id: "listen_port",
@ -198,6 +205,7 @@ var PARAMETERS = [
return `<input id="${parameter.id}" name="${parameter.id}" size="6" value="9000" onkeypress="preventNonNumericalInput(event)">`
},
saveInAppConfig: true,
table: networkParametersTable,
},
{
id: "use_beta_channel",
@ -218,6 +226,21 @@ var PARAMETERS = [
default: false,
saveInAppConfig: true,
},
{
id: "cloudflare",
type: ParameterType.custom,
label: "Cloudflare tunnel",
note: `<span id="cloudflare-off">Create a VPN tunnel to share your Easy Diffusion instance with your friends. This will
generate a web server address on the public Internet for your Easy Diffusion instance. </span>
<div id="cloudflare-on" class="displayNone"><div>This Easy Diffusion server is available on the Internet using the
address:</div><div><div id="cloudflare-address"></div><button id="copy-cloudflare-address">Copy</button></div></div>
<b>Anyone knowing this address can access your server.</b> The address of your server will change each time
you share a session.<br>
Uses <a href="https://try.cloudflare.com/" target="_blank">Cloudflare services</a>.`,
icon: ["fa-brands", "fa-cloudflare"],
render: () => '<button id="toggle-cloudflare-tunnel" class="primaryButton">Start</button>',
table: networkParametersTable,
}
]
function getParameterSettingsEntry(id) {
@ -266,7 +289,6 @@ function getParameterElement(parameter) {
}
}
let parametersTable = document.querySelector("#system-settings .parameters-table")
/**
* fill in the system settings popup table
* @param {Array<Parameter> | undefined} parameters
@ -293,7 +315,10 @@ function initParameters(parameters) {
noteElements.push(noteElement)
}
const icon = parameter.icon ? [createElement("i", undefined, ["fa", parameter.icon])] : []
if (typeof(parameter.icon) == "string") {
parameter.icon = [parameter.icon]
}
const icon = parameter.icon ? [createElement("i", undefined, ["fa", ...parameter.icon])] : []
const label = typeof parameter.label === "function" ? parameter.label(parameter) : parameter.label
const labelElement = createElement("label", { for: parameter.id })
@ -313,7 +338,13 @@ function initParameters(parameters) {
elementWrapper,
]
)
parametersTable.appendChild(newrow)
let p = parametersTable
if (parameter.table) {
p = parameter.table
}
p.appendChild(newrow)
parameter.settingsEntry = newrow
})
}
@ -633,7 +664,7 @@ saveSettingsBtn.addEventListener("click", function() {
update_branch: updateBranch,
}
Array.from(parametersTable.children).forEach((parameterRow) => {
document.querySelectorAll('#system-settings [data-setting-id]').forEach((parameterRow) => {
if (parameterRow.dataset.saveInAppConfig === "true") {
const parameterElement =
document.getElementById(parameterRow.dataset.settingId) ||
@ -667,8 +698,25 @@ saveSettingsBtn.addEventListener("click", function() {
})
const savePromise = changeAppConfig(updateAppConfigRequest)
showToast("Settings saved")
saveSettingsBtn.classList.add("active")
Promise.all([savePromise, asyncDelay(300)]).then(() => saveSettingsBtn.classList.remove("active"))
})
listenToNetworkField.addEventListener("change", debounce( ()=>{
saveSettingsBtn.click()
}, 1000))
listenPortField.addEventListener("change", debounce( ()=>{
saveSettingsBtn.click()
}, 1000))
let copyCloudflareAddressBtn = document.querySelector("#copy-cloudflare-address")
let cloudflareAddressField = document.getElementById("cloudflare-address")
copyCloudflareAddressBtn.addEventListener("click", (e) => {
navigator.clipboard.writeText(cloudflareAddressField.innerHTML)
showToast("Copied server address to clipboard")
})
document.addEventListener("system_info_update", (e) => setDeviceInfo(e.detail))

View File

@ -38,6 +38,8 @@ class ModelDropdown {
noneEntry //= ''
modelFilterInitialized //= undefined
sorted //= true
/* MIMIC A REGULAR INPUT FIELD */
get parentElement() {
return this.modelFilter.parentElement
@ -83,10 +85,11 @@ class ModelDropdown {
/* SEARCHABLE INPUT */
constructor(input, modelKey, noneEntry = "") {
constructor(input, modelKey, noneEntry = "", sorted = true) {
this.modelFilter = input
this.noneEntry = noneEntry
this.modelKey = modelKey
this.sorted = sorted
if (modelsOptions !== undefined) {
// reuse models from cache (only useful for plugins, which are loaded after models)
@ -94,7 +97,8 @@ class ModelDropdown {
let modelKeys = Array.isArray(this.modelKey) ? this.modelKey : [this.modelKey]
for (let i = 0; i < modelKeys.length; i++) {
let key = modelKeys[i]
this.inputModels.push(...modelsOptions[key])
let k = Array.isArray(modelsOptions[key]) ? modelsOptions[key] : [modelsOptions[key]]
this.inputModels.push(...k)
}
this.populateModels()
}
@ -102,12 +106,12 @@ class ModelDropdown {
"refreshModels",
this.bind(function(e) {
// reload the models
this.inputModels = modelsOptions[this.modelKey]
this.inputModels = []
let modelKeys = Array.isArray(this.modelKey) ? this.modelKey : [this.modelKey]
for (let i = 0; i < modelKeys.length; i++) {
let key = modelKeys[i]
this.inputModels.push(...modelsOptions[key])
let k = Array.isArray(modelsOptions[key]) ? modelsOptions[key] : [modelsOptions[key]]
this.inputModels.push(...k)
}
this.populateModels()
}, this)
@ -565,11 +569,15 @@ class ModelDropdown {
})
const childFolderNames = Array.from(foldersMap.keys())
this.sortStringArray(childFolderNames)
if (this.sorted) {
this.sortStringArray(childFolderNames)
}
const folderElements = childFolderNames.map((name) => foldersMap.get(name))
const modelNames = Array.from(modelsMap.keys())
this.sortStringArray(modelNames)
if (this.sorted) {
this.sortStringArray(modelNames)
}
const modelElements = modelNames.map((name) => modelsMap.get(name))
if (modelElements.length && folderName) {

View File

@ -402,12 +402,12 @@ function debounce(func, wait, immediate) {
function preventNonNumericalInput(e) {
e = e || window.event
let charCode = typeof e.which == "undefined" ? e.keyCode : e.which
let charStr = String.fromCharCode(charCode)
let re = e.target.getAttribute("pattern") || "^[0-9]+$"
re = new RegExp(re)
const charCode = typeof e.which == "undefined" ? e.keyCode : e.which
const charStr = String.fromCharCode(charCode)
const newInputValue = `${e.target.value}${charStr}`
const re = new RegExp(e.target.getAttribute("pattern") || "^[0-9]+$")
if (!charStr.match(re)) {
if (!re.test(charStr) && !re.test(newInputValue)) {
e.preventDefault()
}
}

View File

@ -0,0 +1,326 @@
;(function(){
"use strict";
const PAPERSIZE = [
{id: "a3p", width: 297, height: 420, unit: "mm"},
{id: "a3l", width: 420, height: 297, unit: "mm"},
{id: "a4p", width: 210, height: 297, unit: "mm"},
{id: "a4l", width: 297, height: 210, unit: "mm"},
{id: "ll", width: 279, height: 216, unit: "mm"},
{id: "lp", width: 216, height: 279, unit: "mm"},
{id: "hd", width: 1920, height: 1080, unit: "pixels"},
{id: "4k", width: 3840, height: 2160, unit: "pixels"},
]
// ---- Register plugin
PLUGINS['IMAGE_INFO_BUTTONS'].push({
html: '<i class="fa-solid fa-table-cells-large"></i> Download tiled image',
on_click: onDownloadTiledImage,
filter: (req, img) => req.tiling != "none",
})
var thisImage
function onDownloadTiledImage(req, img) {
document.getElementById("download-tiled-image-dialog").showModal()
thisImage = new Image()
thisImage.src = img.src
thisImage.dataset["prompt"] = img.dataset["prompt"]
}
// ---- Add HTML
document.getElementById('container').lastElementChild.insertAdjacentHTML("afterend",
`<dialog id="download-tiled-image-dialog">
<h1>Download tiled image</h1>
<div class="download-tiled-image dtim-container">
<div class="download-tiled-image-top">
<div class="tab-container">
<span id="tab-image-tiles" class="tab active">
<span>Number of tiles</small></span>
</span>
<span id="tab-image-size" class="tab">
<span>Image dimensions</span>
</span>
</div>
<div>
<div id="tab-content-image-tiles" class="tab-content active">
<div class="tab-content-inner">
<label for="dtim1-width">Width:</label> <input id="dtim1-width" min="1" max="99" type="number" value="2">
<label for="dtim1-height">Height:</label> <input id="dtim1-height" min="1" max="99" type="number" value="2">
</div>
</div>
<div id="tab-content-image-size" class="tab-content">
<div class="tab-content-inner">
<div class="method-2-options">
<label for="dtim2-width">Width:</label> <input id="dtim2-width" size="3" value="1920">
<label for="dtim2-height">Height:</label> <input id="dtim2-height" size="3" value="1080">
<select id="dtim2-unit">
<option>pixels</option>
<option>mm</option>
<option>inches</option>
</select>
</div>
<div class="method-2-dpi">
<label for="dtim2-dpi">DPI:</label> <input id="dtim2-dpi" size="3" value="72">
</div>
<div class="method-2-paper">
<i>Some standard sizes:</i><br>
<button id="dtim2-a3p">A3 portrait</button><button id="dtim2-a3l">A3 landscape</button><br>
<button id="dtim2-a4p">A4 portrait</button><button id="dtim2-a4l">A4 landscape</button><br>
<button id="dtim2-lp">Letter portrait</button><button id="dtim2-ll">Letter landscape</button><br>
<button id="dtim2-hd">Full HD</button><button id="dtim2-4k">4K</button>
</div>
</div>
</div>
</div>
</div>
<div class="download-tiled-image-placement">
<div class="tab-container">
<span id="tab-image-placement" class="tab active">
<span>Tile placement</span>
</span>
</div>
<div>
<div id="tab-content-image-placement" class="tab-content active">
<div class="tab-content-inner">
<img id="dtim-1tl" class="active" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA3BAMAAACiFTSCAAAAMFBMVEUCAgKIhobRz89vMJ7s6uo9PDx4d3ewra0bHR1dXV19NrLa2Nj+/f29u7uWlJQuLi7ws27qAAAACXBIWXMAAAsTAAALEwEAmpwYAAABlUlEQVQ4y7VUMU7DQBCckpYCJEpS0ByhcecuUZQUtvIHGku0vICSDtHkA9eltCylOEBKFInCRworXToK3kDJ7jpn2SYmgGESeWyPRuudvTugHTyC72momKDGMMJDLIhmgK+nWmuPXxtlxkhjExszRKqU6uRuTW7TYTwh6HTpR25+JLcngBJ5jL5wIecqu9nFbid3t27N7vhrtypqV2SfP4zc5pfu/Msb3P6U4fru1eXpVg7tcmnDZ1gb0s1ceAEcSPI3uM2B9xLf7Z3YLlfJ/WCppF1QbbqxeW0brlztjXzprBhJrW8nu4HWGlt/xz1qcrervfmT2ma3WxpTjfK5ZUioNg+VsUL+tiXuI8YJLrd8KHyENyaqPWC8QGiwwlJ4LtyvNtb9vFKrqZXXeebkrEiN3ZUNXHJnO3aJkxt2aH2gDRNTLdyzJvee1CZXUTSJrhA55itlfszUdqDrxCQmGIEu9KfFFCRJYnpIgyB4JJlPWM6cY6MjN+UW5MjdM7FKavF/pFbfRD9zv8rjBa6FT5EJn0HoA8lOiD4+8B3mAAAAAElFTkSuQmCC" />
<img id="dtim-1tr" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA3BAMAAACiFTSCAAAAMFBMVEUCAgKIhobRz89vMJ7s6uo9PDx4d3ewra0bHR1dXV19NrLa2Nj+/f29u7uWlJQuLi7ws27qAAAACXBIWXMAAAsTAAALEwEAmpwYAAABoUlEQVQ4y61UsU7CUBQ9o6uGmJgwNSQOLS7dOndrGfwEDWnC4tQNB8duxoUfYGN86fZgIWy+iVUnwwYf4Oi9lxa0tFSpB9IDXM6775773gUaYWjbtoO+IrI1VsIKnjt2CYCllJqir7Wt7SlWWmn+m+t53sQbU5hAamtJrxRr/mppUnuZOgszOlgJK7gCUS93YVbzKqx2q9U2q71Mbf1Qbxc/qqadu7y509W7nX8Pt/K6JwwKO+HCGLRNKPy4oA9mkYUnwGeSJM9IBDknOJN+PNV2rEy9XyXLvaGcktuY0FBux9AP5rVYd96SofCsWFje0NJwUd2rUse/UTfLPTspd83iFFZZYY4xbKoRsKlmaypjjoaICA+4ZYrO8SJ8mfEV0PF9P0Tb94U3wj7eheaHJ3VZLKxbcs4P6uartz/nMYlKbFnzYtWe5ze0wtSjDd1Ph7iReheucS0aRYM78pwoiiDPUc6Dpg19S9N0ipYOgiClw5TqgN6I6aGD7S2RkcsbppnKbLPnPHt7VZOpxvN/cq1cHf9BbeFeqIsL4Wt8CN/gC1XPfwv6U6jJAAAAAElFTkSuQmCC" /><br>
<img id="dtim-1bl" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA3BAMAAACiFTSCAAAAMFBMVEUCAgKIhobRz89vMJ7s6uo9PDx4d3ewra0bHR1dXV19NrLa2Nj+/f29u7uWlJQuLi7ws27qAAAACXBIWXMAAAsTAAALEwEAmpwYAAABjElEQVQ4y7WUsU7DMBCG/5GVoUiMMLC4ZcmWrVXVDq7yDiyRuvIEjGyIpS/graMVKYMBqVUkhoYOUTc2Bp6BEd+5qdI2SasGLpE/KZc/d76LD/i6JrvFPfMKGfMGDOCTGUMwA/SYA8yL7iEM8w2yzH1AHderh9D1alGuXmmtBaVGchFgQe8J69bOHZnIyCHsYu8AUkZRZLpYSClf0dAm4zCchGOEOWkNW7ggAO0+2QcY/SUS5ozZ+6uqNVHHR9Q8q1Bnm9+B1B17HdGxbDe1zn7sA1V7DskucbfmObOFb0LThrZTsjlGzHcw0iXcU6woQxFv9i3rah5UdSxvSa/+EMn6hp6iroy9+s/YL2mSjlxRE1fUkX2wZNqiPjrDTwmfDnbsjNeH0q9Y9ZRN2diJjeliJ+ksj+2v3W5j3d2N+R5b4T/fcjuvqppMvqfsdbqaxF7VCc3Vho9eUd0pZi6q1cpThemwdYB9NVVK2cy1NsLYmaqNNmZgZ6sQa7VPmWuavQG9ZkkjV+u42QH8BWe+iD71TSARAAAAAElFTkSuQmCC" />
<img id="dtim-1br" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA3BAMAAACiFTSCAAAAMFBMVEUCAgKIhobRz89vMJ7s6uo9PDx4d3ewra0bHR1dXV19NrLa2Nj+/f29u7uWlJQuLi7ws27qAAAACXBIWXMAAAsTAAALEwEAmpwYAAABkklEQVQ4y7VULU/DUBQ9EgtZSEimliWIt2Lmnq5rJ/gJkKXJDGquCGQdwewPzE2+PPeGWeaomgVF6tgPmOTe2zWkox8LC6dLT/ZuTs9957UXPQbuhTxcCF/jU/gGYBpgLH/7yIQNYuHXctniS9hhWlU+Wh03qVX1wwt1+eGKyoZXl8iYFbdmlFIj4N1au0THBUFgLTLrAvphSjcXUPk0RLNocodbpiiC3GcFT4C+7/shur4vvBX28SG0+o/UTHNqrZnXe3uVrW3oKtScuVeUvZYTK3lvyq21pBYRHihzxjlehC/3fHXqgQ5SArapAI9C63w1XR3GUuw75neuN6otV++78SOyza9Dv/lAj1Yf5T061XsQrjnUdSpMoYZ5qLSQvgG7JEmekQgOOWk9sV2l6kxqT4V3Nw1zb7IMyVsvBIcb6+w3lpfn1V+Jw1Awr5tMOq/XqzVdf1Mr9tZ7701JzZ+ia1ab352z6mc6cGO52hiapWPnlFM0U51ximbqUGu90KSOabTyyCWi5UyYO5/n6pPwDYr8fwvXgN7jAAAAAElFTkSuQmCC" /> <br>
<img id="dtim-1center" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA3BAMAAACiFTSCAAAAMFBMVEUBAQGPjY3Rz89DQ0O0sbHz8vJZJoFwbm6+vLxBHV10MqY6ODifnZ1XV1cdHR3c2trZIbFLAAAACXBIWXMAAAsTAAALEwEAmpwYAAACCElEQVQ4y7VVPWvjQBCVnZXt5JRT1LsQwwnS7YHrgLg6hTBsva4MSeUurX5CqqtTuXCVn6CAf0DgimvvJ4iFredmd2VZcqTYIWSap2V4+/HezMgDSAdgIw0cFhVeMQAPIAuUWfF44jAdOix8k8YsmCNXyEuGynzkQ4u68BE8LbKJsJGFDkvfYe6Lufe5ePi7unywUePY4Suhl4iMCaGlFOWQQGiRhw5Tc3b9MIiZe1Deehhkg3nXu5MirGW5NmnmkNjX9EFs1VKN7djgNg+bovae3a15cpg+ZIfquGP97J2h0viJ5cT6SoZaX+lspYyoX2doFBXjlyiaRtHV+XL5++5u+TiITExXQRR5XGeBQIkoyouNjadQSqm1Tn2pmg8bbe5NelFb0nQMKL25XxO7tsSoxmu/q80XbdXK/eZrs/2iR/OR4a53m1eO8ayPfWDJqHU2VuwywNbVnt6czflbttucVGOkmUbdUI2WSG0g0ZvepuNbiu22IM1NPAZ2uV2x7csnDf3FywFdhQPmttYUT6kkubv5D4+bWlOoFNWa5BRJHiJNDzTDQzUd21fqoaHdPaaO1/kp/d1mwwkdunfsIx2K7Q6lapmDIrVihqAsG41qiZ0tbq7ZFhw6jKsWtHPtz+z128zGT4c3z2cVXs5ujjn2773k9472j5vtf/pkYp2TKe/7E00A/gO7G7pwJRGqtAAAAABJRU5ErkJggg==" />
<img id="dtim-4center" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA3BAMAAACiFTSCAAAAMFBMVEUBAQGbmppqLZZ8fHzRz8+LPMZ0MqXs6upTUVE+G1q5ublubm6iRedQInH9/Pze3NzAiv2WAAAACXBIWXMAAAsTAAALEwEAmpwYAAABuUlEQVQ4y6VVvU7CUBSmCEVQao0vQDrcWWcWhiYkDsICc3OS3pmhSbfiI7C6MfgAJo4sPoCDcSdhMGFDBhI24zn3tBjh2DTy3eEj9+P0/Pa05BtMrZQbzG7KJaZFIyLqrS82hqfOnszXnUquLFv3dvLOeiP57uT69teFfHcP5CKJ+QdyN0YsF5Ul8WpUI06SjmM4Ks0Mrk+Zn86Y70+YS8fha0VY2K8GL1XmwDHXnxj54y1GXgbwwNOBrT2lPNWiyFHIEitrhYdkDaB1ay/vsjY6yiiCJJMVykq0pkt+OPoQH44gGX8IskqtFYjWgODQQAwt8y1aAygPdN8GCkHt5LSoaTH75ylnRT0ON5cEtz4nvL81Dc8nlrm+wmEax9t4YQ/0MNRhn1iHYcuJt+PxOMoib5p8uGpySzAfUxbQKqcsSov9VmlRqSPyOHBR6Q9Sx9haKci3lvsNPGtyv8Hogc3c+nkFl3iwasNBOBz0q8yBk8RJvMRXsE3HrT8YTDK2Zs942kc29I6npbZi5ilZjZg/DpaHL++WqNDiypfzt+LfO3VTaO39c6dmsp9nPZJ9Z18i19r8+hJ9A3EAErhB3eXkAAAAAElFTkSuQmCC" /> <br>
</div>
</div>
</div>
</div>
<div class="dtim-ok">
<button class="primaryButton" id="dti-ok">Download</button>
</div>
<div class="dtim-newtab">
<button class="primaryButton" id="dti-newtab">Open in new tab</button>
</div>
<div class="dtim-cancel">
<button class="primaryButton" id="dti-cancel">Cancel</button>
</div>
</div>
</dialog>`)
let downloadTiledImageDialog = document.getElementById("download-tiled-image-dialog")
let dtim1_width = document.getElementById("dtim1-width")
let dtim1_height = document.getElementById("dtim1-height")
let dtim2_width = document.getElementById("dtim2-width")
let dtim2_height = document.getElementById("dtim2-height")
let dtim2_unit = document.getElementById("dtim2-unit")
let dtim2_dpi = document.getElementById("dtim2-dpi")
let tabTiledTilesOptions = document.getElementById("tab-image-tiles")
let tabTiledSizeOptions = document.getElementById("tab-image-size")
linkTabContents(tabTiledTilesOptions)
linkTabContents(tabTiledSizeOptions)
prettifyInputs(downloadTiledImageDialog)
// ---- Predefined image dimensions
PAPERSIZE.forEach( function(p) {
document.getElementById("dtim2-" + p.id).addEventListener("click", (e) => {
dtim2_unit.value = p.unit
dtim2_width.value = p.width
dtim2_height.value = p.height
})
})
// ---- Close popup
document.getElementById("dti-cancel").addEventListener("click", (e) => downloadTiledImageDialog.close())
downloadTiledImageDialog.addEventListener('click', function (event) {
var rect = downloadTiledImageDialog.getBoundingClientRect();
var isInDialog=(rect.top <= event.clientY && event.clientY <= rect.top + rect.height
&& rect.left <= event.clientX && event.clientX <= rect.left + rect.width);
if (!isInDialog) {
downloadTiledImageDialog.close();
}
});
// ---- Stylesheet
const styleSheet = document.createElement("style")
styleSheet.textContent = `
dialog {
background: var(--background-color2);
color: var(--text-color);
border-radius: 7px;
border: 1px solid var(--background-color3);
}
dialog::backdrop {
background: rgba(0, 0, 0, 0.5);
}
button[disabled] {
opacity: 0.5;
}
.method-2-dpi {
margin-top: 1em;
margin-bottom: 1em;
}
.method-2-paper button {
width: 10em;
padding: 4px;
margin: 4px;
}
.download-tiled-image .tab-content {
background: var(--background-color1);
border-radius: 3pt;
}
.dtim-container { display: grid;
grid-template-columns: auto auto;
grid-template-rows: auto auto;
gap: 1em 0px;
grid-auto-flow: row;
grid-template-areas:
"dtim-tab dtim-tab dtim-plc"
"dtim-ok dtim-newtab dtim-cancel";
}
.download-tiled-image-top {
justify-self: center;
grid-area: dtim-tab;
}
.download-tiled-image-placement {
justify-self: center;
grid-area: dtim-plc;
margin-left: 1em;
}
.dtim-ok {
justify-self: center;
align-self: start;
grid-area: dtim-ok;
}
.dtim-newtab {
justify-self: center;
align-self: start;
grid-area: dtim-newtab;
}
.dtim-cancel {
justify-self: center;
align-self: start;
grid-area: dtim-cancel;
}
#tab-content-image-placement img {
margin: 4px;
opacity: 0.3;
border: solid 2px var(--background-color1);
}
#tab-content-image-placement img:hover {
margin: 4px;
opacity: 1;
border: solid 2px var(--accent-color);
filter: brightness(2);
}
#tab-content-image-placement img.active {
margin: 4px;
opacity: 1;
border: solid 2px var(--background-color1);
}
`
document.head.appendChild(styleSheet)
// ---- Placement widget
function updatePlacementWidget(event) {
document.querySelector("#tab-content-image-placement img.active").classList.remove("active")
event.target.classList.add("active")
}
document.querySelectorAll("#tab-content-image-placement img").forEach(
(i) => i.addEventListener("click", updatePlacementWidget)
)
function getPlacement() {
return document.querySelector("#tab-content-image-placement img.active").id.substr(5)
}
// ---- Make the image
function downloadTiledImage(image, width, height, offsetX=0, offsetY=0, new_tab=false) {
const canvas = document.createElement('canvas')
canvas.width = width
canvas.height = height
const context = canvas.getContext('2d')
const w = image.width
const h = image.height
for (var x = offsetX; x < width; x += w) {
for (var y = offsetY; y < height; y += h) {
context.drawImage(image, x, y, w, h)
}
}
if (new_tab) {
var newTab = window.open("")
newTab.document.write(`<html><head><title>${width}×${height}, "${image.dataset["prompt"]}"</title></head><body><img src="${canvas.toDataURL()}"></body></html>`)
} else {
const link = document.createElement('a')
link.href = canvas.toDataURL()
link.download = image.dataset["prompt"].replace(/[^a-zA-Z0-9]+/g, "-").substr(0,22)+crypto.randomUUID()+".png"
link.click()
}
}
function onDownloadTiledImageClick(e, newtab=false) {
var width, height, offsetX, offsetY
if (isTabActive(tabTiledTilesOptions)) {
width = thisImage.width * dtim1_width.value
height = thisImage.height * dtim1_height.value
} else {
if ( dtim2_unit.value == "pixels" ) {
width = dtim2_width.value
height= dtim2_height.value
} else if ( dtim2_unit.value == "mm" ) {
width = Math.floor( dtim2_width.value * dtim2_dpi.value / 25.4 )
height = Math.floor( dtim2_height.value * dtim2_dpi.value / 25.4 )
} else { // inch
width = Math.floor( dtim2_width.value * dtim2_dpi.value )
height = Math.floor( dtim2_height.value * dtim2_dpi.value )
}
}
var placement = getPlacement()
if (placement == "1tl") {
offsetX = 0
offsetY = 0
} else if (placement == "1tr") {
offsetX = width - thisImage.width * Math.ceil( width / thisImage.width )
offsetY = 0
} else if (placement == "1bl") {
offsetX = 0
offsetY = height - thisImage.height * Math.ceil( height / thisImage.height )
} else if (placement == "1br") {
offsetX = width - thisImage.width * Math.ceil( width / thisImage.width )
offsetY = height - thisImage.height * Math.ceil( height / thisImage.height )
} else if (placement == "4center") {
offsetX = width/2 - thisImage.width * Math.ceil( width/2 / thisImage.width )
offsetY = height/2 - thisImage.height * Math.ceil( height/2 / thisImage.height )
} else if (placement == "1center") {
offsetX = width/2 - thisImage.width/2 - thisImage.width * Math.ceil( (width/2 - thisImage.width/2) / thisImage.width )
offsetY = height/2 - thisImage.height/2 - thisImage.height * Math.ceil( (height/2 - thisImage.height/2) / thisImage.height )
}
downloadTiledImage(thisImage, width, height, offsetX, offsetY, newtab)
downloadTiledImageDialog.close()
}
document.getElementById("dti-ok").addEventListener("click", onDownloadTiledImageClick)
document.getElementById("dti-newtab").addEventListener("click", (e) => onDownloadTiledImageClick(e,true))
})()