Parcourir la source

新增缩略图

袁威 il y a 6 jours
Parent
commit
bf9c41c5a6
2 fichiers modifiés avec 72 ajouts et 0 suppressions
  1. 71 0
      app/crud/crud_card.py
  2. 1 0
      app/utils/scheme.py

+ 71 - 0
app/crud/crud_card.py

@@ -4,14 +4,19 @@ from mysql.connector.pooling import PooledMySQLConnection
 import json
 from datetime import datetime
 import copy
+import io
+import os
 
 from app.core.config import settings
+from app.core.minio_client import minio_client
 from app.utils.scheme import CardImageResponse, CardType, SortBy, SortOrder, ImageType
 from app.utils.card_score_calculate import calculate_scores_from_images, SCORE_SOURCE_IMAGE_TYPES
 from app.core.logger import get_logger
 
 logger = get_logger(__name__)
 
+THUMBNAIL_MAX_SIZE = (320, 320)
+
 # 定义灰度图固定的 detection_json 结构
 EMPTY_DETECTION_JSON = {
     "result": {
@@ -23,6 +28,70 @@ EMPTY_DETECTION_JSON = {
 }
 
 
+def _normalize_storage_rel_path(path: Optional[str]) -> Optional[str]:
+    if not path:
+        return None
+    value = str(path)
+    if value.startswith(settings.DATA_HOST_URL):
+        value = value.replace(settings.DATA_HOST_URL, "", 1)
+    return "/" + value.lstrip("/\\")
+
+
+def _build_thumbnail_rel_path(image_path: Optional[str]) -> Optional[str]:
+    rel_path = _normalize_storage_rel_path(image_path)
+    if not rel_path:
+        return None
+    base_name = os.path.basename(rel_path)
+    stem, _ = os.path.splitext(base_name)
+    if not stem:
+        return None
+    return f"/Thumbnail/{stem}_320.jpg"
+
+
+def _get_or_create_thumbnail_url(image_path: Optional[str]) -> Optional[str]:
+    rel_path = _normalize_storage_rel_path(image_path)
+    thumbnail_rel_path = _build_thumbnail_rel_path(image_path)
+    if not rel_path or not thumbnail_rel_path:
+        return None
+
+    source_object_name = f"{settings.MINIO_BASE_PREFIX}{rel_path}"
+    thumbnail_object_name = f"{settings.MINIO_BASE_PREFIX}{thumbnail_rel_path}"
+
+    try:
+        minio_client.stat_object(settings.MINIO_BUCKET, thumbnail_object_name)
+        return settings.get_full_url(thumbnail_rel_path)
+    except Exception:
+        pass
+
+    try:
+        from PIL import Image
+
+        response = minio_client.get_object(settings.MINIO_BUCKET, source_object_name)
+        image_bytes = response.read()
+        response.close()
+        response.release_conn()
+
+        with Image.open(io.BytesIO(image_bytes)) as img:
+            if img.mode not in ("RGB", "L"):
+                img = img.convert("RGB")
+            img.thumbnail(THUMBNAIL_MAX_SIZE)
+            out_bytes = io.BytesIO()
+            img.save(out_bytes, format="JPEG", quality=85)
+            out_bytes.seek(0)
+
+        minio_client.put_object(
+            settings.MINIO_BUCKET,
+            thumbnail_object_name,
+            out_bytes,
+            len(out_bytes.getvalue()),
+            content_type="image/jpeg"
+        )
+        return settings.get_full_url(thumbnail_rel_path)
+    except Exception as e:
+        logger.warning("生成缩略图失败: source=%s error=%s", source_object_name, e)
+        return None
+
+
 def update_card_scores_and_status(db_conn: PooledMySQLConnection, card_id: int):
     """
     更新cards表中的分数和状态。注意:只基于 card_images 表(主4张图)计算。
@@ -229,6 +298,7 @@ def get_card_with_details(db_conn: PooledMySQLConnection, card_id: int) -> Optio
 
         # 处理主图片,补充全路径
         for row in main_image_records:
+            row['thumbnail_path'] = _get_or_create_thumbnail_url(row.get('image_path'))
             row['image_path'] = settings.get_full_url(row.get('image_path'))
             row['detection_image_path'] = settings.get_full_url(row.get('detection_image_path'))
             row['modified_image_path'] = settings.get_full_url(row.get('modified_image_path'))
@@ -277,6 +347,7 @@ def get_card_with_details(db_conn: PooledMySQLConnection, card_id: int) -> Optio
                 "card_id": row['card_id'],
                 "image_type": g_type,
                 "image_path": settings.get_full_url(row['image_path']),
+                "thumbnail_path": _get_or_create_thumbnail_url(row.get('image_path')),
                 "created_at": row['created_at'],
                 "updated_at": row['updated_at'],
                 "detection_json": virtual_detection_json,

+ 1 - 0
app/utils/scheme.py

@@ -103,6 +103,7 @@ class CardImageResponse(BaseModel):
     image_type: str
     image_name: Optional[str] = None
     image_path: str
+    thumbnail_path: Optional[str] = None
     # Gray images need these fields to be Optional/None
     detection_image_path: Optional[str] = None
     modified_image_path: Optional[str] = None