[Python] 纯文本查看 复制代码
# -*- coding: utf-8 -*-
import os
import sys
import math
import random
import threading
import tempfile
import string
import traceback
from PIL import Image, ImageDraw, ImageFont, ImageFilter, ImageEnhance, ImageChops
try:
import numpy as np
except ImportError:
np = None
try:
import imageio
except ImportError:
imageio = None
try:
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, colorchooser
except ImportError:
print("请先安装 tkinter(Windows自带 / Linux: sudo apt install python3-tk)")
sys.exit(1)
PALETTES = {
"经典彩虹": [(255,0,0),(255,100,0),(255,255,0),(0,255,0),(0,255,255),(0,0,255),(180,0,255)],
"赛博朋克": [(255,0,255),(0,255,255),(255,255,0),(255,0,150)],
"蒸汽波": [(255,0,255),(0,255,255),(255,255,100)],
"日落橙光": [(255,80,0),(255,140,60),(255,200,100),(255,240,180)],
"极光绿紫": [(0,255,150),(0,200,255),(100,100,255),(200,0,255)],
"糖果甜心": [(255,100,200),(255,180,255),(180,255,255),(100,255,200)],
"金属玫瑰金": [(255,192,203),(255,182,193),(255,105,180)],
"深海幽蓝": [(0,50,100),(0,100,200),(0,150,255),(100,200,255)],
"复古胶片": [(200,100,50),(255,180,100),(255,220,180)],
"霓虹灯红": [(255,0,100),(255,50,150),(255,100,200)],
"冰霜薄荷": [(100,255,200),(150,255,255),(200,255,255)],
"熔岩红黑": [(255,0,0),(200,0,0),(100,0,0)],
"翡翠绿金": [(0,255,100),(100,255,100),(200,255,100)],
"星空紫蓝": [(50,0,150),(100,50,200),(150,100,255)],
"柠檬黄绿": [(255,255,0),(200,255,0),(150,255,100)],
"薰衣草紫": [(180,150,255),(200,180,255),(220,200,255)],
"樱花粉": [(255,180,200),(255,200,220),(255,220,240)],
"暗夜黑金": [(255,215,0),(255,180,0),(200,150,0)],
"热带橙粉": [(255,100,50),(255,150,100),(255,200,150)],
"蓝莓渐变": [(100,50,255),(150,100,255),(200,150,255)],
"南瓜橙": [(255,140,0),(255,180,50),(255,220,100)],
"草莓奶昔": [(255,150,180),(255,200,220),(255,230,240)],
"午夜蓝": [(0,0,100),(0,50,150),(0,100,200)],
"电光紫": [(200,0,255),(255,50,255),(255,100,255)],
"珊瑚橘红": [(255,100,80),(255,150,120),(255,200,160)],
"银河渐变": [(100,0,200),(150,50,255),(200,100,255)],
"霓虹绿": [(0,255,0),(100,255,100),(200,255,200)],
"海洋蓝": [(0,100,200),(0,150,255),(100,200,255)],
"玫瑰金": [(255,200,200),(255,180,180),(255,150,150)],
"金属银灰": [(200,200,200),(180,180,180),(150,150,150)],
}
def generate_frames(text, font, size, palette, effect_name, frames_count, add_glow):
w, h = size
base = Image.new("RGBA", size)
draw = ImageDraw.Draw(base)
for x in range(w):
t = x / max(w-1, 1)
i = min(int(t * (len(palette)-1)), len(palette)-2)
frac = t * (len(palette)-1) - i
c1, c2 = palette, palette[i+1]
col = tuple(int(c1[j]*(1-frac) + c2[j]*frac) for j in range(3)) + (255,)
draw.line([(x,0),(x,h)], fill=col)
mask = Image.new("L", size, 0)
stroke = Image.new("L", size, 0)
d_mask = ImageDraw.Draw(mask)
d_stroke = ImageDraw.Draw(stroke)
bbox = d_mask.textbbox((0,0), text, font=font)
tw, th = bbox[2]-bbox[0], bbox[3]-bbox[1]
pos = ((w-tw)//2, (h-th)//2)
d_mask.text(pos, text, font=font, fill=255)
for dx in [-2,-1,0,1,2]:
for dy in [-2,-1,0,1,2]:
if dx*dx + dy*dy <= 4:
d_stroke.text((pos[0]+dx, pos[1]+dy), text, font=font, fill=180)
frames = []
for i in range(frames_count):
if effect_name == "彩虹平移":
shift = int(i * 3.8 * w / frames_count) % w
shifted = ImageChops.offset(base, shift, 0)
img = Image.new("RGBA", size)
img.paste(shifted, (0,0), mask)
elif effect_name == "波浪扭曲":
phase = i * 0.35
wave_mask = Image.new("L", size, 0)
for y in range(h):
dx = int(18 * math.sin(2 * math.pi * y / 85 + phase))
row = mask.crop((0,y,w,y+1))
offset_row = ImageChops.offset(row, dx, 0)
wave_mask.paste(offset_row, (max(0,-dx), y))
img = Image.new("RGBA", size)
img.paste(base, (0,0), wave_mask)
elif effect_name == "霓虹闪烁":
bright = 2.3 if i % 7 < 5 else 0.8
img = Image.composite(base, Image.new("RGBA", size, (0,0,0,255)), mask)
img = ImageEnhance.Brightness(img).enhance(bright)
elif effect_name == "抖动毛刺":
dx = random.randint(-22,22)
dy = random.randint(-22,22)
moved = ImageChops.offset(mask, dx, dy)
img = Image.new("RGBA", size)
img.paste(base, (0,0), moved)
elif effect_name == "呼吸灯":
t = (math.sin(i * 0.28) + 1) / 2
img = Image.composite(base, Image.new("RGBA", size, (0,0,0,255)), mask)
img = ImageEnhance.Brightness(img).enhance(0.75 + t * 1.3)
elif effect_name == "故障艺术":
r_shift = random.randint(-15,15)
g_shift = random.randint(-10,10)
b_shift = random.randint(-20,20)
r_mask = ImageChops.offset(mask, r_shift, 0)
g_mask = ImageChops.offset(mask, g_shift, 0)
b_mask = ImageChops.offset(mask, b_shift, 0)
r_layer = Image.new("RGBA", size); r_layer.paste(base, (0,0), r_mask)
g_layer = Image.new("RGBA", size); g_layer.paste(base, (0,0), g_mask)
b_layer = Image.new("RGBA", size); b_layer.paste(base, (0,0), b_mask)
img = Image.merge("RGB", (r_layer.getchannel('R'), g_layer.getchannel('G'), b_layer.getchannel('B')))
img = img.convert("RGBA")
elif effect_name == "3D立体":
offset = (i % 20 - 10)
shadow = Image.new("RGBA", size, (0,0,0,160))
shadow.paste(base, (offset+8, offset+8), mask)
img = Image.new("RGBA", size)
img.paste(shadow, (0,0))
img.paste(base, (offset, offset), mask)
elif effect_name == "液态金属":
phase = i * 0.4
liquid = Image.new("L", size, 0)
for y in range(h):
dx = int(12 * math.sin(2 * math.pi * y / 70 + phase))
row = mask.crop((0,y,w,y+1))
offset_row = ImageChops.offset(row, dx, 0)
liquid.paste(offset_row, (max(0,-dx), y))
img = Image.new("RGBA", size)
img.paste(base, (0,0), liquid)
img = img.filter(ImageFilter.EDGE_ENHANCE)
elif effect_name == "心跳脉冲":
radius = 5 + int(20 * abs(math.sin(i * 0.5)))
glow = base.copy().filter(ImageFilter.GaussianBlur(radius))
img = Image.composite(base, glow, mask)
elif effect_name == "扫描线":
img = Image.composite(base, Image.new("RGBA", size, (0,0,0,255)), mask)
line_y = (i * 12) % h
overlay = Image.new("RGBA", size, (0,0,0,0))
draw = ImageDraw.Draw(overlay)
draw.rectangle([0, line_y-4, w, line_y+4], fill=(255,255,255,100))
img = Image.alpha_composite(img, overlay)
else:
shift = int(i * 3.8 * w / frames_count) % w
shifted = ImageChops.offset(base, shift, 0)
img = Image.new("RGBA", size)
img.paste(shifted, (0,0), mask)
if add_glow:
glow = img.copy().filter(ImageFilter.GaussianBlur(10))
glow = ImageEnhance.Brightness(glow).enhance(2.2)
img = Image.alpha_composite(img, glow)
final = Image.new("RGB", size, (0, 0, 0))
temp = Image.new("RGBA", size)
temp.paste(img, (0,0), img)
final.paste(temp.convert("RGB"), (0,0))
final.paste((255,255,255), (0,0), stroke)
frames.append(final)
return frames
class App:
def __init__(self):
self.root = tk.Tk()
self.root.title("炫彩文字生成器by:煎饼 Q312275714")
self.root.geometry("920x680")
self.root.resizable(False, False)
self.text = tk.StringVar(value="认准煎饼")
self.effect = tk.StringVar(value="彩虹平移")
self.palette_name = tk.StringVar(value="经典彩虹")
self.font_size = tk.IntVar(value=20)
self.frames_count = tk.IntVar(value=36)
self.fps = tk.IntVar(value=24)
self.add_glow = tk.BooleanVar(value=True)
tk.Label(self.root, text="煎饼炫彩文字生成器", font=("微软雅黑", 26, "bold"), fg="#FF1493").pack(pady=25)
f1 = tk.Frame(self.root); f1.pack(pady=12)
tk.Label(f1, text="文字:", font=14).pack(side="left")
tk.Entry(f1, textvariable=self.text, width=38, font=16).pack(side="left", padx=12)
f2 = tk.Frame(self.root); f2.pack(pady=12)
tk.Label(f2, text="特效:", font=14).pack(side="left")
ttk.Combobox(f2, textvariable=self.effect, values=["彩虹平移","波浪扭曲","霓虹闪烁","抖动毛刺","呼吸灯","故障艺术","3D立体","液态金属","心跳脉冲","扫描线"], state="readonly", width=12).pack(side="left", padx=12)
tk.Label(f2, text="调色板:", font=14).pack(side="left")
ttk.Combobox(f2, textvariable=self.palette_name, values=list(PALETTES.keys()), state="readonly", width=22).pack(side="left", padx=12)
f3 = tk.Frame(self.root); f3.pack(pady=12)
tk.Label(f3, text="字体大小:", font=14).pack(side="left")
tk.Scale(f3, from_=60, to=280, orient="horizontal", variable=self.font_size, length=420).pack(side="left", padx=20)
tk.Label(f3, textvariable=self.font_size, font=16, width=4).pack(side="left")
f4 = tk.Frame(self.root); f4.pack(pady=12)
tk.Checkbutton(f4, text="添加霓虹光晕(强烈推荐)", variable=self.add_glow, font=14).pack(side="left")
tk.Label(f4, text=" 帧数:", font=14).pack(side="left")
tk.Spinbox(f4, from_=10, to=200, textvariable=self.frames_count, width=6).pack(side="left", padx=8)
tk.Label(f4, text=" FPS:", font=14).pack(side="left")
tk.Spinbox(f4, from_=10, to=60, textvariable=self.fps, width=6).pack(side="left", padx=8)
self.progress = ttk.Progressbar(self.root, mode="indeterminate")
self.progress.pack(fill="x", padx=120, pady=30)
btns = tk.Frame(self.root); btns.pack(pady=30)
tk.Button(btns, text="预览 GIF", width=18, height=2, bg="#32CD32", fg="white", font=14, command=self.preview).pack(side="left", padx=40)
tk.Button(btns, text="保存 GIF", width=18, height=2, bg="#4169E1", fg="white", font=14, command=self.save_gif).pack(side="left", padx=40)
tk.Button(btns, text="保存 MP4", width=18, height=2, bg="#FF4500", fg="white", font=14, command=self.save_mp4).pack(side="left", padx=40)
def get_font(self):
size = self.font_size.get()
fonts = ["msyh.ttc","msyhbd.ttc","simhei.ttf","simsun.ttc","arial.ttf"]
for f in fonts:
path = os.path.join(os.environ.get("WINDIR",""),"Fonts",f) if os.name=="nt" else f
if os.path.exists(path):
try: return ImageFont.truetype(path, size)
except: continue
if os.path.exists(f):
try: return ImageFont.truetype(f, size)
except: continue
return ImageFont.load_default()
def generate(self):
text = self.text.get().strip()
if not text:
messagebox.showerror("错误", "请输入文字")
return None
size = (900, 360)
palette = PALETTES.get(self.palette_name.get(), PALETTES["经典彩虹"])
font = self.get_font()
return generate_frames(text, font, size, palette, self.effect.get(), self.frames_count.get(), self.add_glow.get())
def run_task(self, func):
self.progress.start(15)
def task():
try:
func()
except Exception as e:
tb = traceback.format_exc()
with open("error_log.txt", "w", encoding="utf-8") as f: f.write(tb)
messagebox.showerror("错误", f"生成失败:{e}")
finally:
self.progress.stop()
threading.Thread(target=task, daemon=True).start()
def preview(self):
def task():
frames = self.generate()
if frames:
safe_name = "preview_" + "".join(random.choices(string.ascii_letters, k=10)) + ".gif"
path = os.path.join(tempfile.gettempdir(), safe_name)
duration = int(1000 / max(1, self.fps.get()))
frames[0].save(path, save_all=True, append_images=frames[1:], duration=duration, loop=0)
os.startfile(path) if os.name == "nt" else os.system(f"open '{path}'" if sys.platform == "darwin" else f"xdg-open '{path}'")
self.run_task(task)
def save_gif(self):
path = filedialog.asksaveasfilename(defaultextension=".gif", filetypes=[("GIF动画", "*.gif")])
if not path: return
def task():
frames = self.generate()
if frames:
duration = int(1000 / max(1, self.fps.get()))
frames[0].save(path, save_all=True, append_images=frames[1:], duration=duration, loop=0)
messagebox.showinfo("成功", f"已保存!\n{path}")
self.run_task(task)
def save_mp4(self):
if not imageio:
messagebox.showerror("错误", "请先运行:pip install imageio imageio-ffmpeg")
return
path = filedialog.asksaveasfilename(defaultextension=".mp4", filetypes=[("MP4视频", "*.mp4")])
if not path: return
def task():
frames = self.generate()
if frames:
writer = imageio.get_writer(path, fps=self.fps.get(), codec='libx264', pixelformat='yuv420p')
for f in frames:
writer.append_data(np.array(f))
writer.close()
messagebox.showinfo("成功", f"已保存 MP4!\n{path}")
self.run_task(task)
def run(self):
self.root.mainloop()
if __name__ == "__main__":
try:
App().run()
except Exception as e:
with open("crash_log.txt", "w", encoding="utf-8") as f:
f.write(traceback.format_exc())
messagebox.showerror("程序崩溃", "已生成 crash_log.txt")