Repository: AUTOMATIC1111/stable-diffusion-webui

Issue:
Long txt2img prompts cause a crash due to file name length
**Describe the bug**

```
Arguments: ('ornate intricate filigree framed, elf wearing ornate intricate detailed carved stained glass (((armor))), determined face, ((perfect face)), heavy makeup, led runes, inky swirling mist, gemstones, ((magic mist background)), ((eyeshadow)), (angry), detailed, intricate,(Alphonse Mucha), (Charlie Bowater), (Daniel Ridgway Knight), (Albert Lynch), (Richard S. Johnson)\nNegative prompt: ugly, fat, obese, chubby, (((deformed))), [blurry], bad anatomy, disfigured, poorly drawn face, mutation, mutated, (extra_limb), (ugly), (poorly drawn hands), messy drawing, large_breasts, penis, nose, eyes, lips, eyelashes, text, red_eyes', '', 'None', 'None', 20, 0, False, False, 1, 1, 7, -1.0, -1.0, 0, 0, 0, 512, 384, 0, False, 1, '', 4, '', True, False, None, '') {}
Traceback (most recent call last):
  File "/home/gaz/src/ai/stable-diffusion-webui/modules/ui.py", line 134, in f
    res = list(func(*args, **kwargs))
  File "/home/gaz/src/ai/stable-diffusion-webui/webui.py", line 55, in f
    res = func(*args, **kwargs)
  File "/home/gaz/src/ai/stable-diffusion-webui/modules/txt2img.py", line 39, in txt2img
    processed = process_images(p)
  File "/home/gaz/src/ai/stable-diffusion-webui/modules/processing.py", line 383, in process_images
    images.save_image(image, p.outpath_samples, "", seeds[i], prompts[i], opts.samples_format, info=infotext(n, i), p=p)
  File "/home/gaz/src/ai/stable-diffusion-webui/modules/images.py", line 360, in save_image
    image.save(fullfn, quality=opts.jpeg_quality, pnginfo=pnginfo)
  File "/home/gaz/src/ai/stable-diffusion-webui/venv/lib/python3.10/site-packages/PIL/Image.py", line 2317, in save
    fp = builtins.open(filename, "w+b")
OSError: [Errno 36] File name too long: 'outputs/txt2img-images/00002-1278808392-ornate intricate filigree framed, elf wearing ornate intricate detailed carved stained glass (((armor))), determined face, ((per.png'
```

**To Reproduce**

I accidentally pasted this whole thing in, including the text "negative prompt" and all the stuff after it:

> ornate intricate filigree framed, elf wearing ornate intricate detailed carved stained glass (((armor))), determined face, ((perfect face)), heavy makeup, led runes, inky swirling mist, gemstones, ((magic mist background)), ((eyeshadow)), (angry), detailed, intricate,(Alphonse Mucha), (Charlie Bowater), (Daniel Ridgway Knight), (Albert Lynch), (Richard S. Johnson)
Negative prompt: ugly, fat, obese, chubby, (((deformed))), [blurry], bad anatomy, disfigured, poorly drawn face, mutation, mutated, (extra_limb), (ugly), (poorly drawn hands), messy drawing, large_breasts, penis, nose, eyes, lips, eyelashes, text, red_eyes

It crashed with the stack trace above.

**Expected behavior**
https://discord.com/channels/1002292111942635562/1010578380300763146/1020288439880519762

**Desktop (please complete the following information):**
 - OS: Linux
 - Browser [e.g. chrome, safari]
 - Commit revision: 8a32a71ca3223cf7b0911fe55db2c6dece2bacca



Discussion:
I think this was addressed in ae32b8a53fd52cb4be195c4b4c329dd81a63ada7 - could you check please?
Yeah I guess that would fix it in most cases.

Adding in something like this would be safer and stop this same bug from popping up in other areas:

    def truncate_filename(full_path: str, hash_chars: int=5):
        """
        Truncates a filename so it fits on the target filesystem, ensuring that
        two files can't collide if they have different endings but the same start.

        full_path: full path to the file, must have both a path and filename component.
        hash_chars: number of characters to use for the hash part. each one gives 
            6 bits of collision resistance. defaults to 5, 30-bit; 0.05% chance for 1,000 files 
        """
        
        containing_dir, full_name = os.path.split(full_path)
        file_name, file_ext = os.path.splitext(full_name)
        
        # the max length depends on the containing dir's filesystem
        max_length = os.statvfs(containing_dir).f_namemax
        
        if len(full_name) <= max_length:
            return full_path
        
        name_hash = base64.urlsafe_b64encode(hashlib.md5(file_name.encode()).digest()).decode()
        new_name = file_name[:max_length - len(file_ext) - hash_chars] + name_hash[:hash_chars]
        
        return new_name + file_ext


It might be worth doing the same thing elsewhere; have `truncate_string(full_string, max_length, hash_chars)` that returns things like `word list too longHzql1` instead of having a max number of words. I like the trick and use it a lot when making caches of downloaded URLs

Resolving patch:
diff --git a/modules/images.py b/modules/images.py
--- a/modules/images.py
+++ b/modules/images.py
@@ -416,6 +416,14 @@ def get_next_sequence_number(path, basename):
     return result + 1
 
 
+def truncate_fullpath(full_path, encoding='utf-8'):
+    dir_name, full_name = os.path.split(full_path)
+    file_name, file_ext = os.path.splitext(full_name)
+    max_length = os.statvfs(dir_name).f_namemax
+    file_name_truncated = file_name.encode(encoding)[:max_length - len(file_ext)].decode(encoding, 'ignore')
+    return os.path.join(dir_name , file_name_truncated + file_ext)
+
+
 def save_image(image, path, basename, seed=None, prompt=None, extension='png', info=None, short_filename=False, no_prompt=False, grid=False, pnginfo_section_name='parameters', p=None, existing_info=None, forced_filename=None, suffix="", save_to_dirs=None):
     """Save an image.
 
@@ -456,7 +464,7 @@ def save_image(image, path, basename, seed=None, prompt=None, extension='png', i
 
     if save_to_dirs:
         dirname = namegen.apply(opts.directories_filename_pattern or "[prompt_words]").lstrip(' ').rstrip('\\ /')
-        path = os.path.join(path, dirname)
+        path = truncate_fullpath(os.path.join(path, dirname))
 
     os.makedirs(path, exist_ok=True)
 
@@ -480,13 +488,13 @@ def save_image(image, path, basename, seed=None, prompt=None, extension='png', i
             fullfn = None
             for i in range(500):
                 fn = f"{basecount + i:05}" if basename == '' else f"{basename}-{basecount + i:04}"
-                fullfn = os.path.join(path, f"{fn}{file_decoration}.{extension}")
+                fullfn = truncate_fullpath(os.path.join(path, f"{fn}{file_decoration}.{extension}"))
                 if not os.path.exists(fullfn):
                     break
         else:
-            fullfn = os.path.join(path, f"{file_decoration}.{extension}")
+            fullfn = truncate_fullpath(os.path.join(path, f"{file_decoration}.{extension}"))
     else:
-        fullfn = os.path.join(path, f"{forced_filename}.{extension}")
+        fullfn = truncate_fullpath(os.path.join(path, f"{forced_filename}.{extension}"))
 
     pnginfo = existing_info or {}
     if info is not None:


Test patch: