一. 前言:只是想寄幾張照片,不想先研究修圖軟體 #
要寄活動照片,郵件卻說附件太大。 改成一張一張傳,又開始跟進度條大眼瞪小眼。 你其實不需要把原始照片交出去,只要讓對方在螢幕上看清楚就好。
拍拍君今天做一個小工具:把 photos 裡的 JPEG、PNG,
縮成最長邊不超過 1200 像素的副本,放到另一個 share 資料夾。
原檔不動、不拉長照片,透明 PNG 也保留透明背景。
下載完整練習包,裡面有完整程式、 三張合成圖片與預期輸出;不必拿私人照片測試,也不必上傳任何網站。 想直接做出成果,依序完成安裝、產生練習檔,再執行批次程式即可。
這不是 AI 修圖,也不是把照片塞給模型。 先前的 本地圖片問答 曾簡短處理模型輸入尺寸; 這篇把焦點放回日常分享:尺寸、檔案大小、透明度,以及副本怎麼留。
二. 安裝:套件叫 Pillow,匯入時叫 PIL #
本文以 Python 3.12、Pillow 13.0.0 實際執行。 先建立新的練習資料夾,在裡面開終端機:
python -m venv .venv
macOS / Linux 啟用環境:
source .venv/bin/activate
Windows PowerShell 則用:
.\.venv\Scripts\Activate.ps1
接著安裝:
python -m pip install Pillow==13.0.0
如果電腦只有 python3,建立環境時把 python 改成 python3。
啟用環境後,下面都使用 python。
已經習慣 uv 的讀者可以沿用自己的環境流程,參考 uv 教學。
安裝名稱是 Pillow,程式卻寫 from PIL import Image,這不是打錯字。
不要另外去安裝名叫 PIL 的老套件。
三. 先做出第一張縮圖 #
練習包已附 photos;如果想自己產生,將以下存成 make_demo.py。
它建立一張有細節的橫圖、一張小圖,以及透明背景的圓形圖案。
這些是程式合成圖,不是真實人物或實拍照片。
import random
from pathlib import Path
from PIL import Image, ImageDraw
folder = Path("photos")
folder.mkdir(exist_ok=True)
rng = random.Random(42)
size = (2400, 1600)
noise = Image.frombytes("RGB", size, rng.randbytes(size[0] * size[1] * 3))
scene = Image.new("RGB", size, "#b8def0")
draw = ImageDraw.Draw(scene)
draw.ellipse((1550, 120, 2000, 570), fill="#fff1ab")
draw.polygon([(0, 1300), (800, 500), (1650, 1300)], fill="#678975")
draw.rectangle((0, 1300, 2400, 1600), fill="#d5bd90")
scene = Image.blend(scene, noise, 0.15)
scene.save(folder / "landscape.jpg", quality=98)
small = Image.new("RGB", (400, 300), "#f0c7b5")
ImageDraw.Draw(small).rectangle((80, 60, 320, 240), fill="#776b91")
small.save(folder / "small.jpg", quality=95)
sticker = Image.new("RGBA", (1800, 1200), (0, 0, 0, 0))
ImageDraw.Draw(sticker).ellipse((400, 100, 1400, 1100), fill="#65b99b")
sticker.save(folder / "sticker.png")
print("已建立三張練習圖片")
執行一次:
python make_demo.py
只在新的練習資料夾執行產生器。 重跑會覆寫那三個同名練習檔。 換成自己的照片時,不要再執行它。
先建立 one_photo.py,只縮一張試試看:
from PIL import Image, ImageOps
with Image.open("photos/landscape.jpg") as source:
image = ImageOps.exif_transpose(source)
image.thumbnail((1200, 1200), Image.Resampling.LANCZOS)
image.convert("RGB").save("preview.jpg", quality=85, optimize=True)
print(image.size)
python one_photo.py
你會拿到 preview.jpg,並看到 (1200, 800)。
它不是 1200×1200 的正方形,因為照片仍然保留原本的長寬比例。
打開原圖和預覽,先確認內容沒有被裁掉、人物沒有被拉寬。
這個單張練習重跑會覆寫 preview.jpg,但不修改 photos 裡的原圖。
四. 完整程式:一次處理整個資料夾 #
接著建立 resize_photos.py。這一支已經包含完整流程,
不用把上一段單張範例拼進來:
from pathlib import Path
from PIL import Image, ImageOps, UnidentifiedImageError
INPUT = Path("photos")
OUTPUT = Path("share")
MAX_EDGE = 1200
JPEG_QUALITY = 85
EXTENSIONS = {".jpg", ".jpeg", ".png"}
if INPUT.resolve() == OUTPUT.resolve():
raise SystemExit("來源和輸出必須是不同資料夾")
if not INPUT.is_dir():
raise SystemExit("找不到 photos 資料夾")
OUTPUT.mkdir(exist_ok=True)
files = sorted(
p for p in INPUT.iterdir()
if p.is_file() and p.suffix.lower() in EXTENSIONS
)
if not files:
raise SystemExit("沒有找到 JPEG 或 PNG 圖片")
completed = 0
for path in files:
target = OUTPUT / path.name
if target.exists():
print(f"略過已存在:{target.name}")
continue
try:
with Image.open(path) as source:
if getattr(source, "n_frames", 1) != 1:
print(f"略過多影格圖片:{path.name}")
continue
if source.format not in {"JPEG", "PNG"}:
print(f"略過格式不符:{path.name}")
continue
image = ImageOps.exif_transpose(source)
original_size = image.size
if path.suffix.lower() == ".png":
alpha = "A" in image.getbands() or "transparency" in image.info
image = image.convert("RGBA" if alpha else "RGB")
file_format = "PNG"
options = {"optimize": True}
else:
image = image.convert("RGB")
file_format = "JPEG"
options = {"quality": JPEG_QUALITY, "optimize": True}
image.thumbnail((MAX_EDGE, MAX_EDGE), Image.Resampling.LANCZOS)
with target.open("xb") as output:
image.save(output, format=file_format, **options)
before = path.stat().st_size / 1024
after = target.stat().st_size / 1024
print(f"{path.name}: {original_size} → {image.size}; "
f"{before:.1f} → {after:.1f} KiB")
completed += 1
except (UnidentifiedImageError, OSError, ValueError) as error:
print(f"未完成 {path.name}:{error}")
print(f"完成 {completed} 張;請到 {OUTPUT} 檢查結果")
執行:
python resize_photos.py
完成後的資料夾大致是:
photo-demo/
├── make_demo.py
├── resize_photos.py
├── photos/ # 原檔,保留不動
│ ├── landscape.jpg
│ ├── small.jpg
│ └── sticker.png
└── share/ # 要寄出去的副本
├── landscape.jpg
├── small.jpg
└── sticker.png
輸出保留原檔名,所以 photo.jpg 和 photo.png 也不會被硬改成同一個 JPEG。
程式只找第一層資料夾,不會往下掃描相簿,也不會處理 HEIC、GIF 或 RAW。
大小寫副檔名如 .JPG 可以辨識;動畫 PNG 則略過,不偷偷只留下第一格。
若想多了解找檔案的部分,可以接著看 pathlib 教學。
五. 縮尺寸和調品質,是兩個不同的旋鈕 #
MAX_EDGE = 1200 控制像素數;JPEG_QUALITY = 85 控制 JPEG 編碼品質。
它們都可能影響檔案大小,但不是同一件事。
| 調整項目 | 改變什麼 | 什麼時候先調 |
|---|---|---|
| 最長邊 2400 → 1200 | 寬和高各減半,像素總數剩四分之一 | 原圖很大,只需要螢幕預覽 |
| JPEG 品質 95 → 85 | 有損壓縮的取捨,像素尺寸不一定改變 | 尺寸夠用,但檔案還想再小一點 |
| PNG 的 optimize | 嘗試更合適的無損編碼方式 | 想保留透明或清楚的圖案邊緣 |
「像素剩四分之一」不等於「檔案一定剩四分之一」。
檔案還受到畫面內容、雜訊、編碼方式與原本壓縮程度影響。
Pillow 的 quality=85 也不是「保證保留 85% 畫質」。
thumbnail((1200, 1200)) 的意思是放進這個外框,保持比例,不放大原本的小圖。
2400×1600 變成 1200×800;直式照片則可能變成 800×1200。
400×300 的小圖仍是 400×300,但重新存成 JPEG 後,位元組和畫質仍可能改變。
因此我們永遠從 photos 的原圖產生新副本,
不要拿 share 的 JPEG 一輪一輪再壓縮,讓損失一直累積。
也不需要把 quality 設成 100:那不是無損模式,通常還會讓檔案大很多。
六. 手機照片橫躺、透明圖黑底,怎麼避開? #
1. 先修正顯示方向,再縮尺寸 #
有些手機照片把方向存在 EXIF 裡:像素本身是橫的,
照片檢視器讀了方向標記,才顯示成直的。
ImageOps.exif_transpose(source) 先依標記旋轉或翻轉,移除該方向資訊,
再做縮圖,就比較不會出現寄出去後照片突然橫躺的情況。
這不是辨識哪邊是天空的 AI;如果原圖根本沒有正確方向標記, 程式不會自動猜到你想轉哪一邊。遇到這種照片,先手動轉正再處理。
2. PNG 留在 PNG,不把透明硬丟掉 #
JPEG 不支援透明背景,PNG 可以。
程式對含 alpha 或透明色資訊的 PNG 使用 RGBA,再以 PNG 儲存,
所以貼圖周圍仍然透明,不會因為一律轉 JPEG 而多出一塊底色。
RGBA 可以理解成紅、綠、藍,外加一個透明度通道。
範例也處理以調色盤透明色表示的 PNG,不只辨認檔案當下是不是 RGBA。
PNG 沒有使用這裡的 JPEG quality 參數;縮掉像素仍會改變影像內容,
只是儲存階段不像 JPEG 再引入同樣的有損壓縮。
這個版本針對一般螢幕分享,不是印刷色彩管理或原始照片封存工具。 不承諾完整保留 EXIF、ICC 等中繼資料,也不把它當作完整的隱私清除器。 若需要原始拍攝資訊、精準色彩或後續修圖,請保留原檔。
七. 檢查結果,再決定要不要壓得更小 #
練習包在本文環境的結果如下;KiB 是 1024 位元組。 不同 Pillow 或 JPEG 編碼函式庫版本,數字可能略有差異。
| 練習圖片 | 原尺寸 → 新尺寸 | 原大小 → 新大小 |
|---|---|---|
landscape.jpg |
2400×1600 → 1200×800 | 2879.8 → 101.0 KiB |
small.jpg |
400×300 → 400×300 | 4.6 → 2.1 KiB |
sticker.png |
1800×1200 → 1200×800 | 14.0 → 33.2 KiB |
合成橫圖刻意帶有細節與雜訊,用來觀察縮圖效果; 它不代表每張真實照片都會得到相同的壓縮比例。 非常小的圖重新編碼後也可能變大,所以程式印的是前後數字,不是保證省幾成。
先從 share 打開幾張:橫式、直式、透明 PNG 各挑一張。
看看方向是否正確、邊緣是否完整,再放大看文字或細節是否還能接受。
不要只看到終端機寫「完成」就把整包寄出去。
如果總大小還超過附件限制,可以把 MAX_EDGE 改成 1000,
或把 JPEG_QUALITY 改成 80,並把 OUTPUT 改成新的資料夾,例如 share-small。
預設遇到同名輸出會略過,不會因為改了參數就自動重做舊副本。
保留兩個資料夾對照,比憑印象猜畫質方便。
要留更多細節,可以把最長邊設為 1600,代價通常是較大的檔案。 文件截圖、文字密集圖片則優先留 PNG,別為了檔案小把字邊緣壓得糊糊的。 這份小工具沒有「每張剛好小於 500 KB」的保證,也不會自動搜尋最佳品質。
八. 換成自己的照片,只要注意幾件小事 #
另開一個工作資料夾,放入 resize_photos.py,再建立 photos,
先放兩三張自己的照片副本試跑;不要複製並執行練習圖產生器。
真正要分享的是 share,不是原始的 photos。
「找不到 photos」通常是終端機位置不對;路徑是相對於目前工作資料夾,
不是相對於程式檔所在位置。先切換到包含 photos 的那一層再執行。
「沒有找到圖片」則先檢查副檔名;HEIC 不在本文範圍,先用照片工具匯出 JPEG。
如果某張讀取失敗,程式會印出「未完成」,並繼續處理其他圖片。 最後的完成張數只算本次成功的新副本,略過的舊檔不算。 磁碟寫入途中出錯時,輸出可能只留下不完整檔案; 先確認錯誤原因,再刪掉那份失敗副本重跑,不要刪原圖。
圖片解碼仍需要記憶體,逐張處理不等於可以無限大。 遇到 Pillow 的大圖警告或明顯不合理的尺寸,先確認檔案來源, 不要為了讓程式繼續跑就直接關掉保護設定。
結語:原圖留給自己,輕巧副本拿來分享 #
拍拍君喜歡這種做完就能用的小工具。 不需要做出一個完整修圖軟體,只要掌握兩個旋鈕:像素尺寸與 JPEG 品質, 再把比例、方向、透明度顧好,就能少做很多重複點選。
先跑練習包,再換幾張自己的照片。 下次附件太大,不必一張張打開另存,也不用急著把原圖刪掉啦。