|
|
|
|
| import os
|
| from pathlib import Path
|
| from typing import List, Tuple, Optional
|
| from PIL import Image, ImageOps
|
|
|
|
|
| try:
|
| from Config.settings import GALLERY_DIR, THUMB_DIR, GALLERY_ALLOWED_EXT, THUMB_MAX
|
| except Exception:
|
| try:
|
| from config.settings import GALLERY_DIR, THUMB_DIR, GALLERY_ALLOWED_EXT, THUMB_MAX
|
| except Exception:
|
|
|
| BASE_DIR = Path(__file__).resolve().parent
|
| DATA_DIR = BASE_DIR / "data"
|
| GALLERY_DIR = DATA_DIR / "reports_photos"
|
| THUMB_DIR = GALLERY_DIR / "_thumbs"
|
| GALLERY_ALLOWED_EXT = {".png", ".jpg", ".jpeg", ".webp"}
|
| THUMB_MAX = (512, 512)
|
|
|
| GALLERY_DIR.mkdir(parents=True, exist_ok=True)
|
| THUMB_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
| def _gallery_is_image(filename: str) -> bool:
|
| ext = os.path.splitext(filename)[1].lower()
|
| return ext in GALLERY_ALLOWED_EXT
|
|
|
| def _gallery_safe_name(base: str) -> str:
|
| return "".join(c for c in base if c.isalnum() or c in ("-", "_")).strip("_")
|
|
|
| def _thumb_path_for(orig_path: Path) -> Path:
|
| base = orig_path.stem
|
| return THUMB_DIR / f"{base}-thumb-{THUMB_MAX[0]}x{THUMB_MAX[1]}.jpg"
|
|
|
| def _generate_thumbnail(orig_path: Path, thumb_path: Path) -> None:
|
| try:
|
| with Image.open(orig_path) as im:
|
| im = ImageOps.exif_transpose(im)
|
| if im.mode in ("RGBA", "LA"):
|
| bg = Image.new("RGB", im.size, (255, 255, 255))
|
| bg.paste(im, mask=im.split()[-1])
|
| im = bg
|
| elif im.mode != "RGB":
|
| im = im.convert("RGB")
|
| im.thumbnail(THUMB_MAX)
|
| thumb_path.parent.mkdir(parents=True, exist_ok=True)
|
| im.save(str(thumb_path), "JPEG", quality=85, optimize=True, progressive=True)
|
| except Exception as e:
|
| print(f"[thumb] Falha ao gerar miniatura para {orig_path}: {e}")
|
|
|
| def _ensure_thumbnail(orig_path: Path) -> Path:
|
| tpath = _thumb_path_for(orig_path)
|
| try:
|
| orig_mtime = orig_path.stat().st_mtime
|
| thumb_mtime = tpath.stat().st_mtime if tpath.exists() else -1
|
| if thumb_mtime < orig_mtime:
|
| _generate_thumbnail(orig_path, tpath)
|
| except Exception as e:
|
| print(f"[thumb] Erro: {e}")
|
| return tpath
|
|
|
| def gallery_list_images_sorted() -> List[Tuple[str, float, Path, Path]]:
|
| """
|
| [(nome, mtime, caminho_original, caminho_thumb)] do mais novo ao mais antigo.
|
| """
|
| if not GALLERY_DIR.is_dir():
|
| return []
|
| items = []
|
| for name in os.listdir(GALLERY_DIR):
|
| full = GALLERY_DIR / name
|
| if full.is_file() and _gallery_is_image(name):
|
| try:
|
| mtime = full.stat().st_mtime
|
| tpath = _ensure_thumbnail(full)
|
| items.append((name, mtime, full, tpath))
|
| except Exception:
|
| continue
|
| items.sort(key=lambda t: t[1], reverse=True)
|
| return items
|
|
|
| def save_report_image(uploaded_file, uploader_name: Optional[str] = None, uploader_email: Optional[str] = None) -> Optional[Path]:
|
| if uploaded_file is None:
|
| return None
|
|
|
| base, ext = os.path.splitext(uploaded_file.name or "")
|
| ext = ext.lower()
|
| if ext not in GALLERY_ALLOWED_EXT:
|
| raise ValueError("Formato não suportado. Use PNG/JPG/JPEG/WEBP.")
|
|
|
| who_bits = []
|
| if uploader_name: who_bits.append(_gallery_safe_name(uploader_name))
|
| if uploader_email: who_bits.append(_gallery_safe_name((uploader_email or "").split("@")[0]))
|
| who_str = ("-" + "-".join(filter(None, who_bits))) if who_bits else ""
|
|
|
| from datetime import datetime
|
| ts = datetime.now().strftime("%Y%m%d-%H%M%S")
|
| safe_base = _gallery_safe_name(base) or "relatorio"
|
| fname = f"{ts}-{safe_base}{who_str}{ext}"
|
| dest = GALLERY_DIR / fname
|
|
|
| img = Image.open(uploaded_file)
|
| img = ImageOps.exif_transpose(img)
|
| if ext in (".jpg", ".jpeg") and img.mode not in ("RGB",):
|
| img = img.convert("RGB")
|
| img.save(dest)
|
| _ensure_thumbnail(dest)
|
| return dest
|
|
|
| def delete_report_image(filename: str, is_admin: bool) -> None:
|
| """
|
| Exclui (original + thumb) de forma segura e restrita a Admin.
|
| - Protege contra path traversal (só apaga dentro de GALLERY_DIR)
|
| """
|
| if not is_admin:
|
| raise PermissionError("Somente Admin pode excluir imagens.")
|
| if "/" in filename or "\\" in filename:
|
| raise ValueError("Nome de arquivo inválido.")
|
| target = (GALLERY_DIR / filename).resolve()
|
| if GALLERY_DIR.resolve() not in target.parents and target != GALLERY_DIR.resolve():
|
| raise ValueError("Caminho inválido.")
|
| if target.exists():
|
| target.unlink()
|
| tpath = _thumb_path_for(target)
|
| if tpath.exists():
|
| tpath.unlink() |