"""阿里云 OSS 图床:图片压缩(3:4、≥1340×1785、<2MB)+ 上传。 - compress_for_oss(image_path, out_path): 中心裁剪到 3:4 → 缩放/放大到 1340×1785 → JPEG quality 迭代压到 <2MB。 - upload_to_oss(cfg, local_path, object_key): 用 oss2 上传到 config.oss 指定的 bucket,返回可访问 URL。 - build_oss_key(country, timestamp, code, rand4): key = {国家}/{时间戳}/{货号}_{4位随机}.jpg(货号=前缀+3位计数,000 起最多 999) - oss 配置(config.yaml oss 段): oss_bucket / oss_endpoint / oss_key_id / oss_key_secret(或环境变量 OSS_ACCESS_KEY_ID / OSS_ACCESS_KEY_SECRET) """ import io from pathlib import Path from typing import Optional from PIL import Image # 目标规格:3:4 宽高比,最小 1340×1785,文件 < 2MB TARGET_W, TARGET_H = 1340, 1785 MAX_BYTES = 2 * 1024 * 1024 def compress_for_oss(image_path: str, out_path: str, max_bytes: int = MAX_BYTES) -> str: """压缩图片到 3:4 / ≥1340×1785 / <2MB(JPEG)。返回输出路径。""" with Image.open(image_path) as im: im = im.convert("RGB") # 1) 中心裁剪到 3:4 w, h = im.size if w / h > 3 / 4: # 太宽 → 裁左右 new_w = int(h * 3 / 4) x0 = (w - new_w) // 2 im = im.crop((x0, 0, x0 + new_w, h)) elif w / h < 3 / 4: # 太高 → 裁上下 new_h = int(w * 4 / 3) y0 = (h - new_h) // 2 im = im.crop((0, y0, w, y0 + new_h)) # 2) 缩放到目标尺寸(≥1340×1785,正好 3:4) im = im.resize((TARGET_W, TARGET_H), Image.LANCZOS) # 3) JPEG quality 迭代,保证 <2MB out = Path(out_path) out.parent.mkdir(parents=True, exist_ok=True) for quality in (92, 85, 78, 70, 62, 55, 48, 40): buf = io.BytesIO() im.save(buf, "JPEG", quality=quality, optimize=True, progressive=True) if buf.tell() <= max_bytes: out.write_bytes(buf.getvalue()) return str(out) # 全部超限 → 用最低质量兜底(仍可能 >2MB,打印警告) out.write_bytes(buf.getvalue()) print(f"[oss] 警告: {Path(image_path).name} 压缩后仍 {buf.tell()/1024/1024:.1f}MB > 2MB(质量 {quality})") return str(out) def upload_to_oss(cfg: dict, local_path: str, object_key: str) -> Optional[str]: """上传本地文件到 OSS,返回 URL;配置缺失/失败返回 None(不中断)。""" bucket = (cfg or {}).get("oss_bucket") or "" endpoint = (cfg or {}).get("oss_endpoint") or "" key_id = (cfg or {}).get("oss_key_id") or "" key_secret = (cfg or {}).get("oss_key_secret") or "" if not (bucket and endpoint and key_id and key_secret): print("[oss] 配置缺失(config.oss),跳过上传") return None try: import oss2 # 直连 session:忽略环境代理(挂 VPN 时代理会拦截国内 OSS); # oss2.Bucket 的 session 必须是 oss2.Session(内部封装 requests),设其底层 trust_env=False _session = oss2.Session() try: _session.session.trust_env = False except AttributeError: pass auth = oss2.Auth(key_id, key_secret) bkt = oss2.Bucket(auth, endpoint, bucket, session=_session) with open(local_path, "rb") as f: bkt.put_object(object_key, f) url = f"https://{bucket}.{endpoint}/{object_key}" print(f"[oss] 已上传: {url}") return url except Exception as e: # noqa: BLE001 print(f"[oss] 上传失败 {local_path}: {e}") return None def build_oss_key(country: str, timestamp: str, code: str, rand4: str, ext: str = "", local: Optional[str] = None) -> str: """对象 key:{国家}/{时间戳}/{货号}_{4位随机}.jpg(如 GB/20260820150945/DG000_Ab3x.jpg)。 ext 未显式指定时,优先从 local(实际待上传文件)的真实后缀推导—— 文件可能是 png/jpg 而非固定 jpg;local 也未提供时回退 ".jpg"。 """ if not ext.strip() and local: ext = Path(local).suffix if Path(local).suffix else ".jpg" ext = ext.strip() or ".jpg" safe = lambda s: "".join(c for c in (s or "") if c.isalnum() or c in "-_").strip() return f"{safe(country)}/{safe(timestamp)}/{safe(code)}_{safe(rand4)}{ext}"