Skip to content
linebot-image-combo logo

linebot-image-combo

LINE Bot 題庫與筆記圖片製作的 Gemini + matplotlib 連續技。整合 AI 生成高品質底圖與程式化中文標籤,產出符合 LINE Bot 規格的圖片(< 300KB、手機可讀)。

NeverSight/skills_feed0installs214stars

SKILL.md

Full skill instructions

LINE Bot 圖片製作連續技

概述

結合 Gemini AI 生成高品質底圖 + matplotlib 加上正確中文標籤的連續技,專為 LINE Bot 題庫與筆記設計。

為什麼需要連續技?

方法優點缺點
純 matplotlib中文標籤正確圖形簡陋、不夠精緻
純 Gemini圖片品質高中文常出錯(亂碼)
連續技品質高 + 中文正確需要兩步驟

LINE Bot 圖片規格(重要!)

硬性限制

項目限制說明
檔案大小< 300KB超過會載入緩慢或失敗
圖片格式JPEG, PNG不支援 GIF 動圖
圖片來源HTTPS必須是安全連線
Hero 比例4:3 或 16:9Flex Message 推薦

手機閱讀限制

LINE Bot 圖片在手機上無法放大! 必須確保:

  • 字體夠大(手機上直接可讀)
  • 圖片不要太寬(手機螢幕約 360-400px)
  • 重要資訊不要太密集

優化後的圖片規格

尺寸標準(針對 LINE Bot 優化)

用途figsizeDPI輸出尺寸檔案大小
題庫圖片(8, 6)120~960×720px< 150KB
筆記圖片(8, 8)120~960×960px< 200KB
橫向圖表(10, 6)120~1200×720px< 180KB
大型圖(需壓縮)(10, 8)100~1000×800px< 250KB

字體大小標準(手機可讀)

# LINE Bot 專用字體大小(比一般用途更大)
FONT_TITLE = 24      # 圖片標題
FONT_LABEL = 18      # 標籤文字(主要內容)
FONT_SUBLABEL = 14   # 次要標籤
FONT_NOTE = 12       # 註解(最小值,謹慎使用)

對比原本的設定:

項目原設定LINE Bot 優化原因
figsize(14, 12)(8, 6)原本太大,檔案超過 300KB
DPI150120降低以控制檔案大小
標籤字體11-1216-18原本在手機上太小

連續技流程

Step 1:Gemini 生成無標籤底圖

cd ~/​.claude/​skills/​fanpage-article-writer/​scripts

# 生成無標籤的高品質圖片
python generate_image.py "圖片描述,WITHOUT any text labels" \
    --output "底圖.png" \
    --model gemini

Prompt 技巧:

✅ 正確:
"A detailed anatomical diagram of [主題].
Clean medical illustration style WITHOUT any text labels or annotations.
High quality educational diagram, white background."

❌ 錯誤:
"心臟解剖圖,標示中文說明"  # 中文標籤會出錯

Step 2:matplotlib 加中文標籤

# -*- coding: utf-8 -*-
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
from PIL import Image
import io

# 中文字體設定(必須)
plt.rcParams['font.sans-serif'] = ['Microsoft JhengHei', 'SimHei', 'Arial Unicode MS']
plt.rcParams['axes.unicode_minus'] = False

def add_labels_for_linebot(base_image_path, output_path, labels, title):
    """
    為 Gemini 底圖加上中文標籤(LINE Bot 優化版)

    Parameters:
    - base_image_path: Gemini 生成的底圖路徑
    - output_path: 輸出路徑
    - labels: 標籤列表 [(x%, y%, 文字, 箭頭x%, 箭頭y%), ...]
    - title: 圖片標題
    """
    img = mpimg.imread(base_image_path)

    # LINE Bot 優化尺寸
    fig, ax = plt.subplots(figsize=(8, 6))
    ax.imshow(img)
    ax.axis('off')

    h, w = img.shape[:2]

    for lx, ly, text, ax_x, ax_y in labels:
        x = lx * w
        y = ly * h
        arrow_x = ax_x * w
        arrow_y = ax_y * h

        bbox_props = dict(
            boxstyle='round,pad=0.3',
            facecolor='white',
            edgecolor='#333333',
            alpha=0.92,
            linewidth=1.5
        )

        ax.annotate(
            text,
            xy=(arrow_x, arrow_y),
            xytext=(x, y),
            fontsize=16,  # LINE Bot 優化:字體加大
            fontweight='bold',
            ha='center',
            va='center',
            bbox=bbox_props,
            arrowprops=dict(
                arrowstyle='-|>',
                color='#333333',
                lw=1.5,
                connectionstyle='arc3,rad=0.1'
            )
        )

    ax.set_title(title, fontsize=20, fontweight='bold', pad=10)

    # 儲存為 LINE Bot 優化格式
    save_for_linebot(fig, output_path)
    plt.close()


