python将Excel数据写进图片中

需求

一张图片模板,里面有两个字段,内容都要来自Excel,Excel里,对应两个字段。

分析

针对这种需求,没有代码基础的人可以直接使用ppt+Excel的结合的方式。有代码基础的人我直接推荐接下来的代码。

目录结构

auto.py就是主文件内容就是下面的代码。image.png就是要被操作的图片模板。test.xlsx是需要对应放上图片的数据。以本例子为例就是名称和编码的内容。

代码

python 复制代码
# -*- coding: utf-8 -*-
"""
物料标识卡(小模板 299x92)自动生成
- 写字区域用相对比例,适配任意尺寸模板
- 名称自动换行(最多2行),编码单行
- 自动修正无效矩形(left>right / 越界)
- 支持红框调试与放大渲染(提升清晰度)
依赖: pip install pillow pandas openpyxl
"""

import os, re
import pandas as pd
from PIL import Image, ImageDraw, ImageFont

# ========= 基本配置 =========
TEMPLATE = "image.png"      # 模板图(当前这张小模板)
EXCEL    = "test.xlsx"      # 数据表
OUTDIR   = "output"         # 输出目录

# 兼容列名
CAND_CODE = ["物料编码","编码","料号","物料代号","物料編碼"]
CAND_NAME = ["物料名称","名称","品名","物料名稱"]

# 字体(按系统情况自选一个存在的)
FONT_CANDIDATES = [
    "C:/Windows/Fonts/msyh.ttc",     # 微软雅黑
    "C:/Windows/Fonts/simsun.ttc",   # 宋体
    "/System/Library/Fonts/STHeiti Light.ttc",  # mac 旧系统
    "/System/Library/Fonts/PingFang.ttc",       # mac 苹方(若存在)
]
COLOR = (0, 68, 178)   # 文字颜色(与模板蓝一致或相近)

# ========= 版面(相对比例) =========
# 这组比例已针对 299×92 的模板精调,如对不上可微调 0.01~0.02
# (left, top, right, bottom) 都是 0~1 的比例
NAME_BOX_RATIO = (0.19, 0.50, 0.46, 0.74)  # "名称"字段的可写区做上右下
CODE_BOX_RATIO = (0.74, 0.55, 0.97, 0.74)  # "物料编码"字段的可写区 (0.74, 0.60, 0.97, 0.86

# 细微像素级偏移(先保持 0,0;想再贴线就 +-1~2 调)
NUDGE_NAME = (0, 0)
NUDGE_CODE = (0, 0)

# 对齐与字号
V_RATIO = 0.50           # 垂直几何居中(0.5)
H_ALIGN = 'left'         # 'left' 或 'right'
MIN_FONT      = 18
NAME_MAX_FONT = 50
CODE_MAX_FONT = 50

# 其他
SAFE_MARGIN_PX = 6       # 防止贴边
DRAW_GUIDE = False       # True 显示红框调试
UPSCALE = 3              # 放大渲染倍数:1/2/3(2或3更清晰)

# ========= 工具函数 =========
def choose_font(size: int):
    for fp in FONT_CANDIDATES:
        try:
            return ImageFont.truetype(fp, size)
        except Exception:
            continue
    # 全部失败则退回默认位图字体
    return ImageFont.load_default()

def text_wh(draw, txt, font):
    x1, y1, x2, y2 = draw.textbbox((0, 0), txt, font=font)
    return x2 - x1, y2 - y1

def fit_font(draw, txt, max_w, max_font, min_font=MIN_FONT, safe_margin=SAFE_MARGIN_PX):
    target_w = max(1, max_w - safe_margin)
    for sz in range(max_font, min_font - 1, -1):
        f = choose_font(sz)
        w, _ = text_wh(draw, txt, f)
        if w <= target_w:
            return f
    return choose_font(min_font)

def norm(s: str) -> str:
    s = re.sub(r"\s+", "", str(s or "")).lower()
    s = s.replace("編","编").replace("名稱","名称")
    return s

def find_col(df: pd.DataFrame, cands):
    nm = {c: norm(c) for c in df.columns}
    cs = [norm(x) for x in cands]
    # 完全匹配优先
    for col, n in nm.items():
        if n in cs:
            return col
    # 子串次之
    for col, n in nm.items():
        if any(c in n for c in cs):
            return col
    raise KeyError(f"找不到列:{cands}")

