
大家好,我是枝枝。
最近很眼馋某人的小卡片,把文字记录在小卡片里,简洁又美观,真是泰裤辣!
终于狠下心,在ChatGPT的帮助下,自己手把手写了一个程序。

如果有更好的方法,欢迎告诉我,感激不尽!
对你有用的话,记得动动小手指,点个赞哟!
以下是相关代码分享,注释里包含了我对代码逻辑的理解,哪怕零基础,拿去就能用。
使用工具:Python,vscode
需要准备:个人头像,字体文件
还有,最重要的:思考逻辑的啊脑袋
# 创建渐变图片
def generate_gradient(height1):
"""
生成一个从上到下的渐变背景图片。
参数:
width -- 图片宽度
height -- 图片高度
start_color -- 渐变开始的颜色 (RGB格式)
end_color -- 渐变结束的颜色 (RGB格式)
"""
# 定义图像大小和颜色
width = 580
font = ImageFont.truetype('simhei', 30)
count = font.getsize('枝')[1] # 计算单个字符高度
# 设定图片高度随文字行数而调整
height = height1 * count + 340 + height1 * 13
end_color = (191, 151, 211) # 渐变开始颜色
start_color = (210, 231, 220) # 渐变结束颜色,紫色
base = Image.new('RGB', (width, height)) # 创建图片
draw = ImageDraw.Draw(base) # 创建绘图对象
# 对角线的长度
diagonal_length = math.sqrt(width**2 + height**2)
# 遍历每一个像素点,计算每一个像素点的RGB
for x in range(width):
for y in range(height):
# 当前点到起始点的距离
distance = math.sqrt(x**2 + y**2)
# 计算当前位置的颜色
ratio = distance / diagonal_length # ratio 的范围是0-1,0表示起点坐标, 1表示对角线对点坐标
r = int((end_color[0] - start_color[0]) * ratio + start_color[0]) # end_color[0] - start_color 这个计算表示渐变的颜色范围,乘于ratio表示处于哪个渐变颜色
g = int((end_color[1] - start_color[1]) * ratio + start_color[1]) # 如果ratio代表颜色渐变的比例,取值两个颜色的差值
b = int((end_color[2] - start_color[2]) * ratio + start_color[2]) # 每个像素点最终呈现的颜色就和三原色原理相关了,也就是红绿蓝,有个基本规律是数值越大越亮
draw.point((x, y), (r, g, b)) # 绘制颜色
# 定义边距和圆角半径
margin = 60 # 边距
corner_radius = 28 # 圆角的半径
shadow_offset = 10 # 阴影偏移量
shadow_blur = 15 # 阴影模糊半径
# 计算方形的左上角和右下角坐标
left = margin
top = margin
right = width - margin
bottom = height - margin
# 创建阴影图像
shadow_image = Image.new('RGBA', (width, height), (0, 0, 0, 0))
shadow_draw = ImageDraw.Draw(shadow_image)
# 阴影矩形的位置
shadow_rect = [left + shadow_offset, top + shadow_offset,
right + shadow_offset, bottom + shadow_offset]
# 绘制阴影矩形,确定四个角后绘制矩形
shadow_draw.rectangle(shadow_rect, fill=(0, 0, 0, 128))
# 应用模糊滤镜
shadow_image = shadow_image.filter(
ImageFilter.GaussianBlur(radius=shadow_blur))
# 将阴影合并到原图
base.paste(shadow_image, (0, 0), shadow_image)
# 绘制圆角矩形
draw = ImageDraw.Draw(base)
# 绘制矩形的四个边
color = (226, 200, 225) # 定义矩形的颜色
draw.rectangle([left + corner_radius, top, right -
corner_radius, bottom], fill=color)
draw.rectangle([left, top + corner_radius, right,
bottom - corner_radius], fill=color)
# 绘制四个角的圆
draw.pieslice([left, top, left + 2 * corner_radius, top +
2 * corner_radius], start=180, end=270, fill=color)
draw.pieslice([right - 2 * corner_radius, top, right, top +
2 * corner_radius], start=270, end=360, fill=color)
draw.pieslice([left, bottom - 2 * corner_radius, left + 2 *
corner_radius, bottom], start=90, end=180, fill=color)
draw.pieslice([right - 2 * corner_radius, bottom - 2 *
corner_radius, right, bottom], start=0, end=90, fill=color)
return base
1)因为我想要每行绘制11个字,Python里中英文字符宽度是1比1的,但是实际生成的文字,英文只有中文大小的一半。为了视觉上对齐中英文,需要编写一个计算中英文所占字符宽度的规则。
# 获取字符宽度
def get_char_width(char):
if ('\u4e00' <= char <= '\u9fff') or ('\u3000' <= char <= '\u303F') or ('\u0020' <= char <= '\u007F'): # 假设这个范围内的是中文字符 或者中文标点符号
char_width = 2
elif char == ')' or '(' or ',':
char_width = 2
else:
char_width = 1.1 # 定义单字所占文本长度
return char_width
2)一般来说,句首都不是标点符号
# 是否中英文标点符号
def is_cjk_or_english_punctuation(char):
if ('\u3000' <= char <= '\u303F' or '\u0020' <= char <= '\u007F'):
return True
else:
return False
# 开始编写换行规则, 句首不能是标点符号
def change_line(text):
# 这一步是处理从别的别的地方复制过来的文本
text = text.replace(u'\xa0', u' ') # 将不间断空白符 (NO-BREAK SPACE) 替换为标准空格
text = text.replace(u'\u3000', u' ') # 将全角空格 (IDEOGRAPHIC SPACE) 替换为标准空格
# 标点符号列表
punctuation = ['。', ',', '?', '!', ';', ':',
',', '.', '!', '?', ';', ':']
# 先定义初始变量, 用来储存文字和长度定义
current_width = 0 # 定义文本长度变量
final_text = [] # 定义储存所有句子的列表
current_line = '' # 定义初始句子长度, 用来储存句子
for char in text:
char_width = get_char_width(char)
if current_width + char_width <= 22: # 如果当前文本长度 和 当前的文字宽度相加还是小于等于限定的14
'''处理换行逻辑, 把换行符前面的变成一行, 因为这是遍历所有字符, 不添加这个逻辑, 原文 ”#学习原则
如果问题有替代方案,过“ 会变成 “['#学习原则\n\n如果问题', '有替代方案,暂时找不到', '原因,过']”'''
if char is '\n':
final_text.append(current_line)
current_line = ''
current_width = 0
continue
current_width += char_width # 更新当前文本长度
current_line += char # 把文字添加到句子
else:
# 如果这个字符是在标点符号范围内并且当前行末尾不是标点符号,
if is_cjk_or_english_punctuation(char) == True and current_line and is_cjk_or_english_punctuation(current_line[-1]) == False or char in punctuation:
last_char = current_line[-1] # 分离最后一个字符
final_text.append(current_line[:-1]) # 把分离最后一个字符后的句子添加到句子列表
current_line = last_char + char # 更新句子
current_width = get_char_width(
last_char) + get_char_width(char) # 更新句子长度
else:
final_text.append(current_line) # 把句子添加到句子列表
current_line = char # 更新这次循环的字符为下一行第一个字符
current_width = char_width # 更新句子长度
# 把最后一行添加进句子列表, 因为如果最后一句不满足14个字符或者15个的话, 不会被上面的逻辑添加进句子列表
if current_line:
final_text.append(current_line)
# 用换行符连接句子列表, 重新变成字符串
final_text = '\n'.join(final_text) # 用空格是因为已经分行了, 用'\n' 会在输入文本换行的时候多出空行
return final_text
3)开始绘制文字,在测试过程中不断调整图片宽度,文字大小,文字位置,等等。
# 绘制文字
def draw_text(img, final_text, height):
# 创建可绘制对象
draw = ImageDraw.Draw(img)
# 选择字体和字号
font = ImageFont.truetype('simhei.ttf', 30)
# 选择字体颜色
text_color = (0, 0, 0)
# 选择文字位置
text_position = (113, 175)
# 设定行距和字距
char_spacing = 2
line_spacing = 13
x, y = text_position # 解包字符初始绘制坐标
# 遍历文本的每一行,设定间距行距
for line in final_text.split('\n'):
if line.strip(): # 如果文本不是空行
current_x = x # 设置在这里实现重置每行起始绘制位置
for char in line: # 遍历一行的每个字符
# 绘制文字
draw.text((current_x, y), char, font=font, fill=text_color) # 绘制文字
current_x += font.getsize(char)[0] + char_spacing # 刷新绘制文字的横坐标
y += font.getsize(line)[1] + line_spacing # 放在句子的循环内实现控制行距
else:
y += font.getsize('枝')[1] + line_spacing
# 保存修改后的图片
# 指定字体路径
if hasattr(sys, '_MEIPASS'):
# 在打包后的环境中
font_path = os.path.join(
sys._MEIPASS, 'fonts', 'SmileySans-Oblique.otf')
else:
# 在开发环境中
font_path = os.path.join(os.path.dirname(
__file__), 'fonts', 'SmileySans-Oblique.otf')
# 绘制日期时间
draw.text((105, (height + 1) * font.getsize('枝')[1] + 170 + (13 * height) + 13), datetime.now().strftime(' %I:%M %p %m,%b %Y'),
font=ImageFont.truetype(font_path, 25), fill=(150, 150, 150)) # 日期时间
draw.text((175, 110), '枝枝',
font=ImageFont.truetype('simhei', 27), fill=(8, 4, 6)) # 作者署名
# 创建保存图片的文件夹
image_dir = 'D:\zhiz_image'
os.makedirs(image_dir, exist_ok=True)
# 生成图片名称
img_name = datetime.now().strftime('%Y-%m-%d_%H-%M-%S') + '.jpg'
# 指定保存路径
save_path = os.path.join(
image_dir, img_name)
# 打开头像文件
if hasattr(sys, '_MEIPASS'):
# 在打包后的环境中
avater_path = os.path.join(sys._MEIPASS, 'images', '枝枝头像.jpg')
else:
# 在开发环境中
avater_path = os.path.join(
os.path.dirname(__file__), 'images', '枝枝头像.jpg')
avater = Image.open(avater_path) # 打开头像文件
avater = avater.resize((60, 60), Image.Resampling.LANCZOS) # 把图片尺寸改小
img.paste(avater, (105, 90)) # 把头像粘贴到图片里, 参数是文件路径, 粘贴坐标
# 把最终制作好的图片复制到剪切板
output = io.BytesIO()
img.convert("RGB").save(output, "BMP")
data = output.getvalue()[14:]
output.close()
win32clipboard.OpenClipboard()
win32clipboard.EmptyClipboard()
win32clipboard.SetClipboardData(win32clipboard.CF_DIB, data)
win32clipboard.CloseClipboard()
# 保存图片
img.save(save_path)
# 让图片处于选中状态
subprocess.Popen(r'explorer /select,%s' %
save_path) # 意思是, 打开资源管理器/选中命令, 选中的文件路径,
绑定到UI界面里,方便打包为独立程序使用。
在这里使用上面编写好的函数,最后运行。
# Tkinter GUI 设置
def setup_gui():
root = tk.Tk()
root.title('枝枝宝')
# 设定UI界面大小、位置,我比较喜欢处于屏幕中间
window_width = 400
window_height = 400
screen_width = root.winfo_screenwidth()
screen_height = root.winfo_screenheight()
position_right = int(screen_width / 2 - window_width / 2)
position_top = int(screen_height / 2 - window_height / 2)
root.geometry('%dx%d+%d+%d' %
(window_width, window_height, position_right, position_top))
# 设定UI界面中的组件,例如文本框、按钮等。
a_label = tk.Label(root, text='枝枝宝请输入文字:')
a_label.grid(row=0, column=0)
a_text = tk.Text(root, height=17, width=28)
a_text.grid(row=0, column=1, padx=10, pady=10)
a_text.focus_set()
# 定义作图快捷键
def on_button_click(event=None):
text = a_text.get("1.0", "end-1c").rstrip()
a_text.delete('1.0', 'end')
a_text.insert('1.0', text)
print('原始文本是{}'.format(text))
final_text = change_line(text)
height = (final_text.count('\n') + 1)
print('最终文本是', final_text)
img = generate_gradient(height)
draw_text(img, final_text, height)
return 'break'
# 设定按钮来生成图片
button = tk.Button(root, text='枝枝宝大帅哥', command=on_button_click)
button.grid(row=2, column=1, columnspan=2, padx=10, pady=10)
root.bind('<Shift-Return>', lambda event: on_button_click(event))
root.mainloop()
# 主函数
if __name__ == "__main__":
setup_gui()
代码也不是很多,如果使用过程中遇到问题,欢迎来找我哦,如果能帮到你,非常开心。

