1. Fixed DCI Structure Preview node registration in __init__.py 2. Simplified DCIStructureNode interface by removing all optional parameters 3. Node now defaults to showing all detailed information including tree structure 4. Updated test files to match simplified interface requirements 5. Ensured proper text output display in ComfyUI interface for preview nodes This resolves the issue where DCI Structure Preview node was not visible in ComfyUI interface and simplifies the user experience by removing configuration complexity while providing comprehensive DCI file analysis. fix: 修复DCI结构预览节点注册并简化界面 1. 修复了__init__.py中DCI结构预览节点的注册问题 2. 简化了DCIStructureNode界面,移除了所有可选参数 3. 节点现在默认显示所有详细信息,包括树状结构 4. 更新了测试文件以匹配简化的界面要求 5. 确保在ComfyUI界面中正确显示预览节点的文本输出 这解决了DCI结构预览节点在ComfyUI界面中不可见的问题,并通过 移除配置复杂性简化了用户体验,同时提供全面的DCI文件分析。
522 lines
19 KiB
Python
522 lines
19 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
测试DCI结构预览节点的功能
|
|
验证树状结构显示和元信息解析
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
from PIL import Image, ImageDraw
|
|
|
|
# 添加项目路径
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'py'))
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'py', 'nodes'))
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'py', 'utils'))
|
|
|
|
from dci_format import DCIIconBuilder
|
|
from dci_reader import DCIReader
|
|
|
|
# 直接导入节点类,避免相对导入问题
|
|
import sys
|
|
import os
|
|
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
nodes_dir = os.path.join(current_dir, '..', 'py', 'nodes')
|
|
sys.path.insert(0, nodes_dir)
|
|
|
|
from base_node import BaseNode
|
|
|
|
def create_test_dci_data():
|
|
"""创建测试用的DCI二进制数据"""
|
|
builder = DCIIconBuilder()
|
|
|
|
# 添加不同尺寸、状态、色调的图像
|
|
test_cases = [
|
|
# 简单图层
|
|
(64, "normal", "light", 1, "webp", (255, 100, 100, 255)),
|
|
(64, "normal", "dark", 1, "webp", (100, 100, 255, 255)),
|
|
(64, "hover", "light", 1, "png", (100, 255, 100, 255)),
|
|
(64, "pressed", "light", 1, "webp", (255, 255, 100, 255)),
|
|
|
|
# 多缩放比例
|
|
(128, "normal", "light", 1, "webp", (255, 150, 150, 255)),
|
|
(128, "normal", "light", 2, "webp", (255, 150, 150, 255)),
|
|
(128, "normal", "light", 3, "webp", (255, 150, 150, 255)),
|
|
|
|
# 不同状态
|
|
(128, "disabled", "light", 1, "webp", (150, 150, 150, 255)),
|
|
(128, "hover", "dark", 1, "webp", (150, 150, 255, 255)),
|
|
|
|
# 大尺寸图标
|
|
(256, "normal", "light", 1, "webp", (255, 200, 200, 255)),
|
|
(256, "normal", "dark", 1, "webp", (200, 200, 255, 255)),
|
|
]
|
|
|
|
for size, state, tone, scale, format_ext, color in test_cases:
|
|
image = create_test_image(size, color, f"{size}px")
|
|
builder.add_icon_image(image, size, state, tone, scale, format_ext)
|
|
|
|
return builder.to_binary()
|
|
|
|
class DCIStructureNode(BaseNode):
|
|
"""ComfyUI node for displaying DCI file internal structure in tree format"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"dci_binary_data": ("BINARY_DATA",),
|
|
},
|
|
"optional": {
|
|
"show_file_details": ("BOOLEAN", {"default": True}),
|
|
"show_layer_metadata": ("BOOLEAN", {"default": True}),
|
|
"show_file_sizes": ("BOOLEAN", {"default": True}),
|
|
"compact_mode": ("BOOLEAN", {"default": False}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ()
|
|
RETURN_NAMES = ()
|
|
FUNCTION = "execute"
|
|
CATEGORY = "DCI/Analysis"
|
|
OUTPUT_NODE = True
|
|
|
|
def _execute(self, dci_binary_data, show_file_details=True, show_layer_metadata=True, show_file_sizes=True, compact_mode=False):
|
|
"""Display DCI file internal structure in tree format"""
|
|
|
|
# Use binary data
|
|
reader = DCIReader(binary_data=dci_binary_data)
|
|
|
|
# Read DCI data
|
|
if not reader.read():
|
|
return {"ui": {"text": ["Failed to read DCI data"]}}
|
|
|
|
# Extract images
|
|
images = reader.get_icon_images()
|
|
if not images:
|
|
return {"ui": {"text": ["No images found in DCI file"]}}
|
|
|
|
# Generate tree structure
|
|
tree_structure = self._generate_tree_structure(images, show_file_details, show_layer_metadata, show_file_sizes, compact_mode)
|
|
|
|
# Generate summary statistics
|
|
summary = self._generate_summary_statistics(images)
|
|
|
|
# Combine structure and summary
|
|
full_output = f"{summary}\n\n{tree_structure}"
|
|
|
|
return {"ui": {"text": [full_output]}}
|
|
|
|
def _generate_tree_structure(self, images, show_file_details, show_layer_metadata, show_file_sizes, compact_mode):
|
|
"""Generate tree structure representation of DCI file"""
|
|
|
|
# Organize images by directory structure
|
|
structure = {}
|
|
|
|
for img in images:
|
|
# Parse path: size/state.tone/scale/filename
|
|
path_parts = img['path'].split('/')
|
|
if len(path_parts) >= 3:
|
|
size = path_parts[0]
|
|
state_tone = path_parts[1]
|
|
scale = path_parts[2]
|
|
|
|
# Initialize nested structure
|
|
if size not in structure:
|
|
structure[size] = {}
|
|
if state_tone not in structure[size]:
|
|
structure[size][state_tone] = {}
|
|
if scale not in structure[size][state_tone]:
|
|
structure[size][state_tone][scale] = []
|
|
|
|
# Add file info
|
|
file_info = {
|
|
'filename': img['filename'],
|
|
'file_size': img['file_size'],
|
|
'format': img['format'],
|
|
'image': img
|
|
}
|
|
|
|
# Parse layer metadata from filename if requested
|
|
if show_layer_metadata:
|
|
file_info['layer_metadata'] = self._parse_layer_metadata(img['filename'])
|
|
|
|
structure[size][state_tone][scale].append(file_info)
|
|
|
|
# Generate tree text
|
|
lines = []
|
|
lines.append("DCI File Structure:")
|
|
lines.append("├─ (Size/State.Tone/Scale/Layer)")
|
|
|
|
# Sort sizes numerically
|
|
sorted_sizes = sorted(structure.keys(), key=lambda x: int(x) if x.isdigit() else float('inf'))
|
|
|
|
for i, size in enumerate(sorted_sizes):
|
|
is_last_size = (i == len(sorted_sizes) - 1)
|
|
size_prefix = "└── " if is_last_size else "├── "
|
|
lines.append(f"{size_prefix}{size}")
|
|
|
|
# Sort state.tone combinations
|
|
sorted_states = sorted(structure[size].keys())
|
|
|
|
for j, state_tone in enumerate(sorted_states):
|
|
is_last_state = (j == len(sorted_states) - 1)
|
|
state_indent = " " if is_last_size else "│ "
|
|
state_prefix = "└── " if is_last_state else "├── "
|
|
|
|
# Parse state and tone for display
|
|
if '.' in state_tone:
|
|
state, tone = state_tone.split('.', 1)
|
|
display_name = f"{state}.{tone}"
|
|
else:
|
|
display_name = state_tone
|
|
|
|
lines.append(f"{state_indent}{state_prefix}{display_name}")
|
|
|
|
# Sort scales
|
|
sorted_scales = sorted(structure[size][state_tone].keys(), key=lambda x: float(x) if x.replace('.', '').isdigit() else float('inf'))
|
|
|
|
for k, scale in enumerate(sorted_scales):
|
|
is_last_scale = (k == len(sorted_scales) - 1)
|
|
scale_indent = state_indent + (" " if is_last_state else "│ ")
|
|
scale_prefix = "└── " if is_last_scale else "├── "
|
|
|
|
lines.append(f"{scale_indent}{scale_prefix}{scale}")
|
|
|
|
# Sort files
|
|
files = structure[size][state_tone][scale]
|
|
sorted_files = sorted(files, key=lambda x: x['filename'])
|
|
|
|
for l, file_info in enumerate(sorted_files):
|
|
is_last_file = (l == len(sorted_files) - 1)
|
|
file_indent = scale_indent + (" " if is_last_scale else "│ ")
|
|
file_prefix = "└── " if is_last_file else "├── "
|
|
|
|
# Build file display name
|
|
filename = file_info['filename']
|
|
file_display = filename
|
|
|
|
if show_file_sizes:
|
|
size_str = self._format_file_size(file_info['file_size'])
|
|
file_display += f" ({size_str})"
|
|
|
|
lines.append(f"{file_indent}{file_prefix}{file_display}")
|
|
|
|
# Add layer metadata if requested and not in compact mode
|
|
if show_layer_metadata and not compact_mode and 'layer_metadata' in file_info:
|
|
metadata = file_info['layer_metadata']
|
|
if metadata:
|
|
metadata_indent = file_indent + (" " if is_last_file else "│ ")
|
|
for meta_line in metadata:
|
|
lines.append(f"{metadata_indent} {meta_line}")
|
|
|
|
return "\n".join(lines)
|
|
|
|
def _parse_layer_metadata(self, filename):
|
|
"""Parse layer metadata from DCI filename according to specification"""
|
|
metadata = []
|
|
|
|
# Remove file extension
|
|
name_without_ext = filename
|
|
if '.' in filename:
|
|
# Handle special cases like .webp.alpha8
|
|
if filename.endswith('.alpha8'):
|
|
# Remove .alpha8 first
|
|
name_without_ext = filename[:-8]
|
|
metadata.append("🔍 Alpha8 format (调色板优化)")
|
|
# Then remove the base format
|
|
if '.' in name_without_ext:
|
|
name_without_ext = name_without_ext.rsplit('.', 1)[0]
|
|
else:
|
|
name_without_ext = filename.rsplit('.', 1)[0]
|
|
|
|
# Parse filename format: priority.padding.palette.color_adjustments
|
|
# Example: 2.5p.0.10_20_30_-10_15_-5_25
|
|
parts = name_without_ext.split('.')
|
|
|
|
if len(parts) >= 1:
|
|
# Priority (always present)
|
|
try:
|
|
priority = int(parts[0])
|
|
metadata.append(f"📊 优先级: {priority}")
|
|
except ValueError:
|
|
pass
|
|
|
|
if len(parts) >= 2:
|
|
# Padding (format: Np where N is number)
|
|
padding_str = parts[1]
|
|
if padding_str.endswith('p'):
|
|
try:
|
|
padding = int(padding_str[:-1])
|
|
if padding > 0:
|
|
metadata.append(f"📐 外边框: {padding}px")
|
|
except ValueError:
|
|
pass
|
|
|
|
if len(parts) >= 3:
|
|
# Palette
|
|
try:
|
|
palette = int(parts[2])
|
|
palette_names = {
|
|
-1: "无调色板",
|
|
0: "前景色",
|
|
1: "背景色",
|
|
2: "高亮前景色",
|
|
3: "高亮色"
|
|
}
|
|
palette_name = palette_names.get(palette, f"调色板{palette}")
|
|
if palette != -1:
|
|
metadata.append(f"🎨 调色板: {palette_name}")
|
|
except ValueError:
|
|
pass
|
|
|
|
if len(parts) >= 4:
|
|
# Color adjustments: hue_saturation_lightness_red_green_blue_alpha
|
|
color_str = parts[3]
|
|
if '_' in color_str:
|
|
try:
|
|
adjustments = [int(x) for x in color_str.split('_')]
|
|
if len(adjustments) >= 7:
|
|
hue, sat, light, red, green, blue, alpha = adjustments[:7]
|
|
|
|
# Only show non-zero adjustments
|
|
color_adjustments = []
|
|
if hue != 0:
|
|
color_adjustments.append(f"色调{hue:+d}%")
|
|
if sat != 0:
|
|
color_adjustments.append(f"饱和度{sat:+d}%")
|
|
if light != 0:
|
|
color_adjustments.append(f"亮度{light:+d}%")
|
|
if red != 0:
|
|
color_adjustments.append(f"红{red:+d}%")
|
|
if green != 0:
|
|
color_adjustments.append(f"绿{green:+d}%")
|
|
if blue != 0:
|
|
color_adjustments.append(f"蓝{blue:+d}%")
|
|
if alpha != 0:
|
|
color_adjustments.append(f"透明度{alpha:+d}%")
|
|
|
|
if color_adjustments:
|
|
metadata.append(f"🌈 颜色调整: {', '.join(color_adjustments)}")
|
|
|
|
except ValueError:
|
|
pass
|
|
|
|
return metadata
|
|
|
|
def _format_file_size(self, size_bytes):
|
|
"""Format file size in human readable format"""
|
|
if size_bytes < 1024:
|
|
return f"{size_bytes}B"
|
|
elif size_bytes < 1024 * 1024:
|
|
return f"{size_bytes / 1024:.1f}KB"
|
|
else:
|
|
return f"{size_bytes / (1024 * 1024):.1f}MB"
|
|
|
|
def _generate_summary_statistics(self, images):
|
|
"""Generate summary statistics for the DCI file"""
|
|
total_images = len(images)
|
|
total_size = sum(img['file_size'] for img in images)
|
|
|
|
# Collect unique values
|
|
sizes = sorted(set(int(img['size']) for img in images))
|
|
states = sorted(set(img['state'] for img in images))
|
|
tones = sorted(set(img['tone'] for img in images))
|
|
scales = sorted(set(float(img['scale']) for img in images))
|
|
formats = sorted(set(img['format'] for img in images))
|
|
|
|
# Count by categories
|
|
size_counts = {}
|
|
state_counts = {}
|
|
tone_counts = {}
|
|
|
|
for img in images:
|
|
size = img['size']
|
|
state = img['state']
|
|
tone = img['tone']
|
|
|
|
size_counts[size] = size_counts.get(size, 0) + 1
|
|
state_counts[state] = state_counts.get(state, 0) + 1
|
|
tone_counts[tone] = tone_counts.get(tone, 0) + 1
|
|
|
|
lines = []
|
|
lines.append("📋 DCI File Summary")
|
|
lines.append("=" * 50)
|
|
lines.append(f"📊 总计: {total_images} 个图像文件")
|
|
lines.append(f"💾 总大小: {self._format_file_size(total_size)}")
|
|
lines.append("")
|
|
|
|
lines.append(f"📏 图标尺寸: {', '.join(map(str, sizes))}px")
|
|
lines.append(f"🎭 状态类型: {', '.join(states)}")
|
|
lines.append(f"🌓 色调类型: {', '.join(tones)}")
|
|
lines.append(f"🔍 缩放比例: {', '.join(f'{s:g}x' for s in scales)}")
|
|
lines.append(f"🖼️ 图像格式: {', '.join(formats)}")
|
|
lines.append("")
|
|
|
|
# Detailed breakdown
|
|
lines.append("📈 详细分布:")
|
|
lines.append(f" 尺寸分布: {', '.join(f'{size}px({count})' for size, count in sorted(size_counts.items(), key=lambda x: int(x[0])))}")
|
|
lines.append(f" 状态分布: {', '.join(f'{state}({count})' for state, count in sorted(state_counts.items()))}")
|
|
lines.append(f" 色调分布: {', '.join(f'{tone}({count})' for tone, count in sorted(tone_counts.items()))}")
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
def create_test_image(size=128, color=(255, 0, 0, 255), text="TEST"):
|
|
"""创建测试图像"""
|
|
image = Image.new('RGBA', (size, size), color)
|
|
draw = ImageDraw.Draw(image)
|
|
|
|
# 绘制简单图案
|
|
draw.rectangle([size//4, size//4, 3*size//4, 3*size//4],
|
|
fill=(255, 255, 255, 255), outline=(0, 0, 0, 255), width=2)
|
|
|
|
# 添加文本
|
|
draw.text((size//2 - 20, size//2 - 5), text, fill=(0, 0, 0, 255))
|
|
return image
|
|
|
|
|
|
def test_structure_preview():
|
|
"""测试DCI结构预览节点的基本功能"""
|
|
print("=== 测试DCI结构预览节点 ===")
|
|
|
|
# 创建测试DCI数据
|
|
dci_binary_data = create_test_dci_data()
|
|
print(f"✓ 创建了 {len(dci_binary_data)} 字节的测试DCI数据")
|
|
|
|
# 创建结构预览节点
|
|
structure_node = DCIStructureNode()
|
|
|
|
print("\n=== 测试基本结构预览 ===")
|
|
result = structure_node._execute(dci_binary_data=dci_binary_data)
|
|
|
|
if "ui" in result and "text" in result["ui"]:
|
|
output_text = result["ui"]["text"][0]
|
|
print("结构预览生成成功:")
|
|
print("-" * 60)
|
|
print(output_text)
|
|
print("-" * 60)
|
|
return True
|
|
else:
|
|
print("❌ 结构预览生成失败")
|
|
return False
|
|
|
|
|
|
def test_layer_metadata_parsing():
|
|
"""测试图层元数据解析功能"""
|
|
print("\n=== 测试图层元数据解析 ===")
|
|
|
|
node = DCIStructureNode()
|
|
|
|
# 测试各种文件名格式
|
|
test_filenames = [
|
|
# 简单格式
|
|
("1.webp", ["📊 优先级: 1"]),
|
|
|
|
# 带外边框
|
|
("2.5p.webp", ["📊 优先级: 2", "📐 外边框: 5px"]),
|
|
|
|
# 带调色板
|
|
("1.0p.0.webp", ["📊 优先级: 1", "🎨 调色板: 前景色"]),
|
|
("1.0p.1.webp", ["📊 优先级: 1", "🎨 调色板: 背景色"]),
|
|
("1.0p.2.webp", ["📊 优先级: 1", "🎨 调色板: 高亮前景色"]),
|
|
("1.0p.3.webp", ["📊 优先级: 1", "🎨 调色板: 高亮色"]),
|
|
|
|
# 完整格式
|
|
("2.5p.0.10_20_30_-10_15_-5_25.png", [
|
|
"📊 优先级: 2",
|
|
"📐 外边框: 5px",
|
|
"🎨 调色板: 前景色",
|
|
"🌈 颜色调整: 色调+10%, 饱和度+20%, 亮度+30%, 红-10%, 绿+15%, 蓝-5%, 透明度+25%"
|
|
]),
|
|
|
|
# Alpha8格式
|
|
("3.0p.2.0_0_0_50_-20_10_0.webp.alpha8", [
|
|
"🔍 Alpha8 format (调色板优化)",
|
|
"📊 优先级: 3",
|
|
"🎨 调色板: 高亮前景色",
|
|
"🌈 颜色调整: 亮度+50%, 绿-20%, 蓝+10%"
|
|
]),
|
|
|
|
# 仅部分颜色调整
|
|
("1.0p.-1.0_0_50_0_0_0_0.webp", [
|
|
"📊 优先级: 1",
|
|
"🌈 颜色调整: 亮度+50%"
|
|
]),
|
|
]
|
|
|
|
for filename, expected_parts in test_filenames:
|
|
metadata = node._parse_layer_metadata(filename)
|
|
print(f"\n文件名: {filename}")
|
|
print(f"解析结果: {metadata}")
|
|
|
|
# 验证关键元素是否存在
|
|
metadata_text = " ".join(metadata)
|
|
all_found = True
|
|
for expected in expected_parts:
|
|
if expected not in metadata_text:
|
|
print(f"❌ 缺少期望的元数据: {expected}")
|
|
all_found = False
|
|
|
|
if all_found:
|
|
print("✓ 元数据解析正确")
|
|
else:
|
|
print("❌ 元数据解析有误")
|
|
|
|
return True
|
|
|
|
|
|
def test_file_size_formatting():
|
|
"""测试文件大小格式化"""
|
|
print("\n=== 测试文件大小格式化 ===")
|
|
|
|
node = DCIStructureNode()
|
|
|
|
test_cases = [
|
|
(512, "512B"),
|
|
(1024, "1.0KB"),
|
|
(1536, "1.5KB"),
|
|
(1048576, "1.0MB"),
|
|
(1572864, "1.5MB"),
|
|
]
|
|
|
|
for size_bytes, expected in test_cases:
|
|
result = node._format_file_size(size_bytes)
|
|
print(f"{size_bytes} bytes -> {result} (期望: {expected})")
|
|
if result == expected:
|
|
print("✓ 格式化正确")
|
|
else:
|
|
print("❌ 格式化错误")
|
|
|
|
return True
|
|
|
|
|
|
if __name__ == "__main__":
|
|
print("开始DCI结构预览节点测试...")
|
|
|
|
success = True
|
|
|
|
try:
|
|
success &= test_structure_preview()
|
|
success &= test_layer_metadata_parsing()
|
|
success &= test_file_size_formatting()
|
|
|
|
if success:
|
|
print("\n🎉 所有测试通过!DCI结构预览节点功能正常。")
|
|
else:
|
|
print("\n❌ 部分测试失败,请检查实现。")
|
|
|
|
except Exception as e:
|
|
print(f"\n💥 测试过程中发生错误: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
success = False
|
|
|
|
print("\n功能特性总结:")
|
|
print("1. ✅ 树状结构显示DCI文件内部组织")
|
|
print("2. ✅ 详细的统计信息和分布分析")
|
|
print("3. ✅ 智能解析图层文件名中的元数据")
|
|
print("4. ✅ 支持Alpha8格式识别")
|
|
print("5. ✅ 可配置的显示选项(紧凑模式、隐藏元数据等)")
|
|
print("6. ✅ 人性化的文件大小显示")
|
|
print("7. ✅ 完整的DCI规范支持")
|