from PIL import Image, ImageDraw, ImageFont
import numpy as np
import os
import glob
import time
import re
def hex_to_rgb(hex_color):
"""将16进制颜色字符串转换为RGB元组
Args:
hex_color: 16进制颜色字符串,格式为 "#RRGGBB" 或 "RRGGBB"
Returns:
tuple: RGB颜色元组 (R, G, B)
Raises:
ValueError: 如果颜色格式不正确
"""
if hex_color is None:
return None
hex_color = hex_color.lstrip('#')
if not re.match(r'^[0-9A-Fa-f]{6}$', hex_color):
raise ValueError(f"无效的16进制颜色格式: {hex_color},应为6位16进制数")
r = int(hex_color[0:2], 16)
g = int(hex_color[2:4], 16)
b = int(hex_color[4:6], 16)
return (r, g, b)
def arrange_icons_layout_1(canvas, icon_folder='icon', start_y=1000):
"""布局1:左右两组,各6个图标"""
step_start_time = time.time()
try:
icon_files = glob.glob(os.path.join(icon_folder, '*.png'))
if not icon_files:
print("没有找到图标文件")
return
print(f"[信息] 找到 {len(icon_files)} 个图标文件")
icons = []
load_start_time = time.time()
specified_icons = [
"com_autopai_smart_sound_effect.png",
"com_incall_apps_personalcenter.png",
"com_wt_carcamera.png",
"com_wt_airconditioner.png",
"com_wt_vehiclecenter.png",
"com_autopai_car_dialer.png",
"com_tinnove_mediacenter.png",
"com_tinnove_scenemode.png",
"com_tinnove_scenemode.png",
"com_wtcl_electronicdirections.png",
"com_wt_scene.png",
"com_tinnove_gamezone.png",
]
app_names = [
"音效", "个人中心", "空调", "车辆中心", "电话", "AI空间",
"情景模式", "导航", "场景", "游戏", "空调2", "AI空间2"
]
found_icons = []
for icon_name in specified_icons:
icon_path = os.path.join(icon_folder, icon_name)
if os.path.exists(icon_path):
found_icons.append(icon_path)
else:
print(f"[失败] 错误: 找不到图标文件 {icon_name}")
return
if len(found_icons) < 12:
remaining_icons = [f for f in icon_files if f not in found_icons]
found_icons.extend(remaining_icons[:12-len(found_icons)])
print(f"[警告] 指定的图标不足12个,补充了 {12-len(found_icons)} 个其他图标")
canvas_width, canvas_height = canvas.size
icon_width = int(canvas_width * 3.9 / 100)
icon_height = int(canvas_height * 8.33 / 100)
for i, icon_file in enumerate(found_icons[:12]):
try:
icon = Image.open(icon_file)
icon = icon.resize((icon_width, icon_height), Image.Resampling.LANCZOS)
app_name = app_names[i] if i < len(app_names) else f"应用{i+1}"
icons.append((icon, app_name))
except Exception as e:
print(f"[警告] 加载图标失败 {icon_file}: {e}")
load_time = (time.time() - load_start_time) * 1000
print(f"[成功] 图标加载完成 (耗时: {load_time:.2f}ms)")
if not icons:
print("没有成功加载任何图标")
return
canvas_width, canvas_height = canvas.size
icon_width_percent = 3.9
icon_height_percent = 8.33
icon_spacing_percent = 3.125
left_group_start_percent = 5.47
group_spacing_percent = 10.84
icon_width = int(canvas_width * icon_width_percent / 100)
icon_height = int(canvas_height * icon_height_percent / 100)
icon_spacing = int(canvas_width * icon_spacing_percent / 100)
left_group_start = int(canvas_width * left_group_start_percent / 100)
group_spacing = int(canvas_width * group_spacing_percent / 100)
right_group_start = left_group_start + 6 * (icon_width + icon_spacing) - icon_spacing + group_spacing
print(f"画布尺寸: {canvas_width}x{canvas_height}")
print(f"图标尺寸: {icon_width}x{icon_height} (宽度{icon_width_percent}%, 高度{icon_height_percent}%)")
print(f"图标间距: {icon_spacing}px ({icon_spacing_percent}%)")
print(f"左侧组起始位置: x={left_group_start} ({left_group_start_percent}%)")
print(f"右侧组起始位置: x={right_group_start}")
print(f"组间距: {group_spacing}px ({group_spacing_percent}%)")
placement_start_time = time.time()
for i, (icon, app_name) in enumerate(icons):
if i < 6:
icon_x = left_group_start + i * (icon_width + icon_spacing)
icon_y = start_y
group_name = "左侧组"
else:
icon_x = right_group_start + (i - 6) * (icon_width + icon_spacing)
icon_y = start_y
group_name = "右侧组"
canvas.paste(icon, (icon_x, icon_y), icon if icon.mode == 'RGBA' else None)
placement_time = (time.time() - placement_start_time) * 1000
print(f"[成功] 图标放置完成 (耗时: {placement_time:.2f}ms)")
print(f"[成功] 成功放置 {len(icons)} 个图标(左右两组各6个)")
total_step_time = (time.time() - step_start_time) * 1000
print(f"[成功] 图标排列总耗时: {total_step_time:.2f}ms")
except Exception as e:
print(f"排列图标时出错: {e}")
def arrange_icons_layout_2(canvas, icon_folder='icon', start_y=1000):
"""布局2:左侧3行18个图标,右侧1行6个图标"""
step_start_time = time.time()
try:
icon_files = glob.glob(os.path.join(icon_folder, '*.png'))
if not icon_files:
print("没有找到图标文件")
return
print(f"[信息] 找到 {len(icon_files)} 个图标文件")
icons = []
load_start_time = time.time()
specified_icons = [
"com_tinnove_mediacenter.png",
"com_autopai_car_dialer.png",
"com_wt_vehiclecenter.png",
"com_wt_maintenance.png",
"com_tinnove_aispace.png",
"com_tinnove_carshow.png",
"com_wt_airconditioner.png",
"com_autopai_album.png",
"com_incall_apps_personalcenter.png",
"com_wtcl_filemanager.png",
"com_incall_dvr.png",
"com_wtcl_electronicdirections.png",
"com_wt_gamecenter.png",
"com_wt_carcamera.png",
"com_autopai_smart_sound_effect.png",
"com_tinnove_aispace.png",
"com_tinnove_scenemode.png",
"com_tinnove_link_client.png",
"com_wtcl_electronicdirections.png",
"com_wt_scene.png",
"com_tinnove_gamezone.png",
]
app_names = [
"音效", "个人中心", "空调", "车辆中心", "电话", "AI空间",
"场景", "导航", "情景", "游戏", "相册", "应用市场",
"健康", "记录仪", "AI语音",
"云摄像头", "客服", "连接", "媒体", "导航", "微信"
]
found_icons = []
for icon_name in specified_icons:
icon_path = os.path.join(icon_folder, icon_name)
if os.path.exists(icon_path):
found_icons.append(icon_path)
else:
print(f"[失败] 错误: 找不到图标文件 {icon_name}")
return
if len(found_icons) < 21:
remaining_icons = [f for f in icon_files if f not in found_icons]
found_icons.extend(remaining_icons[:21-len(found_icons)])
print(f"[警告] 指定的图标不足21个,补充了 {21-len(found_icons)} 个其他图标")
canvas_width, canvas_height = canvas.size
icon_width = int(canvas_width * 3.9 / 100)
icon_height = int(canvas_height * 8.33 / 100)
for i, icon_file in enumerate(found_icons[:21]):
try:
icon = Image.open(icon_file)
icon = icon.resize((icon_width, icon_height), Image.Resampling.LANCZOS)
app_name = app_names[i] if i < len(app_names) else f"应用{i+1}"
icons.append((icon, app_name))
except Exception as e:
print(f"[警告] 加载图标失败 {icon_file}: {e}")
load_time = (time.time() - load_start_time) * 1000
print(f"[成功] 图标加载完成 (耗时: {load_time:.2f}ms)")
if not icons:
print("没有成功加载任何图标")
return
canvas_width, canvas_height = canvas.size
icon_width_percent = 3.9
icon_height_percent = 8.33
icon_spacing_percent = 3.125
row_spacing_percent = 15.28
left_group_start_percent = 5.47
left_group_top_percent = 27.78
right_group_top_percent = 45.14
group_spacing_percent = 11.24
icon_width = int(canvas_width * icon_width_percent / 100)
icon_height = int(canvas_height * icon_height_percent / 100)
icon_spacing = int(canvas_width * icon_spacing_percent / 100)
row_spacing = int(canvas_height * row_spacing_percent / 100)
left_group_start = int(canvas_width * left_group_start_percent / 100)
left_group_top = int(canvas_height * left_group_top_percent / 100)
right_group_top = int(canvas_height * right_group_top_percent / 100)
group_spacing = int(canvas_width * group_spacing_percent / 100)
right_group_start = left_group_start + 6 * (icon_width + icon_spacing) - icon_spacing + group_spacing
print(f"画布尺寸: {canvas_width}x{canvas_height}")
print(f"图标尺寸: {icon_width}x{icon_height} (宽度{icon_width_percent}%, 高度{icon_height_percent}%)")
print(f"图标间距: {icon_spacing}px ({icon_spacing_percent}%)")
print(f"行间距: {row_spacing}px ({row_spacing_percent}%)")
print(f"左侧组起始位置: x={left_group_start} ({left_group_start_percent}%), y={left_group_top} ({left_group_top_percent}%)")
print(f"右侧组起始位置: x={right_group_start}, y={right_group_top} ({right_group_top_percent}%)")
print(f"组间距: {group_spacing}px ({group_spacing_percent}%)")
placement_start_time = time.time()
for i, (icon, app_name) in enumerate(icons):
if i < 15:
if i < 12:
row = i // 6
col = i % 6
else:
row = 2
col = i - 12
icon_x = left_group_start + col * (icon_width + icon_spacing)
icon_y = left_group_top + row * (icon_height + row_spacing)
group_name = f"左侧组第{row+1}行"
else:
icon_x = right_group_start + (i - 15) * (icon_width + icon_spacing)
icon_y = right_group_top
group_name = "右侧组"
canvas.paste(icon, (icon_x, icon_y), icon if icon.mode == 'RGBA' else None)
placement_time = (time.time() - placement_start_time) * 1000
print(f"[成功] 图标放置完成 (耗时: {placement_time:.2f}ms)")
print(f"[成功] 成功放置 {len(icons)} 个图标(左侧3行15个,右侧1行6个)")
total_step_time = (time.time() - step_start_time) * 1000
print(f"[成功] 图标排列总耗时: {total_step_time:.2f}ms")
except Exception as e:
print(f"排列图标时出错: {e}")
def create_layered_image(layout_type=1, canvas_size=(2560, 720), black_layer_color=None):
"""创建分层图片:背景+黑色图层+背景图层+图标
Args:
layout_type: 布局类型 (1 或 2)
canvas_size: 画布尺寸 (宽度, 高度)
black_layer_color: 黑色图层转换颜色,格式为16进制字符串 "#RRGGBB" 或 None
None表示不转换颜色,保持原样
"""
total_start_time = time.time()
try:
canvas_width, canvas_height = canvas_size
canvas = Image.new('RGBA', (canvas_width, canvas_height), (0, 0, 0, 0))
print(f"创建画布: {canvas_width}x{canvas_height}")
if layout_type == 1:
bg_file = 'bg.png'
black_file = 'black.png'
else:
bg_file = 'bg2.png'
black_file = 'black2.png'
step_start_time = time.time()
try:
background_img = Image.open('wallpaper_1.png')
background_img = background_img.resize((canvas_width, canvas_height), Image.Resampling.LANCZOS)
canvas.paste(background_img, (0, 0))
step_time = (time.time() - step_start_time) * 1000
print(f"[成功] 背景图片 wallpaper_1.png 已加载并调整大小 (耗时: {step_time:.2f}ms)")
except Exception as e:
print(f"背景图片加载失败: {e}")
canvas.paste((128, 128, 128), (0, 0, canvas_width, canvas_height))
print("使用默认灰色背景")
step_start_time = time.time()
try:
black_img = Image.open(black_file)
black_img = black_img.resize((canvas_width, canvas_height), Image.Resampling.LANCZOS)
start_time = time.time()
if black_layer_color is not None:
try:
rgb_color = hex_to_rgb(black_layer_color)
if rgb_color is None:
raise ValueError("颜色值不能为空")
black_array = np.array(black_img)
black_array[:, :, :3] = rgb_color
processed_img = Image.fromarray(black_array)
end_time = time.time()
processing_time = (end_time - start_time) * 1000
print(f"[成功] 黑色图层已转换为颜色 {black_layer_color} (RGB: {rgb_color}),耗时: {processing_time:.2f}ms")
except ValueError as e:
print(f"[错误] 颜色格式错误: {e}")
processed_img = black_img
print(f"[警告] 黑色图层保持原样")
else:
processed_img = black_img
print(f"[成功] 黑色图层保持原样,未进行颜色转换")
canvas.paste(processed_img, (0, 0), processed_img if processed_img.mode == 'RGBA' else None)
step_time = (time.time() - step_start_time) * 1000
print(f"[成功] 黑色图层已加载并调整大小 (总耗时: {step_time:.2f}ms)")
except Exception as e:
print(f"加载黑色图层失败: {e}")
step_start_time = time.time()
try:
bgbg_img = Image.open(bg_file)
bgbg_img = bgbg_img.resize((canvas_width, canvas_height), Image.Resampling.LANCZOS)
canvas.paste(bgbg_img, (0, 0), bgbg_img if bgbg_img.mode == 'RGBA' else None)
step_time = (time.time() - step_start_time) * 1000
print(f"[成功] 背景图层 {bg_file} 已加载并调整大小 (耗时: {step_time:.2f}ms)")
except Exception as e:
print(f"背景图层加载失败: {e}")
step_start_time = time.time()
start_y_percent = 45.14
start_y = int(canvas_height * start_y_percent / 100)
if layout_type == 1:
arrange_icons_layout_1(canvas, icon_folder='icon', start_y=start_y)
else:
arrange_icons_layout_2(canvas, icon_folder='icon', start_y=start_y)
step_time = (time.time() - step_start_time) * 1000
print(f"[成功] 图标排列完成 (耗时: {step_time:.2f}ms)")
step_start_time = time.time()
try:
output_filename = f'preview{layout_type}.png'
canvas.save(output_filename)
step_time = (time.time() - step_start_time) * 1000
print(f"[成功] 图片已保存为: {output_filename} (耗时: {step_time:.2f}ms)")
except Exception as e:
print(f"保存图片失败: {e}")
print(f"错误类型: {type(e).__name__}")
print(f"当前工作目录: {os.getcwd()}")
return None
total_time = (time.time() - total_start_time) * 1000
print(f"\n=== 总耗时统计 ===")
print(f"总耗时: {total_time:.2f}ms")
return canvas
except Exception as e:
print(f"创建分层图片时出错: {e}")
return None
def analyze_image(filename):
"""分析生成的图片信息"""
try:
img = Image.open(filename)
print(f"生成图片信息:")
print(f" 尺寸: {img.size}")
print(f" 模式: {img.mode}")
print(f" 格式: {img.format}")
img_array = np.array(img)
print(f" 数组形状: {img_array.shape}")
print(f" 数据类型: {img_array.dtype}")
if len(img_array.shape) == 3:
print(f" 颜色通道数: {img_array.shape[2]}")
except Exception as e:
print(f"分析图片时出错: {e}")
def main():
"""主函数:生成两种布局的图片"""
print("=== 统一图片生成程序 ===")
print("目标: 生成两种不同布局的图片")
print()
total_start_time = time.time()
black_layer_color = "#000000"
print("=== 生成布局1图片 ===")
canvas1 = create_layered_image(layout_type=1, canvas_size=(2560, 720), black_layer_color=black_layer_color)
if canvas1:
analyze_image('preview1.png')
print("[成功] 布局1图片生成完成!")
else:
print("[失败] 布局1图片生成失败!")
print()
print("=== 生成布局2图片 ===")
canvas2 = create_layered_image(layout_type=2, canvas_size=(2560, 720), black_layer_color=black_layer_color)
if canvas2:
analyze_image('preview2.png')
print("[成功] 布局2图片生成完成!")
else:
print("[失败] 布局2图片生成失败!")
total_time = (time.time() - total_start_time) * 1000
print(f"\n=== 总耗时统计 ===")
print(f"总耗时: {total_time:.2f}ms")
if __name__ == "__main__":
main()
const fs = require('fs');
const path = require('path');
const sharp = require('sharp');
const glob = require('glob');
function hexToRgb(hexColor) {
if (hexColor === null || hexColor === undefined) {
return null;
}
hexColor = hexColor.replace(/^#/, '');
if (!/^[0-9A-Fa-f]{6}$/.test(hexColor)) {
throw new Error(`无效的16进制颜色格式: ${hexColor},应为6位16进制数`);
}
const r = parseInt(hexColor.substring(0, 2), 16);
const g = parseInt(hexColor.substring(2, 4), 16);
const b = parseInt(hexColor.substring(4, 6), 16);
return { r, g, b };
}
async function loadAndResizeIcon(iconPath, width, height) {
try {
return await sharp(iconPath)
.resize(width, height, {
fit: 'contain',
background: { r: 0, g: 0, b: 0, alpha: 0 }
})
.toBuffer();
} catch (e) {
console.log(`[警告] 加载图标失败 ${iconPath}: ${e}`);
throw e;
}
}
async function arrangeIconsLayout1(canvasSize, iconFolder = 'icon', startY = 1000) {
const stepStartTime = Date.now();
const { width: canvasWidth, height: canvasHeight } = canvasSize;
try {
const iconFolderPath = path.join(process.cwd(), iconFolder);
console.log(`搜索图标文件,路径: ${iconFolderPath}`);
if (!fs.existsSync(iconFolderPath)) {
console.log(`图标文件夹不存在: ${iconFolderPath}`);
return [];
}
let iconFiles = [];
try {
const dirContents = fs.readdirSync(iconFolderPath);
iconFiles = dirContents
.filter(file => file.toLowerCase().endsWith('.png'))
.map(file => path.join(iconFolderPath, file));
console.log(`找到 ${iconFiles.length} 个PNG图标文件`);
console.log(`前5个文件: ${iconFiles.slice(0, 5).map(f => path.basename(f)).join(', ')}`);
} catch (e) {
console.log(`读取目录失败: ${e.message}`);
return [];
}
if (!iconFiles.length) {
console.log("没有找到PNG图标文件,搜索路径: " + iconFolderPath);
return [];
}
console.log(`[信息] 找到 ${iconFiles.length} 个图标文件`);
const specifiedIcons = [
"com_autopai_smart_sound_effect.png",
"com_incall_apps_personalcenter.png",
"com_wt_carcamera.png",
"com_wt_airconditioner.png",
"com_wt_vehiclecenter.png",
"com_autopai_car_dialer.png",
"com_tinnove_mediacenter.png",
"com_tinnove_scenemode.png",
"com_tinnove_scenemode.png",
"com_wtcl_electronicdirections.png",
"com_wt_scene.png",
"com_tinnove_gamezone.png",
];
const appNames = [
"音效", "个人中心", "空调", "车辆中心", "电话", "AI空间",
"情景模式", "导航", "场景", "游戏", "空调2", "AI空间2"
];
const foundIcons = [];
for (const iconName of specifiedIcons) {
const iconPath = path.join(process.cwd(), iconFolder, iconName);
if (fs.existsSync(iconPath)) {
foundIcons.push(iconPath);
} else {
console.log(`[失败] 错误: 找不到图标文件 ${iconName},搜索路径: ${iconPath}`);
return [];
}
}
if (foundIcons.length < 12) {
const remainingIcons = iconFiles.filter(f => !foundIcons.includes(f));
foundIcons.push(...remainingIcons.slice(0, 12 - foundIcons.length));
console.log(`[警告] 指定的图标不足12个,补充了 ${12 - foundIcons.length} 个其他图标`);
}
const iconWidthPercent = 3.9;
const iconHeightPercent = 8.33;
const iconSpacingPercent = 3.125;
const leftGroupStartPercent = 5.47;
const groupSpacingPercent = 10.84;
const iconWidth = Math.floor(canvasWidth * iconWidthPercent / 100);
const iconHeight = Math.floor(canvasHeight * iconHeightPercent / 100);
const iconSpacing = Math.floor(canvasWidth * iconSpacingPercent / 100);
const leftGroupStart = Math.floor(canvasWidth * leftGroupStartPercent / 100);
const groupSpacing = Math.floor(canvasWidth * groupSpacingPercent / 100);
const rightGroupStart = leftGroupStart + 6 * (iconWidth + iconSpacing) - iconSpacing + groupSpacing;
console.log(`画布尺寸: ${canvasWidth}x${canvasHeight}`);
console.log(`图标尺寸: ${iconWidth}x${iconHeight} (宽度${iconWidthPercent}%, 高度${iconHeightPercent}%)`);
console.log(`图标间距: ${iconSpacing}px (${iconSpacingPercent}%)`);
console.log(`左侧组起始位置: x=${leftGroupStart} (${leftGroupStartPercent}%)`);
console.log(`右侧组起始位置: x=${rightGroupStart}`);
console.log(`组间距: ${groupSpacing}px (${groupSpacingPercent}%)`);
const placementStartTime = Date.now();
const compositeOperations = [];
for (let i = 0; i < Math.min(foundIcons.length, 12); i++) {
try {
const iconFile = foundIcons[i];
const appName = i < appNames.length ? appNames[i] : `应用${i+1}`;
let iconX, iconY;
if (i < 6) {
iconX = leftGroupStart + i * (iconWidth + iconSpacing);
iconY = startY;
} else {
iconX = rightGroupStart + (i - 6) * (iconWidth + iconSpacing);
iconY = startY;
}
const resizedIcon = await loadAndResizeIcon(iconFile, iconWidth, iconHeight);
compositeOperations.push({
input: resizedIcon,
top: iconY,
left: iconX
});
} catch (e) {
console.log(`[警告] 处理图标失败 ${foundIcons[i]}: ${e}`);
}
}
const placementTime = Date.now() - placementStartTime;
console.log(`[成功] 图标放置完成 (耗时: ${placementTime.toFixed(2)}ms)`);
console.log(`[成功] 成功放置 ${compositeOperations.length} 个图标(左右两组各6个)`);
const totalStepTime = Date.now() - stepStartTime;
console.log(`[成功] 图标排列总耗时: ${totalStepTime.toFixed(2)}ms`);
return compositeOperations;
} catch (e) {
console.log(`排列图标时出错: ${e}`);
return [];
}
}
async function arrangeIconsLayout2(canvasSize, iconFolder = 'icon', startY = 1000) {
const stepStartTime = Date.now();
const { width: canvasWidth, height: canvasHeight } = canvasSize;
try {
const iconFolderPath = path.join(process.cwd(), iconFolder);
console.log(`搜索图标文件,路径: ${iconFolderPath}`);
if (!fs.existsSync(iconFolderPath)) {
console.log(`图标文件夹不存在: ${iconFolderPath}`);
return [];
}
let iconFiles = [];
try {
const dirContents = fs.readdirSync(iconFolderPath);
iconFiles = dirContents
.filter(file => file.toLowerCase().endsWith('.png'))
.map(file => path.join(iconFolderPath, file));
console.log(`找到 ${iconFiles.length} 个PNG图标文件`);
console.log(`前5个文件: ${iconFiles.slice(0, 5).map(f => path.basename(f)).join(', ')}`);
} catch (e) {
console.log(`读取目录失败: ${e.message}`);
return [];
}
if (!iconFiles.length) {
console.log("没有找到PNG图标文件,搜索路径: " + iconFolderPath);
return [];
}
console.log(`[信息] 找到 ${iconFiles.length} 个图标文件`);
const specifiedIcons = [
"com_tinnove_mediacenter.png",
"com_autopai_car_dialer.png",
"com_wt_vehiclecenter.png",
"com_wt_maintenance.png",
"com_tinnove_aispace.png",
"com_tinnove_carshow.png",
"com_wt_airconditioner.png",
"com_autopai_album.png",
"com_incall_apps_personalcenter.png",
"com_wtcl_filemanager.png",
"com_incall_dvr.png",
"com_wtcl_electronicdirections.png",
"com_wt_gamecenter.png",
"com_wt_carcamera.png",
"com_autopai_smart_sound_effect.png",
"com_tinnove_aispace.png",
"com_tinnove_scenemode.png",
"com_tinnove_link_client.png",
"com_wtcl_electronicdirections.png",
"com_wt_scene.png",
"com_tinnove_gamezone.png",
];
const appNames = [
"音效", "个人中心", "空调", "车辆中心", "电话", "AI空间",
"场景", "导航", "情景", "游戏", "相册", "应用市场",
"健康", "记录仪", "AI语音",
"云摄像头", "客服", "连接", "媒体", "导航", "微信"
];
const foundIcons = [];
for (const iconName of specifiedIcons) {
const iconPath = path.join(process.cwd(), iconFolder, iconName);
if (fs.existsSync(iconPath)) {
foundIcons.push(iconPath);
} else {
console.log(`[失败] 错误: 找不到图标文件 ${iconName},搜索路径: ${iconPath}`);
return [];
}
}
if (foundIcons.length < 21) {
const remainingIcons = iconFiles.filter(f => !foundIcons.includes(f));
foundIcons.push(...remainingIcons.slice(0, 21 - foundIcons.length));
console.log(`[警告] 指定的图标不足21个,补充了 ${21 - foundIcons.length} 个其他图标`);
}
const iconWidthPercent = 3.9;
const iconHeightPercent = 8.33;
const iconSpacingPercent = 3.125;
const rowSpacingPercent = 15.28;
const leftGroupStartPercent = 5.47;
const leftGroupTopPercent = 27.78;
const rightGroupTopPercent = 45.14;
const groupSpacingPercent = 11.24;
const iconWidth = Math.floor(canvasWidth * iconWidthPercent / 100);
const iconHeight = Math.floor(canvasHeight * iconHeightPercent / 100);
const iconSpacing = Math.floor(canvasWidth * iconSpacingPercent / 100);
const rowSpacing = Math.floor(canvasHeight * rowSpacingPercent / 100);
const leftGroupStart = Math.floor(canvasWidth * leftGroupStartPercent / 100);
const leftGroupTop = Math.floor(canvasHeight * leftGroupTopPercent / 100);
const rightGroupTop = Math.floor(canvasHeight * rightGroupTopPercent / 100);
const groupSpacing = Math.floor(canvasWidth * groupSpacingPercent / 100);
const rightGroupStart = leftGroupStart + 6 * (iconWidth + iconSpacing) - iconSpacing + groupSpacing;
console.log(`画布尺寸: ${canvasWidth}x${canvasHeight}`);
console.log(`图标尺寸: ${iconWidth}x${iconHeight} (宽度${iconWidthPercent}%, 高度${iconHeightPercent}%)`);
console.log(`图标间距: ${iconSpacing}px (${iconSpacingPercent}%)`);
console.log(`行间距: ${rowSpacing}px (${rowSpacingPercent}%)`);
console.log(`左侧组起始位置: x=${leftGroupStart} (${leftGroupStartPercent}%), y=${leftGroupTop} (${leftGroupTopPercent}%)`);
console.log(`右侧组起始位置: x=${rightGroupStart}, y=${rightGroupTop} (${rightGroupTopPercent}%)`);
console.log(`组间距: ${groupSpacing}px (${groupSpacingPercent}%)`);
const placementStartTime = Date.now();
const compositeOperations = [];
for (let i = 0; i < Math.min(foundIcons.length, 21); i++) {
try {
const iconFile = foundIcons[i];
const appName = i < appNames.length ? appNames[i] : `应用${i+1}`;
let iconX, iconY;
if (i < 15) {
let row, col;
if (i < 12) {
row = Math.floor(i / 6);
col = i % 6;
} else {
row = 2;
col = i - 12;
}
iconX = leftGroupStart + col * (iconWidth + iconSpacing);
iconY = leftGroupTop + row * (iconHeight + rowSpacing);
} else {
iconX = rightGroupStart + (i - 15) * (iconWidth + iconSpacing);
iconY = rightGroupTop;
}
const resizedIcon = await loadAndResizeIcon(iconFile, iconWidth, iconHeight);
compositeOperations.push({
input: resizedIcon,
top: iconY,
left: iconX
});
} catch (e) {
console.log(`[警告] 处理图标失败 ${foundIcons[i]}: ${e}`);
}
}
const placementTime = Date.now() - placementStartTime;
console.log(`[成功] 图标放置完成 (耗时: ${placementTime.toFixed(2)}ms)`);
console.log(`[成功] 成功放置 ${compositeOperations.length} 个图标(左侧3行15个,右侧1行6个)`);
const totalStepTime = Date.now() - stepStartTime;
console.log(`[成功] 图标排列总耗时: ${totalStepTime.toFixed(2)}ms`);
return compositeOperations;
} catch (e) {
console.log(`排列图标时出错: ${e}`);
return [];
}
}
async function createLayeredImage(layoutType = 1, canvasSize = { width: 2560, height: 720 }, blackLayerColor = null) {
const totalStartTime = Date.now();
const { width: canvasWidth, height: canvasHeight } = canvasSize;
try {
const bgFile = path.join(process.cwd(), layoutType === 1 ? 'bg.png' : 'bg2.png');
const blackFile = path.join(process.cwd(), layoutType === 1 ? 'black.png' : 'black2.png');
const wallpaperPath = path.join(process.cwd(), 'wallpaper_1.png');
const wallpaperBuffer = await sharp(wallpaperPath).resize(canvasWidth, canvasHeight).toBuffer();
let blackBuffer = await sharp(blackFile).resize(canvasWidth, canvasHeight).toBuffer();
if (blackLayerColor !== null) {
const rgbColor = hexToRgb(blackLayerColor);
blackBuffer = await sharp(blackBuffer).tint(rgbColor).png().toBuffer();
}
const bgBuffer = await sharp(bgFile).resize(canvasWidth, canvasHeight).toBuffer();
const startYPercent = 45.14;
const startY = Math.floor(canvasHeight * startYPercent / 100);
let iconComposites;
if (layoutType === 1) {
iconComposites = await arrangeIconsLayout1(canvasSize, 'icon', startY);
} else {
iconComposites = await arrangeIconsLayout2(canvasSize, 'icon', startY);
}
let canvas = sharp({
create: {
width: canvasWidth,
height: canvasHeight,
channels: 4,
background: { r: 0, g: 0, b: 0, alpha: 0 }
}
});
const allLayers = [
{ input: wallpaperBuffer, top: 0, left: 0 },
{ input: blackBuffer, top: 0, left: 0 },
{ input: bgBuffer, top: 0, left: 0 },
...iconComposites
];
canvas = await canvas.composite(allLayers);
const outputFilename = `preview${layoutType}.png`;
await canvas.toFile(outputFilename);
const totalTime = Date.now() - totalStartTime;
console.log(`[成功] 图片已保存为: ${outputFilename}`);
console.log(`总耗时: ${totalTime.toFixed(2)}ms`);
return canvas;
} catch (e) {
console.log(`创建分层图片时出错: ${e}`);
return null;
}
}
async function analyzeImage(filename) {
try {
const metadata = await sharp(filename).metadata();
console.log(`生成图片信息:`);
console.log(` 尺寸: ${metadata.width}x${metadata.height}`);
console.log(` 格式: ${metadata.format}`);
console.log(` 通道数: ${metadata.channels}`);
console.log(` 文件路径: ${path.resolve(filename)}`);
const stats = fs.statSync(filename);
console.log(` 文件大小: ${stats.size} 字节`);
} catch (e) {
console.log(`分析图片时出错: ${e}`);
}
}
async function main() {
console.log("=== 统一图片生成程序 ===\n目标: 生成两种不同布局的图片\n");
const totalStartTime = Date.now();
const blackLayerColor = "#000000";
console.log("=== 生成布局1图片 ===");
const canvas1 = await createLayeredImage(1, { width: 2560, height: 720 }, blackLayerColor);
if (canvas1) {
await analyzeImage('preview1.png');
console.log("[成功] 布局1图片生成完成!");
} else {
console.log("[失败] 布局1图片生成失败!");
}
console.log();
console.log("=== 生成布局2图片 ===");
const canvas2 = await createLayeredImage(2, { width: 2560, height: 720 }, blackLayerColor);
if (canvas2) {
await analyzeImage('preview2.png');
console.log("[成功] 布局2图片生成完成!");
} else {
console.log("[失败] 布局2图片生成失败!");
}
const totalTime = Date.now() - totalStartTime;
console.log(`\n=== 总耗时统计 ===`);
console.log(`总耗时: ${totalTime.toFixed(2)}ms`);
}
if (require.main === module) {
main().catch(err => {
console.error("程序执行出错:", err);
});
}
module.exports = {
hexToRgb,
loadAndResizeIcon,
arrangeIconsLayout1,
arrangeIconsLayout2,
createLayeredImage,
analyzeImage,
main
};