def normalize_box(box, W, H, min_w=2, min_h=2):
    l, t, r, b = box
    # 排序,保证 l<=r, t<=b
    if l > r: l, r = r, l
    if t > b: t, b = b, t
    # 夹到边界
    l = max(0, min(l, W-1)); r = max(0, min(r, W-1))
    t = max(0, min(t, H-1)); b = max(0, min(b, H-1))
    # 保证最小宽高(防止 0 宽/高引发报错)
    if r - l < min_w: r = min(W-1, l + min_w)
    if b - t < min_h: b = min(H-1, t + min_h)
    return (l, t, r, b)

def ratio2box(ratio_box, W, H, nudge=(0,0)):
    l = int(ratio_box[0] * W) + nudge[0]
    t = int(ratio_box[1] * H) + nudge[1]
    r = int(ratio_box[2] * W) + nudge[0]
    b = int(ratio_box[3] * H) + nudge[1]
    return normalize_box((l, t, r, b), W, H)

def draw_in_box(draw, text, box, v_ratio=0.5, h_align='left', max_font=46):
    l, t, r, b = box
    max_w = r - l
    font = fit_font(draw, text, max_w, max_font=max_font)
    w, h = text_wh(draw, text, font)
    # 标准几何居中:去除之前实现里的 "*2"
    y = int(t + (b - t - h) * v_ratio)
    x = (r - w) if h_align == 'right' else l
    draw.text((x, y), text, font=font, fill=COLOR)

def _wrap_fit_lines(draw, text, box, max_font, min_font=MIN_FONT, max_lines=2,
                    safe_margin=SAFE_MARGIN_PX, line_spacing=1.08):
    l, t, r, b = box
    max_w = r - l - safe_margin
    max_h = b - t - safe_margin
    seps = ['/', '/', '、', ',', ',', ' ']

    def tokenize(s):
        tokens, cur = [], ''
        for ch in str(s).replace('\n', ' '):
            if ch in seps:
                if cur: tokens.append(cur); cur = ''
                tokens.append(ch)
            else:
                cur += ch
        if cur: tokens.append(cur)
        return tokens

    tokens = tokenize(text)
    for sz in range(max_font, min_font - 1, -1):
        f = choose_font(sz)
        lines, cur = [], ''
        for tk in tokens:
            trial = cur + tk
            w, _ = text_wh(draw, trial, f)
            if w <= max_w or cur == '':
                cur = trial
            else:
                lines.append(cur); cur = tk
            if len(lines) >= max_lines:
                break
        if cur and len(lines) < max_lines:
            lines.append(cur)

        # 对超宽行做硬切保障
        def hard_wrap(s):
            out, buf = [], ''
            for ch in s:
                trial = buf + ch
                w, _ = text_wh(draw, trial, f)
                if w <= max_w or buf == '':
                    buf = trial
                else:
                    out.append(buf); buf = ch
            if buf: out.append(buf)
            return out

        fixed = []
        for ln in lines:
            if text_wh(draw, ln, f)[0] <= max_w:
                fixed.append(ln)
            else:
                fixed.extend(hard_wrap(ln))
            if len(fixed) > max_lines:
                fixed = fixed[:max_lines]; break

        if not fixed:
            continue

        line_h = int(sz * line_spacing)
        if line_h * len(fixed) <= max_h:
            return f, fixed[:max_lines]

    # 兜底:省略号
    f = choose_font(min_font)
    lines = [''] * max_lines
    i = 0; buf = ''
    for ch in ''.join(tokens):
        trial = buf + ch
        if text_wh(draw, trial, f)[0] <= max_w:
            buf = trial
        else:
            lines[i] = buf; i += 1; buf = ch
            if i >= max_lines: break
    if i < max_lines: lines[i] = buf
    last = lines[-1]
    while last and text_wh(draw, last + '...', f)[0] > max_w:
        last = last[:-1]
    lines[-1] = (last + '...') if last else '...'
    return f, lines