def save_for_linebot(fig, output_path, max_size_kb=280):
    """
    儲存圖片並確保 < 300KB

    自動調整品質以符合 LINE Bot 限制
    """
    # 先儲存為 PNG
    buf = io.BytesIO()
    fig.savefig(buf, format='png', dpi=120, bbox_inches='tight',
                facecolor='white', pad_inches=0.05)
    buf.seek(0)

    # 檢查大小
    size_kb = len(buf.getvalue()) / 1024

    if size_kb <= max_size_kb:
        # 大小 OK,直接儲存
        with open(output_path, 'wb') as f:
            f.write(buf.getvalue())
        print(f"已儲存: {output_path} ({size_kb:.1f} KB)")
    else:
        # 太大,轉換為 JPEG 並壓縮
        img = Image.open(buf)
        if img.mode == 'RGBA':
            img = img.convert('RGB')

        # 嘗試不同品質
        for quality in [85, 75, 65, 55]:
            jpeg_buf = io.BytesIO()
            img.save(jpeg_buf, format='JPEG', quality=quality, optimize=True)
            jpeg_size = len(jpeg_buf.getvalue()) / 1024

            if jpeg_size <= max_size_kb:
                jpeg_path = output_path.rsplit('.', 1)[0] + '.jpg'
                with open(jpeg_path, 'wb') as f:
                    f.write(jpeg_buf.getvalue())
                print(f"已儲存: {jpeg_path} ({jpeg_size:.1f} KB, quality={quality})")
                return

        # 如果還是太大,縮小尺寸
        img.thumbnail((800, 800), Image.LANCZOS)
        jpeg_buf = io.BytesIO()
        img.save(jpeg_buf, format='JPEG', quality=70, optimize=True)
        jpeg_path = output_path.rsplit('.', 1)[0] + '.jpg'
        with open(jpeg_path, 'wb') as f:
            f.write(jpeg_buf.getvalue())
        print(f"已儲存(縮小): {jpeg_path} ({len(jpeg_buf.getvalue())/​1024:.1f} KB)")

完整範例:心臟解剖圖

1. 生成底圖

cd ~/​.claude/​skills/​fanpage-article-writer/​scripts

python generate_image.py \
    "A detailed anatomical cross-section of the human heart showing four chambers, septum, valves, and major blood vessels. Clean medical illustration style WITHOUT any text labels. Blue for right side, red for left side. White background." \
    --output "C:/​Users/​user/​Documents/​temp/​heart_base.png" \
    --model gemini

2. 加標籤腳本

# heart_linebot.py
# -*- coding: utf-8 -*-
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
from PIL import Image
import io

plt.rcParams['font.sans-serif'] = ['Microsoft JhengHei', 'SimHei']
plt.rcParams['axes.unicode_minus'] = False

def create_heart_diagram():
    img = mpimg.imread('C:/​Users/​user/​Documents/​temp/​heart_base.png')

    # LINE Bot 優化尺寸
    fig, ax = plt.subplots(figsize=(8, 6))
    ax.imshow(img)
    ax.axis('off')

    h, w = img.shape[:2]

    # 標籤:(標籤x%, 標籤y%, 文字, 箭頭x%, 箭頭y%)
    labels = [
        (0.08, 0.15, '上腔靜脈', 0.22, 0.20),
        (0.50, 0.05, '主動脈', 0.42, 0.18),
        (0.85, 0.10, '肺動脈', 0.60, 0.18),
        (0.05, 0.40, '右心房', 0.25, 0.38),
        (0.92, 0.35, '左心房', 0.72, 0.35),
        (0.05, 0.65, '右心室', 0.35, 0.60),
        (0.92, 0.65, '左心室', 0.65, 0.60),
        (0.50, 0.90, '心室中隔', 0.50, 0.70),
    ]

    for lx, ly, text, ax_x, ax_y in labels:
        ax.annotate(
            text,
            xy=(ax_x * w, ax_y * h),
            xytext=(lx * w, ly * h),
            fontsize=16,
            fontweight='bold',
            ha='center',
            va='center',
            bbox=dict(boxstyle='round,pad=0.3', facecolor='white',
                     edgecolor='#333', alpha=0.9),
            arrowprops=dict(arrowstyle='-|>', color='#333', lw=1.5)
        )

    # 圖例
    ax.text(0.20 * w, 0.95 * h, '● 含氧血', fontsize=12, color='#C62828',
            fontweight='bold', bbox=dict(facecolor='white', alpha=0.9))
    ax.text(0.75 * w, 0.95 * h, '● 缺氧血', fontsize=12, color='#1565C0',
            fontweight='bold', bbox=dict(facecolor='white', alpha=0.9))

    ax.set_title('心臟結構解剖圖', fontsize=18, fontweight='bold', pad=8)

    # 儲存(自動壓縮)
    save_for_linebot(fig, 'heart_anatomy_linebot.png')
    plt.close()


