fix(photos): 入库压缩保留 EXIF 并补未来上传的压缩缺口
- 迁移器与新增 photos_ingest.py 压缩时显式 exif= 传参(PIL 重存默认 剥离——此前 21 张已全部丢 EXIF,图库参数条构建期数据源被抽空) - exif=None 会使 PIL JPEG 保存 len(None) 崩,改条件传参 - photos_ingest.py:幂等入库处理器(长边 2400/q86,>1.5M 才动), 堵住 gitea/本地上传原图直入仓的缺口(196M 事故成因) - 重跑迁移:19/21 带 EXIF(2 张原图本无),体积 9.7M 验证: pnpm astro build 11 页 exit 0;EXIF 保留计数 19/21 Spec: none (Phase 2 归档后的独立小修:EXIF 修复 + ingest 补缺) Co-authored-by: GLM-5.3
No files matched your search
|
Before Width: | Height: | Size: 371 KiB After Width: | Height: | Size: 414 KiB |
|
Before Width: | Height: | Size: 248 KiB After Width: | Height: | Size: 292 KiB |
|
Before Width: | Height: | Size: 262 KiB After Width: | Height: | Size: 303 KiB |
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 194 KiB |
|
Before Width: | Height: | Size: 622 KiB After Width: | Height: | Size: 623 KiB |
|
Before Width: | Height: | Size: 381 KiB After Width: | Height: | Size: 382 KiB |
|
Before Width: | Height: | Size: 498 KiB After Width: | Height: | Size: 499 KiB |
|
Before Width: | Height: | Size: 396 KiB After Width: | Height: | Size: 397 KiB |
|
Before Width: | Height: | Size: 235 KiB After Width: | Height: | Size: 235 KiB |
|
Before Width: | Height: | Size: 823 KiB After Width: | Height: | Size: 823 KiB |
|
Before Width: | Height: | Size: 719 KiB After Width: | Height: | Size: 719 KiB |
|
Before Width: | Height: | Size: 530 KiB After Width: | Height: | Size: 531 KiB |
|
Before Width: | Height: | Size: 846 KiB After Width: | Height: | Size: 847 KiB |
|
Before Width: | Height: | Size: 436 KiB After Width: | Height: | Size: 437 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 470 KiB After Width: | Height: | Size: 482 KiB |
|
Before Width: | Height: | Size: 654 KiB After Width: | Height: | Size: 666 KiB |
|
Before Width: | Height: | Size: 619 KiB After Width: | Height: | Size: 630 KiB |
|
Before Width: | Height: | Size: 456 KiB After Width: | Height: | Size: 467 KiB |
@@ -32,11 +32,15 @@ def compress(data: bytes) -> bytes:
|
|||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
img = Image.open(BytesIO(data))
|
img = Image.open(BytesIO(data))
|
||||||
|
# EXIF 必须显式带上:PIL 重存默认剥离,而构建期 exifr 正是从文件提 EXIF
|
||||||
|
exif = img.info.get("exif")
|
||||||
if img.mode not in ("RGB", "L"):
|
if img.mode not in ("RGB", "L"):
|
||||||
img = img.convert("RGB")
|
img = img.convert("RGB")
|
||||||
img.thumbnail((2400, 2400))
|
img.thumbnail((2400, 2400))
|
||||||
buf = BytesIO()
|
buf = BytesIO()
|
||||||
img.save(buf, "JPEG", quality=86, optimize=True)
|
# PIL 的 JPEG 保存不接受 exif=None(len(None) 崩),仅在确有 EXIF 时传参
|
||||||
|
kwargs = {"exif": exif} if exif else {}
|
||||||
|
img.save(buf, "JPEG", quality=86, optimize=True, **kwargs)
|
||||||
return buf.getvalue()
|
return buf.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""照片入库处理器:把 content/photos/ 下超限图片压缩到站内规范。
|
||||||
|
|
||||||
|
为什么存在:gitea web/本地上传的是原图(曾发生 196M/21 张入仓事故,
|
||||||
|
见 git log aea21c8 的重写说明);sharp 只派生 dist 缩略图不管仓内体积。
|
||||||
|
工作流:上传照片到 content/photos/(顺带跑一次 sidecar 生成或手写)后、
|
||||||
|
git commit 前执行本脚本;幂等,已在规范内的文件原样跳过(零 diff)。
|
||||||
|
|
||||||
|
规范与迁移器一致:长边 ≤2400px、JPEG q86(scripts/migrate/photos_migrate.py)。
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
PHOTOS = Path("content/photos")
|
||||||
|
MAX_EDGE = 2400
|
||||||
|
QUALITY = 86
|
||||||
|
# 跳过阈值:小于此字节数的 jpg 视为已处理过(2400px q86 成片通常 200KB-1MB)
|
||||||
|
SKIP_BELOW_BYTES = 1_500_000
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
changed, skipped = [], 0
|
||||||
|
for jpg in sorted(PHOTOS.glob("*.jpg")):
|
||||||
|
size = jpg.stat().st_size
|
||||||
|
if size < SKIP_BELOW_BYTES:
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
img = Image.open(jpg)
|
||||||
|
# EXIF 显式保留:重存默认剥离会抽走图库参数条的构建期数据源
|
||||||
|
exif = img.info.get("exif")
|
||||||
|
if img.mode not in ("RGB", "L"):
|
||||||
|
img = img.convert("RGB")
|
||||||
|
img.thumbnail((MAX_EDGE, MAX_EDGE))
|
||||||
|
kwargs = {"exif": exif} if exif else {}
|
||||||
|
img.save(jpg, "JPEG", quality=QUALITY, optimize=True, **kwargs)
|
||||||
|
changed.append((jpg.name, size, jpg.stat().st_size))
|
||||||
|
|
||||||
|
for name, before, after in changed:
|
||||||
|
print(f" compressed: {name} {before/1e6:.1f}M -> {after/1e6:.2f}M")
|
||||||
|
print(f"photos ingest: {len(changed)} compressed, {skipped} skipped (within spec)")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||