def draw_wrapped_in_box(draw, text, box, v_ratio=0.5, h_align='left',
                        max_font=46, min_font=MIN_FONT, max_lines=2, line_spacing=1.08):
    l, t, r, b = box
    f, lines = _wrap_fit_lines(draw, text, box, max_font, min_font, max_lines, line_spacing)
    line_h = int(f.size * line_spacing)
    total_h = line_h * len(lines)
    y0 = int(t + (b - t - total_h) * v_ratio)
    for idx, ln in enumerate(lines):
        w, h = text_wh(draw, ln, f)
        x = (r - w) if h_align == 'right' else l
        y = y0 + idx * line_h
        draw.text((x, y), ln, font=f, fill=COLOR)

# ========= 主流程 =========
def main():
    os.makedirs(OUTDIR, exist_ok=True)
    df = pd.read_excel(EXCEL, dtype=str).fillna("")
    base = Image.open(TEMPLATE).convert("RGBA")

    # 放大渲染(整体先放大再写字,字更锐利)
    if UPSCALE > 1:
        W0, H0 = base.size
        base = base.resize((W0*UPSCALE, H0*UPSCALE), resample=Image.LANCZOS)

    W, H = base.size
    name_box = ratio2box(NAME_BOX_RATIO, W, H, NUDGE_NAME)
    code_box = ratio2box(CODE_BOX_RATIO, W, H, NUDGE_CODE)

    # 调试输出
    print("Template size:", (W, H))
    print("name_box:", name_box, "code_box:", code_box)

    col_code = find_col(df, CAND_CODE)
    col_name = find_col(df, CAND_NAME)

    for i, row in df.iterrows():
        code = str(row.get(col_code, "")).strip()
        name = str(row.get(col_name, "")).strip()

        img = base.copy()
        draw = ImageDraw.Draw(img)

        if DRAW_GUIDE:
            draw.rectangle(name_box, outline=(255, 0, 0), width=1)
            draw.rectangle(code_box, outline=(255, 0, 0), width=1)

        # 编码:单行
        draw_in_box(draw, code, code_box, v_ratio=V_RATIO, h_align=H_ALIGN, max_font=CODE_MAX_FONT)
        # 名称:自动换行(最多两行)
        draw_wrapped_in_box(draw, name, name_box, v_ratio=V_RATIO, h_align=H_ALIGN,
                            max_font=NAME_MAX_FONT, min_font=MIN_FONT, max_lines=2, line_spacing=1.08)

        out = os.path.join(OUTDIR, f"{code or '未命名'}_{i+1}.png")
        img.save(out, dpi=(300, 300))
        print("Saved:", out)

    print("\n✅ 完成,输出目录:", os.path.abspath(OUTDIR))

if __name__ == "__main__":
    main()

效果


扩展

我如何调整数据的位置 比如 觉得他在虚线太靠上了,怎么做呢。我如果图片换模板,该怎么做呢。就需要回到代码去调整。

复制代码
NAME_BOX_RATIO = (0.19, 0.50, 0.46, 0.74)  # "名称"字段的可写区做上右下
CODE_BOX_RATIO = (0.74, 0.55, 0.97, 0.74)  # "物料编码"字段的可写区 (0.74, 0.60, 0.97, 0.86

这部分就是的大小调整。这个框可以让他暂时显示

复制代码
DRAW_GUIDE = False       # True 显示红框调试

调整上述的参数就可以调整框的大小,进而调整字体自适应大小。

相关推荐
老友@2 小时前
Java Excel 导出:EasyExcel 使用详解
java·开发语言·excel·easyexcel·excel导出
xiaoxiongip6662 小时前
假设两个设备在不同网段,网关怎么设置才能通呢
网络·爬虫·python·https·智能路由器
逻极2 小时前
Scikit-learn 实战:15 分钟构建生产级中国房价预测模型
python·机器学习·scikit-learn
行板Andante2 小时前
AttributeError: ‘super‘ object has no attribute ‘sklearn_tags‘解决
人工智能·python·sklearn
tryCbest2 小时前
Python基础之爬虫技术(一)
开发语言·爬虫·python
husterlichf2 小时前
逻辑回归以及python(sklearn)详解
python·逻辑回归·sklearn
hixiong1232 小时前
C# OpenCVSharp实现Hand Pose Estimation Mediapipe
开发语言·opencv·ai·c#·手势识别
集成显卡2 小时前
AI取名大师 | PM2 部署 Bun.js 应用及配置 Let‘s Encrypt 免费 HTTPS 证书
开发语言·javascript·人工智能
AI小云2 小时前
【Numpy数据运算】数组间运算
开发语言·python·numpy