def save_for_linebot(fig, output_path, max_kb=280):
    """儲存並壓縮到 LINE Bot 規格"""
    buf = io.BytesIO()
    fig.savefig(buf, format='png', dpi=120, bbox_inches='tight',
                facecolor='white', pad_inches=0.03)
    buf.seek(0)
    size_kb = len(buf.getvalue()) / 1024

    if size_kb <= max_kb:
        with open(output_path, 'wb') as f:
            f.write(buf.getvalue())
        print(f"已儲存: {output_path} ({size_kb:.1f} KB)")
    else:
        # 轉 JPEG 壓縮
        img = Image.open(buf).convert('RGB')
        for q in [85, 75, 65]:
            jbuf = io.BytesIO()
            img.save(jbuf, 'JPEG', quality=q, optimize=True)
            if len(jbuf.getvalue())/​1024 <= max_kb:
                jpg_path = output_path.replace('.png', '.jpg')
                with open(jpg_path, 'wb') as f:
                    f.write(jbuf.getvalue())
                print(f"已儲存: {jpg_path} ({len(jbuf.getvalue())/​1024:.1f} KB)")
                return
        print("警告:無法壓縮到目標大小")


if __name__ == '__main__':
    create_heart_diagram()

API Key 管理

Gemini API Key 設定位置:

~/​.claude/​skills/​fanpage-article-writer/​scripts/​api_keys.json

檢查 API Key 狀態:

cd ~/​.claude/​skills/​fanpage-article-writer/​scripts
python generate_image.py --status

圖片命名規範

題庫圖片

ch{章}-{節}-q{題號}-{描述}.png      # 題目圖
ch{章}-{節}-a{題號}-{描述}.png      # 答案解析圖

筆記圖片

{主題}_{內容}.png
例:heart_anatomy.png, heart_conduction.png

常見問題

圖片太大(> 300KB)

  1. 降低 DPI:dpi=100 或 dpi=80
  2. 縮小 figsize:figsize=(6, 4)
  3. 轉 JPEG:quality=75
  4. 縮小尺寸:img.thumbnail((800, 800))

字體太小

  • 標籤字體至少 fontsize=14
  • 標題字體至少 fontsize=18
  • 避免密集標籤,必要時分多張圖

Gemini 中文出錯

  • Prompt 中加 WITHOUT any text labels
  • 使用 matplotlib 後製所有文字

LINE 圖片快取

更新圖片後加版本參數:

https://example.com/images/heart.png?v=2

相關 Skill

  • linebot-notebook-design:Flex Message 設計規範
  • quiz-builder:題庫系統專用指南
  • fanpage-article-writer:Gemini API 腳本位置

Noto Sans CJK TC 字體(罕見中文字支援)

問題

微軟正黑體不支援某些罕見中文字,例如超重元素 105-118 的名稱:

原子序符號中文名微軟正黑體
105Db𨧀⬜ 顯示為方框
106Sg𨭎⬜ 顯示為方框
107Bh𨨏⬜ 顯示為方框
108Hs𨭆⬜ 顯示為方框
117Ts鿬⬜ 顯示為方框
118Og鿫⬜ 顯示為方框

解決方案:使用 Noto Sans CJK TC

Google 的 Noto Sans CJK TC 字體完整支援所有 CJK 字符。

下載字體:

# npm 安裝方式
npm install @aspect-build/​aspect-fonts-noto-sans-cjk-tc

# 或直接下載 OTF 檔
# https://github.com/googlefonts/noto-cjk/releases

matplotlib 使用方式:

import matplotlib.font_manager as fm
import matplotlib.patheffects as path_effects

# 載入 Noto Sans CJK TC 字體
NOTO_FONT_PATH = "C:/​Users/​user/​Documents/​temp/​NotoSansCJKtc-Regular.otf"
noto_font = fm.FontProperties(fname=NOTO_FONT_PATH)

# 使用 fontproperties 參數
ax.text(0.95, 0.08, "鿬",  # 罕見字
        fontsize=48, color='white', fontweight='bold',
        fontproperties=noto_font,  # 指定字體
        path_effects=[
            path_effects.withStroke(linewidth=3, foreground='black')  # 黑邊
        ])

