用Pillow打造水井三寶地方圖卡處理工具
讓烏龜、白馬與姻緣花的照片,變成尺寸一致、文字清楚、可追溯來源的地方圖卡。
完成本篇後,你能安全開啟與驗證圖片、依EXIF修正方向、比較thumbnail與fit、加入繁體中文標題及半透明浮水印、正確輸出JPEG/PNG、批次產生圖卡與處理紀錄,並說明肖像權、著作權、位置資訊及AI生成標示。
一、地方圖卡不是「把字貼上去」而已
同一張水井三寶照片可能來自手機、相機、社區長者或AI創作,具有不同方向、尺寸、色彩模式、檔案格式及使用權。圖卡工具至少要回答四件事:
| 問題 | 程式處理 | USR責任 |
|---|---|---|
| 圖片能否安全讀取? | 限制大小、辨識格式、捕捉錯誤 | 不接受不明來源大檔直接進系統 |
| 版型是否一致? | 轉向、縮放、裁切與色彩模式 | 不扭曲人物、工藝或地方物件 |
| 文字是否清楚? | 字型、文字框、對比與安全邊界 | 名稱與故事先由社區確認 |
| 能否合法公開? | 輸出時移除不必要metadata | 確認授權、肖像同意及AI標示 |
二、安裝Pillow:安裝名稱與匯入名稱不同
import subprocess
import sys
subprocess.run(
[sys.executable, "-m", "pip", "install", "Pillow"],
check=True,
)pip安裝名稱是Pillow,程式匯入名稱仍是PIL。請在專案虛擬環境安裝,並把實際驗證版本記錄在依賴檔中。
三、讀取圖片基本資訊
from pathlib import Path
from PIL import Image
source = Path("images/turtle.jpg")
with Image.open(source) as image:
print("格式:", image.format)
print("尺寸:", image.size)
print("色彩模式:", image.mode)
print("影格數:", getattr(image, "n_frames", 1))Image.open()會延遲讀取像素,因此要使用with確保檔案關閉。不要只依副檔名判定內容;Pillow會由檔案內容辨識格式。
四、不可信圖片先驗證,再重新開啟
import warnings
from pathlib import Path
from PIL import Image, UnidentifiedImageError
MAX_FILE_BYTES = 15 * 1024 * 1024
ALLOWED_FORMATS = {"JPEG", "PNG", "WEBP"}
def validate_image(path):
path = Path(path)
if not path.is_file():
raise FileNotFoundError(path)
if path.stat().st_size > MAX_FILE_BYTES:
raise ValueError("檔案超過15 MB限制")
with warnings.catch_warnings():
warnings.simplefilter("error", Image.DecompressionBombWarning)
try:
with Image.open(path) as image:
detected_format = image.format
if detected_format not in ALLOWED_FORMATS:
raise ValueError("不支援的圖片格式")
image.verify()
except UnidentifiedImageError as error:
raise ValueError("無法辨識圖片") from error
return detected_formatverify()檢查後必須重新開啟圖片,不能繼續用同一物件處理像素。檔案大小與像素量是兩種不同風險;Pillow會對異常巨大像素數提出decompression bomb警告或錯誤,不應為方便直接關閉保護。
五、手機照片方向錯誤:套用EXIF Orientation
from PIL import Image, ImageOps
def open_oriented_rgb(path):
validate_image(path)
with Image.open(path) as image:
oriented = ImageOps.exif_transpose(image)
return oriented.convert("RGB")
image = open_oriented_rgb("images/turtle.jpg")
print(image.size, image.mode)ImageOps.exif_transpose()依EXIF方向旋轉或鏡射像素,並處理方向資訊。若略過這一步,手機上直立的照片在輸出後可能橫躺。
六、thumbnail:完整保留、放進指定範圍
from pathlib import Path
from PIL import Image
image = open_oriented_rgb("images/turtle.jpg")
thumbnail = image.copy()
thumbnail.thumbnail(
(800, 800),
resample=Image.Resampling.LANCZOS,
)
Path("output").mkdir(parents=True, exist_ok=True)
thumbnail.save("output/turtle_thumbnail.jpg", quality=88)
print(thumbnail.size)thumbnail()會原地修改圖片,保持長寬比並限制在指定框內,通常不會放大小圖。它適合完整保留照片,但輸出寬高不一定完全一致。
七、ImageOps.fit:裁成一致的社群圖卡
from PIL import Image, ImageOps
image = open_oriented_rgb("images/white_horse.jpg")
card_photo = ImageOps.fit(
image,
(1200, 800),
method=Image.Resampling.LANCZOS,
centering=(0.5, 0.45),
)
card_photo.save("output/white_horse_1200x800.jpg", quality=90)
print(card_photo.size)fit()會等比例縮放後裁切,得到固定尺寸。centering可以調整保留重心;人物、樹木或工藝品不能一律置中,最好提供裁切預覽讓社區夥伴確認。
八、不要用resize硬拉成固定尺寸
from PIL import ImageOps, Image
def make_card_background(image, size=(1200, 1200), mode="crop"):
if mode == "crop":
return ImageOps.fit(
image, size, method=Image.Resampling.LANCZOS
)
if mode == "contain":
return ImageOps.pad(
image,
size,
method=Image.Resampling.LANCZOS,
color=(245, 240, 224),
)
raise ValueError("mode必須是crop或contain")crop填滿版面但會裁掉邊緣;contain完整保留但可能出現留白。直接 resize((1200,1200))會改變比例,使姻緣花、烏龜或人物變形。
九、繁體中文字型要由專案明確提供
不同電腦的中文字型位置不同。最穩定做法是在取得合法再散布授權後,將指定字型作為專案資產,或由部署環境透過設定提供路徑。
import os
from pathlib import Path
from PIL import ImageFont
def load_font(size):
font_path = Path(os.environ["SHUIJING_FONT_PATH"])
if not font_path.is_file():
raise FileNotFoundError("找不到指定中文字型")
return ImageFont.truetype(str(font_path), size=size)
title_font = load_font(64)不要假設Arial、微軟正黑體或Noto一定存在;也不要未確認授權就把系統字型複製到公開專案。
十、使用textbbox量字,再畫文字底板
from PIL import ImageDraw
def draw_title(image, title, font, margin=48):
draw = ImageDraw.Draw(image, "RGBA")
box = draw.textbbox((0, 0), title, font=font)
text_width = box[2] - box[0]
text_height = box[3] - box[1]
x = margin
y = image.height - text_height - margin * 2
panel = (
x - 20,
y - 16,
min(x + text_width + 20, image.width - margin),
y + text_height + 20,
)
draw.rounded_rectangle(
panel, radius=18, fill=(13, 66, 55, 205)
)
draw.text(
(x, y), title, font=font,
fill=(255, 255, 255, 255),
stroke_width=1,
stroke_fill=(0, 0, 0, 180),
)
card = make_card_background(
open_oriented_rgb("images/marriage_flower.jpg")
)
draw_title(card, "水井三寶|姻緣花", load_font(64))textbbox()可先取得文字範圍,再決定底板大小。長標題仍可能超出畫面,下一節加入自動縮小。
十一、讓長標題自動縮小
from PIL import ImageDraw
def fit_font(draw, text, max_width, start=72, minimum=28):
for size in range(start, minimum - 1, -2):
font = load_font(size)
left, top, right, bottom = draw.textbbox(
(0, 0), text, font=font
)
if right - left <= max_width:
return font
raise ValueError("標題太長,請編輯文字或改用多行版型")自動縮小仍應設最低可讀字級。若再縮就看不清楚,應改寫標題或設計多行文字,而不是讓字擠成一條。
十二、加入半透明來源標示
from PIL import Image, ImageDraw
def add_credit(image, text, font):
base = image.convert("RGBA")
layer = Image.new("RGBA", base.size, (0, 0, 0, 0))
draw = ImageDraw.Draw(layer)
box = draw.textbbox((0, 0), text, font=font)
width = box[2] - box[0]
height = box[3] - box[1]
position = (
base.width - width - 30,
24,
)
draw.text(
position, text, font=font,
fill=(255, 255, 255, 190),
stroke_width=2,
stroke_fill=(0, 0, 0, 150),
)
return Image.alpha_composite(base, layer)
credited = add_credit(
card, "影像來源:經社區授權", load_font(28)
)浮水印不能取代授權。文字要依真實來源填寫;AI生成或經AI大幅修改的圖片,也應依使用情境清楚標示。
十三、輸出JPEG或PNG前先處理色彩模式
from pathlib import Path
def save_for_web(image, destination):
destination = Path(destination)
destination.parent.mkdir(parents=True, exist_ok=True)
suffix = destination.suffix.lower()
if suffix in {".jpg", ".jpeg"}:
image.convert("RGB").save(
destination,
format="JPEG",
quality=88,
optimize=True,
)
elif suffix == ".png":
image.convert("RGBA").save(
destination,
format="PNG",
optimize=True,
)
else:
raise ValueError("輸出格式只允許JPEG或PNG")
save_for_web(credited, "output/marriage_flower_card.jpg")JPEG不支援透明度,RGBA直接存JPEG會出錯;PNG適合透明圖示,但照片可能較大。重新建立輸出檔可避免原照片中的GPS等EXIF跟著公開,但若你主動傳入exif或其他metadata則另當別論。
十四、完成單張水井三寶圖卡函式
from PIL import ImageDraw
def create_local_card(
source, destination, title, credit,
size=(1200, 1200), crop_mode="crop"
):
image = open_oriented_rgb(source)
card = make_card_background(image, size, crop_mode)
draw = ImageDraw.Draw(card, "RGBA")
title_font = fit_font(
draw, title, max_width=size[0] - 120
)
draw_title(card, title, title_font, margin=48)
card = add_credit(card, credit, load_font(28))
save_for_web(card, destination)
return {
"source": str(source),
"destination": str(destination),
"title": title,
"size": size,
}
result = create_local_card(
"images/turtle.jpg",
"output/turtle_card.jpg",
"水井三寶|烏龜",
"影像來源:經社區授權",
)
print(result)十五、批次處理三張圖卡
JOBS = [
{
"source": "images/turtle.jpg",
"destination": "output/turtle_card.jpg",
"title": "水井三寶|烏龜",
},
{
"source": "images/white_horse.jpg",
"destination": "output/white_horse_card.jpg",
"title": "水井三寶|白馬",
},
{
"source": "images/marriage_flower.jpg",
"destination": "output/marriage_flower_card.jpg",
"title": "水井三寶|姻緣花",
},
]
records = []
for job in JOBS:
try:
record = create_local_card(
**job,
credit="影像來源:經社區授權",
)
record["status"] = "success"
except Exception as error:
record = {
"source": job["source"],
"status": "failed",
"error": type(error).__name__,
}
records.append(record)
for record in records:
print(record)批次工具不應因一張壞圖讓所有工作中止,但錯誤紀錄不要包含個資、秘密路徑或完整內部例外內容。
十六、用雜湊與尺寸驗證輸出
import hashlib
from pathlib import Path
from PIL import Image
def inspect_output(path, expected_size=(1200, 1200)):
path = Path(path)
digest = hashlib.sha256(path.read_bytes()).hexdigest()
with Image.open(path) as image:
image.load()
if image.size != expected_size:
raise ValueError("輸出尺寸不正確")
return {
"path": str(path),
"format": image.format,
"mode": image.mode,
"size": image.size,
"bytes": path.stat().st_size,
"sha256": digest,
}
print(inspect_output("output/turtle_card.jpg"))雜湊可協助確認檔案是否變更,不能證明圖片內容正確。仍要人工檢查裁切、文字、對比、地方名稱與來源標示。
十七、發布前的人文與倫理檢查
| 檢查 | 發布前要確認 |
|---|---|
| 著作權 | 攝影者、插畫者或權利人是否同意使用及改作? |
| 肖像與隱私 | 可辨識人物是否同意?是否含門牌、車牌或位置資訊? |
| 地方詮釋 | 「水井三寶」名稱與故事是否經社區夥伴確認? |
| AI透明 | AI生成、補圖或大幅修改是否清楚標示? |
| 可近用性 | 網站是否提供替代文字,而非只把文字畫進圖片? |
十八、課堂挑戰
十九、用AI協助檢查,但不要交出未授權照片
請擔任Python Pillow程式碼審查助教。
我要把社區授權照片製作成1200×1200地方圖卡。
請檢查:不可信圖片驗證、解壓縮炸彈、EXIF方向、
等比例縮放、裁切重心、中文字型授權、文字可讀性、
JPEG/PNG模式、metadata移除、錯誤處理與不覆蓋原圖。
不要要求我上傳未取得同意的人像或含GPS的原始照片。
請提供最小測試案例與人工審查清單。
沒有留言:
張貼留言