تحويل لوحة الإدارة الداخلية من secret header إلى session auth حقيقي مع صلاحيات admin. إضافة دعم إدارة الأدوار من داخل لوحة الإدارة نفسها، مع حماية الحسابات المعتمدة عبر INTERNAL_ADMIN_EMAILS. تحسين بيانات المستخدم في الواجهة والباكند لتشمل role وis_allowlisted_admin. إضافة اختبار frontend مخصص لصفحة /internal/admin بدل الاعتماد فقط على build واختبار routes. تحسين إضافي في الأداء عبر إزالة الاعتماد على pdfjs-dist/pdf.worker في عدّ صفحات PDF واستبداله بمسار أخف باستخدام pdf-lib. تحسين تقسيم الـ chunks في build لتقليل أثر الحزم الكبيرة وفصل أجزاء مثل network, icons, pdf-core, وeditor. التحقق الذي تم: نجاح build للواجهة. نجاح اختبار صفحة الإدارة الداخلية في frontend. نجاح اختبارات auth/admin في backend. نجاح full backend suite مسبقًا مع EXIT:0. ولو تريد نسخة أقصر جدًا، استخدم هذه: آخر التحديثات: تم تحسين نظام الإدارة الداخلية ليعتمد على صلاحيات وجلسات حقيقية بدل secret header، مع إضافة إدارة أدوار من لوحة admin نفسها، وإضافة اختبارات frontend مخصصة للوحة، وتحسين أداء الواجهة عبر إزالة pdf.worker وتحسين تقسيم الـ chunks في build. جميع الاختبارات والتحققات الأساسية المطلوبة نجح
177 lines
5.6 KiB
Python
177 lines
5.6 KiB
Python
"""Image extra tools — Crop, Rotate/Flip."""
|
|
import os
|
|
import logging
|
|
|
|
from PIL import Image
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class ImageExtraError(Exception):
|
|
"""Custom exception for image extra tool failures."""
|
|
pass
|
|
|
|
|
|
FORMAT_MAP = {
|
|
"jpg": "JPEG",
|
|
"jpeg": "JPEG",
|
|
"png": "PNG",
|
|
"webp": "WEBP",
|
|
}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Image Crop
|
|
# ---------------------------------------------------------------------------
|
|
def crop_image(
|
|
input_path: str,
|
|
output_path: str,
|
|
left: int,
|
|
top: int,
|
|
right: int,
|
|
bottom: int,
|
|
quality: int = 85,
|
|
) -> dict:
|
|
"""Crop an image to a specified rectangle.
|
|
|
|
Args:
|
|
input_path: Path to the input image
|
|
output_path: Path for the cropped output
|
|
left: Left edge in pixels
|
|
top: Top edge in pixels
|
|
right: Right edge in pixels
|
|
bottom: Bottom edge in pixels
|
|
quality: Output quality for lossy formats
|
|
|
|
Returns:
|
|
dict with original and cropped dimensions
|
|
|
|
Raises:
|
|
ImageExtraError: If crop fails
|
|
"""
|
|
try:
|
|
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
|
|
|
with Image.open(input_path) as img:
|
|
orig_w, orig_h = img.size
|
|
|
|
if left < 0 or top < 0 or right > orig_w or bottom > orig_h:
|
|
raise ImageExtraError(
|
|
f"Crop area ({left},{top},{right},{bottom}) outside image bounds ({orig_w}x{orig_h})."
|
|
)
|
|
if left >= right or top >= bottom:
|
|
raise ImageExtraError("Invalid crop area: left must be < right, top must be < bottom.")
|
|
|
|
cropped = img.crop((left, top, right, bottom))
|
|
|
|
ext = os.path.splitext(output_path)[1].lower().strip(".")
|
|
pil_format = FORMAT_MAP.get(ext, "PNG")
|
|
|
|
save_kwargs = {"optimize": True}
|
|
if pil_format in ("JPEG", "WEBP"):
|
|
save_kwargs["quality"] = quality
|
|
if cropped.mode in ("RGBA", "P", "LA"):
|
|
bg = Image.new("RGB", cropped.size, (255, 255, 255))
|
|
if cropped.mode == "P":
|
|
cropped = cropped.convert("RGBA")
|
|
bg.paste(cropped, mask=cropped.split()[-1] if "A" in cropped.mode else None)
|
|
cropped = bg
|
|
|
|
cropped.save(output_path, format=pil_format, **save_kwargs)
|
|
|
|
new_w = right - left
|
|
new_h = bottom - top
|
|
logger.info(f"Image crop: {orig_w}x{orig_h} → {new_w}x{new_h}")
|
|
return {
|
|
"original_width": orig_w,
|
|
"original_height": orig_h,
|
|
"cropped_width": new_w,
|
|
"cropped_height": new_h,
|
|
}
|
|
|
|
except ImageExtraError:
|
|
raise
|
|
except (IOError, OSError, Image.DecompressionBombError) as e:
|
|
raise ImageExtraError(f"Image crop failed: {str(e)}")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Image Rotate / Flip
|
|
# ---------------------------------------------------------------------------
|
|
def rotate_flip_image(
|
|
input_path: str,
|
|
output_path: str,
|
|
rotation: int = 0,
|
|
flip_horizontal: bool = False,
|
|
flip_vertical: bool = False,
|
|
quality: int = 85,
|
|
) -> dict:
|
|
"""Rotate and/or flip an image.
|
|
|
|
Args:
|
|
input_path: Path to the input image
|
|
output_path: Path for the output image
|
|
rotation: Rotation angle (0, 90, 180, 270)
|
|
flip_horizontal: Mirror horizontally
|
|
flip_vertical: Mirror vertically
|
|
quality: Output quality for lossy formats
|
|
|
|
Returns:
|
|
dict with original and new dimensions
|
|
|
|
Raises:
|
|
ImageExtraError: If operation fails
|
|
"""
|
|
if rotation not in (0, 90, 180, 270):
|
|
raise ImageExtraError("Rotation must be 0, 90, 180, or 270 degrees.")
|
|
|
|
try:
|
|
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
|
|
|
with Image.open(input_path) as img:
|
|
orig_w, orig_h = img.size
|
|
result = img
|
|
|
|
if rotation:
|
|
# PIL rotates counter-clockwise, so negate for clockwise
|
|
result = result.rotate(-rotation, expand=True)
|
|
|
|
if flip_horizontal:
|
|
result = result.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
|
|
|
|
if flip_vertical:
|
|
result = result.transpose(Image.Transpose.FLIP_TOP_BOTTOM)
|
|
|
|
new_w, new_h = result.size
|
|
|
|
ext = os.path.splitext(output_path)[1].lower().strip(".")
|
|
pil_format = FORMAT_MAP.get(ext, "PNG")
|
|
|
|
save_kwargs = {"optimize": True}
|
|
if pil_format in ("JPEG", "WEBP"):
|
|
save_kwargs["quality"] = quality
|
|
if result.mode in ("RGBA", "P", "LA"):
|
|
bg = Image.new("RGB", result.size, (255, 255, 255))
|
|
if result.mode == "P":
|
|
result = result.convert("RGBA")
|
|
bg.paste(result, mask=result.split()[-1] if "A" in result.mode else None)
|
|
result = bg
|
|
|
|
result.save(output_path, format=pil_format, **save_kwargs)
|
|
|
|
logger.info(f"Image rotate/flip: {orig_w}x{orig_h} → {new_w}x{new_h}, rot={rotation}")
|
|
return {
|
|
"original_width": orig_w,
|
|
"original_height": orig_h,
|
|
"new_width": new_w,
|
|
"new_height": new_h,
|
|
"rotation": rotation,
|
|
"flipped_horizontal": flip_horizontal,
|
|
"flipped_vertical": flip_vertical,
|
|
}
|
|
|
|
except ImageExtraError:
|
|
raise
|
|
except (IOError, OSError, Image.DecompressionBombError) as e:
|
|
raise ImageExtraError(f"Image rotate/flip failed: {str(e)}")
|