重點:

  • 使用 fontproperties= 參數,不是 fontfamily=
  • path_effects.withStroke() 可加黑邊讓白字更清晰
  • 字體檔約 16MB,不要放進 Git repo

Breaking Bad 風格元素圖片生成

使用 Gemini API 直接生成 Breaking Bad 風格的元素週期表 hero 圖片。

API 直接呼叫方式

from google import genai
from google.genai import types

# 初始化 client
client = genai.Client(api_key=API_KEY)

# 生成圖片
response = client.models.generate_content(
    model="gemini-2.0-flash-exp",
    contents=prompt,
    config=types.GenerateContentConfig(
        response_modalities=['image', 'text']  # 重要!
    )
)

# 取得圖片資料
for part in response.candidates[0].content.parts:
    if part.inline_data is not None:
        image_data = part.inline_data.data
        with open(output_path, 'wb') as f:
            f.write(image_data)

Breaking Bad 風格 Prompt 範例

def generate_prompt(element, color_hex):
    symbol = element['symbol']  # e.g., "H"
    rest_name = "ydrogen"       # 去掉符號後的名稱
    atomic_num = 1
    atomic_mass = "1.008"

    return f"""Create a Breaking Bad TV series style periodic table element card.

CRITICAL REQUIREMENTS:
1. BACKGROUND: Dark navy blue (#1a1a2e) with {color_hex} colored smoke/​vapor filling the ENTIRE image background.

2. ELEMENT BOX (center-left):
   - Green square box (#00A300) with white border (3px)
   - Large white letter '{symbol}' in the center (bold, sans-serif)
   - Small white number '{atomic_num}' in top-right corner of the box

3. TEXT (to the right of the box):
   - '{rest_name}' in white, same baseline as the symbol
   - Below that: '{atomic_mass}' in smaller white text

4. LAYOUT:
   - Design should be centered and fit a 2:1 aspect ratio (wide format)
   - NO solid colored panels blocking the smoke background

5. STYLE:
   - Cinematic, dramatic lighting
   - Similar to Breaking Bad title card aesthetic

DO NOT add any other text, watermarks, or decorations."""

後處理:裁切 + 中文標籤 + 壓縮

def post_process_image(input_path, output_path, element, target_width=1200):
    """後處理:裁切成 2:1、加中文標籤、壓縮到 < 300KB"""
    from PIL import Image
    import matplotlib.pyplot as plt
    import matplotlib.patheffects as path_effects
    import matplotlib.font_manager as fm

    # 載入 Noto 字體
    noto_font = fm.FontProperties(fname=NOTO_FONT_PATH)

    # 開啟圖片
    img = Image.open(input_path)
    width, height = img.size

    # 裁切成 2:1 比例(從中心裁切)
    target_height = width // 2
    if height > target_height:
        top = (height - target_height) // 2
        img = img.crop((0, top, width, top + target_height))

    # 調整大小
    target_height = target_width // 2
    img = img.resize((target_width, target_height), Image.Resampling.LANCZOS)

    # 轉換為 RGB
    if img.mode != 'RGB':
        background = Image.new('RGB', img.size, (26, 26, 46))
        if img.mode == 'RGBA':
            background.paste(img, mask=img.split()[3])
        else:
            background.paste(img)
        img = background

    # 用 matplotlib 加中文標籤
    fig, ax = plt.subplots(figsize=(12, 6), dpi=100)
    ax.imshow(img)
    ax.axis('off')

    # 加上中文名稱(右下角)
    ax.text(0.95, 0.08, element['name_zh'],
            transform=ax.transAxes,
            fontsize=48, color='white', fontweight='bold',
            ha='right', va='bottom',
            fontproperties=noto_font,
            path_effects=[
                path_effects.withStroke(linewidth=3, foreground='black')
            ])

    # 儲存並壓縮
    plt.tight_layout(pad=0)
    plt.savefig(output_path, dpi=100, bbox_inches='tight', pad_inches=0,
                format='jpeg', quality=85)
    plt.close()

生成結果

  • 輸出尺寸:1200×600px(2:1 比例)
  • 檔案大小:~80-150KB(符合 LINE Bot 限制)
  • 風格:Breaking Bad 電視劇風格,深藍背景 + 彩色煙霧

Windows Console 編碼問題

在 Windows 上執行 Python 腳本時,若遇到 UnicodeEncodeError,在程式開頭加入:

import sys
if sys.platform == 'win32':
    sys.stdout.reconfigure(encoding='utf-8', errors='replace')

變更記錄

日期版本變更內容
2026-01-181.0初版制定,整合 Gemini + matplotlib 連續技
2026-01-201.1新增 Noto Sans CJK TC 字體、Breaking Bad 風格生成、Windows 編碼修正