Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
19 changes: 17 additions & 2 deletions folder_paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,11 @@ def map_filename(filename: str) -> tuple[int, str]:
prefix_len = len(os.path.basename(filename_prefix))
prefix = filename[:prefix_len + 1]
try:
digits = int(filename[prefix_len + 1:].split('_')[0])
# Extract the part after prefix (e.g., "00001_.png" or "00001.png")
remainder = filename[prefix_len + 1:]
# Try to parse digits - handle both "00001_" and "00001." formats
digits_str = remainder.split('_')[0].split('.')[0]
digits = int(digits_str)
except:
digits = 0
return digits, prefix
Expand Down Expand Up @@ -464,7 +468,18 @@ def compute_vars(input: str, image_width: int, image_height: int) -> str:
raise Exception(err)

try:
counter = max(filter(lambda a: os.path.normcase(a[1][:-1]) == os.path.normcase(filename) and a[1][-1] == "_", map(map_filename, os.listdir(full_output_folder))))[0] + 1
# Support both old format (filename_) and new format (filename) for backward compatibility
def matches_filename(a):
prefix = a[1]
# Match new format: "filename" (exact match)
if os.path.normcase(prefix) == os.path.normcase(filename):
return True
# Match old format: "filename_" (with trailing underscore)
if prefix.endswith("_") and os.path.normcase(prefix[:-1]) == os.path.normcase(filename):
return True
return False

counter = max(filter(lambda a: matches_filename(a), map(map_filename, os.listdir(full_output_folder))))[0] + 1
except ValueError:
counter = 1
except FileNotFoundError:
Expand Down
4 changes: 2 additions & 2 deletions nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,7 @@ def save(self, samples, filename_prefix="ComfyUI", prompt=None, extra_pnginfo=No
for x in extra_pnginfo:
metadata[x] = json.dumps(extra_pnginfo[x])

file = f"{filename}_{counter:05}_.latent"
file = f"{filename}_{counter:05}.latent"

results: list[FileLocator] = []
results.append({
Expand Down Expand Up @@ -1615,7 +1615,7 @@ def save_images(self, images, filename_prefix="ComfyUI", prompt=None, extra_pngi
metadata.add_text(x, json.dumps(extra_pnginfo[x]))

filename_with_batch_num = filename.replace("%batch_num%", str(batch_number))
file = f"{filename_with_batch_num}_{counter:05}_.png"
file = f"{filename_with_batch_num}_{counter:05}.png"
img.save(os.path.join(full_output_folder, file), pnginfo=metadata, compress_level=self.compress_level)
results.append({
"filename": file,
Expand Down
91 changes: 91 additions & 0 deletions tests-unit/folder_paths_test/test_filename_format.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"""Tests for output filename format.

Relates to issue #1389: Trailing underscore in output file names
"""

import os
import tempfile
import pytest

import folder_paths


class TestFilenameFormat:
"""Test output filename format without trailing underscore."""

def test_new_filename_format_no_trailing_underscore(self):
"""New files should not have trailing underscore before extension."""
# Expected format: "prefix_00001.png" not "prefix_00001_.png"
filename = "ComfyUI"
counter = 1
new_format = f"{filename}_{counter:05}.png"

assert new_format == "ComfyUI_00001.png"
assert not new_format.endswith("_.png"), "Filename should not have trailing underscore"

def test_get_save_image_path_backward_compatible(self):
"""get_save_image_path should work with both old and new format files."""
with tempfile.TemporaryDirectory() as tmpdir:
# Create files with old format (trailing underscore)
old_format_files = [
"TestPrefix_00001_.png",
"TestPrefix_00002_.png",
]
for f in old_format_files:
open(os.path.join(tmpdir, f), 'w').close()

# get_save_image_path should recognize old format and return next counter
full_path, filename, counter, subfolder, _ = folder_paths.get_save_image_path(
"TestPrefix", tmpdir
)

# Counter should be 3 (after 00001 and 00002)
assert counter == 3

def test_get_save_image_path_new_format(self):
"""get_save_image_path should work with new format files (no trailing underscore)."""
with tempfile.TemporaryDirectory() as tmpdir:
# Create files with new format (no trailing underscore)
new_format_files = [
"NewPrefix_00001.png",
"NewPrefix_00002.png",
"NewPrefix_00003.png",
]
for f in new_format_files:
open(os.path.join(tmpdir, f), 'w').close()

# get_save_image_path should recognize new format
full_path, filename, counter, subfolder, _ = folder_paths.get_save_image_path(
"NewPrefix", tmpdir
)

# Counter should be 4 (after 00001, 00002, 00003)
assert counter == 4

def test_get_save_image_path_mixed_formats(self):
"""get_save_image_path should handle mixed old and new format files."""
with tempfile.TemporaryDirectory() as tmpdir:
# Mix of old and new format files
files = [
"MixedPrefix_00001_.png", # old format
"MixedPrefix_00002.png", # new format
"MixedPrefix_00003_.png", # old format
]
for f in files:
open(os.path.join(tmpdir, f), 'w').close()

full_path, filename, counter, subfolder, _ = folder_paths.get_save_image_path(
"MixedPrefix", tmpdir
)

# Counter should be 4 (recognizing both formats)
assert counter == 4

def test_get_save_image_path_empty_directory(self):
"""get_save_image_path should return counter 1 for empty directory."""
with tempfile.TemporaryDirectory() as tmpdir:
full_path, filename, counter, subfolder, _ = folder_paths.get_save_image_path(
"EmptyDir", tmpdir
)

assert counter == 1