تم الانتهاء من آخر دفعة تحسينات على المشروع، وتشمل:

تحويل لوحة الإدارة الداخلية من 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. جميع الاختبارات والتحققات الأساسية المطلوبة نجح
This commit is contained in:
Your Name
2026-03-16 13:50:45 +02:00
parent b5d97324a9
commit 957d37838c
85 changed files with 9915 additions and 119 deletions

View File

@@ -34,6 +34,23 @@ from app.tasks.pdf_tools_tasks import (
unlock_pdf_task,
)
from app.tasks.flowchart_tasks import extract_flowchart_task
from app.tasks.ocr_tasks import ocr_image_task, ocr_pdf_task
from app.tasks.removebg_tasks import remove_bg_task
from app.tasks.pdf_ai_tasks import (
chat_with_pdf_task, summarize_pdf_task, translate_pdf_task, extract_tables_task,
)
from app.tasks.pdf_to_excel_tasks import pdf_to_excel_task
from app.tasks.html_to_pdf_tasks import html_to_pdf_task
from app.tasks.qrcode_tasks import generate_qr_task
from app.tasks.pdf_convert_tasks import (
pdf_to_pptx_task, excel_to_pdf_task, pptx_to_pdf_task, sign_pdf_task,
)
from app.tasks.pdf_extra_tasks import (
crop_pdf_task, flatten_pdf_task, repair_pdf_task, edit_metadata_task,
)
from app.tasks.image_extra_tasks import crop_image_task, rotate_flip_image_task
from app.tasks.barcode_tasks import generate_barcode_task
from app.services.barcode_service import SUPPORTED_BARCODE_TYPES
logger = logging.getLogger(__name__)
@@ -680,3 +697,760 @@ def extract_flowchart_route():
)
record_accepted_usage(actor, "pdf-flowchart", task.id)
return jsonify({"task_id": task.id, "message": "Flowchart extraction started."}), 202
# ===========================================================================
# Phase 2: Previously uncovered existing tools
# ===========================================================================
# ---------------------------------------------------------------------------
# OCR — POST /api/v1/ocr/image & /api/v1/ocr/pdf
# ---------------------------------------------------------------------------
@v1_bp.route("/ocr/image", methods=["POST"])
@limiter.limit("10/minute")
def ocr_image_route():
"""Extract text from an image using OCR."""
actor, err = _resolve_and_check()
if err:
return err
if "file" not in request.files:
return jsonify({"error": "No file provided."}), 400
file = request.files["file"]
lang = request.form.get("lang", "eng")
try:
original_filename, ext = validate_actor_file(
file, allowed_types=ALLOWED_IMAGE_TYPES, actor=actor
)
except FileValidationError as e:
return jsonify({"error": e.message}), e.code
task_id, input_path = generate_safe_path(ext, folder_type="upload")
file.save(input_path)
task = ocr_image_task.delay(
input_path, task_id, original_filename, lang,
**build_task_tracking_kwargs(actor),
)
record_accepted_usage(actor, "ocr-image", task.id)
return jsonify({"task_id": task.id, "message": "OCR started."}), 202
@v1_bp.route("/ocr/pdf", methods=["POST"])
@limiter.limit("10/minute")
def ocr_pdf_route():
"""Extract text from a PDF using OCR."""
actor, err = _resolve_and_check()
if err:
return err
if "file" not in request.files:
return jsonify({"error": "No file provided."}), 400
file = request.files["file"]
lang = request.form.get("lang", "eng")
try:
original_filename, ext = validate_actor_file(file, allowed_types=["pdf"], actor=actor)
except FileValidationError as e:
return jsonify({"error": e.message}), e.code
task_id, input_path = generate_safe_path(ext, folder_type="upload")
file.save(input_path)
task = ocr_pdf_task.delay(
input_path, task_id, original_filename, lang,
**build_task_tracking_kwargs(actor),
)
record_accepted_usage(actor, "ocr-pdf", task.id)
return jsonify({"task_id": task.id, "message": "OCR started."}), 202
# ---------------------------------------------------------------------------
# Remove Background — POST /api/v1/image/remove-bg
# ---------------------------------------------------------------------------
@v1_bp.route("/image/remove-bg", methods=["POST"])
@limiter.limit("5/minute")
def remove_bg_route():
"""Remove background from an image."""
actor, err = _resolve_and_check()
if err:
return err
if "file" not in request.files:
return jsonify({"error": "No file provided."}), 400
file = request.files["file"]
try:
original_filename, ext = validate_actor_file(
file, allowed_types=ALLOWED_IMAGE_TYPES, actor=actor
)
except FileValidationError as e:
return jsonify({"error": e.message}), e.code
task_id, input_path = generate_safe_path(ext, folder_type="upload")
file.save(input_path)
task = remove_bg_task.delay(
input_path, task_id, original_filename,
**build_task_tracking_kwargs(actor),
)
record_accepted_usage(actor, "remove-bg", task.id)
return jsonify({"task_id": task.id, "message": "Background removal started."}), 202
# ---------------------------------------------------------------------------
# PDF AI — POST /api/v1/pdf-ai/chat, summarize, translate, extract-tables
# ---------------------------------------------------------------------------
@v1_bp.route("/pdf-ai/chat", methods=["POST"])
@limiter.limit("5/minute")
def chat_pdf_route():
"""Chat with a PDF using AI."""
actor, err = _resolve_and_check()
if err:
return err
if "file" not in request.files:
return jsonify({"error": "No file provided."}), 400
file = request.files["file"]
question = request.form.get("question", "").strip()
if not question:
return jsonify({"error": "Question is required."}), 400
try:
original_filename, ext = validate_actor_file(file, allowed_types=["pdf"], actor=actor)
except FileValidationError as e:
return jsonify({"error": e.message}), e.code
task_id, input_path = generate_safe_path(ext, folder_type="upload")
file.save(input_path)
task = chat_with_pdf_task.delay(
input_path, task_id, original_filename, question,
**build_task_tracking_kwargs(actor),
)
record_accepted_usage(actor, "chat-pdf", task.id)
return jsonify({"task_id": task.id, "message": "Chat started."}), 202
@v1_bp.route("/pdf-ai/summarize", methods=["POST"])
@limiter.limit("5/minute")
def summarize_pdf_route():
"""Summarize a PDF using AI."""
actor, err = _resolve_and_check()
if err:
return err
if "file" not in request.files:
return jsonify({"error": "No file provided."}), 400
file = request.files["file"]
length = request.form.get("length", "medium")
if length not in ("short", "medium", "long"):
length = "medium"
try:
original_filename, ext = validate_actor_file(file, allowed_types=["pdf"], actor=actor)
except FileValidationError as e:
return jsonify({"error": e.message}), e.code
task_id, input_path = generate_safe_path(ext, folder_type="upload")
file.save(input_path)
task = summarize_pdf_task.delay(
input_path, task_id, original_filename, length,
**build_task_tracking_kwargs(actor),
)
record_accepted_usage(actor, "summarize-pdf", task.id)
return jsonify({"task_id": task.id, "message": "Summarization started."}), 202
@v1_bp.route("/pdf-ai/translate", methods=["POST"])
@limiter.limit("5/minute")
def translate_pdf_route():
"""Translate a PDF using AI."""
actor, err = _resolve_and_check()
if err:
return err
if "file" not in request.files:
return jsonify({"error": "No file provided."}), 400
file = request.files["file"]
target_language = request.form.get("target_language", "").strip()
if not target_language:
return jsonify({"error": "Target language is required."}), 400
try:
original_filename, ext = validate_actor_file(file, allowed_types=["pdf"], actor=actor)
except FileValidationError as e:
return jsonify({"error": e.message}), e.code
task_id, input_path = generate_safe_path(ext, folder_type="upload")
file.save(input_path)
task = translate_pdf_task.delay(
input_path, task_id, original_filename, target_language,
**build_task_tracking_kwargs(actor),
)
record_accepted_usage(actor, "translate-pdf", task.id)
return jsonify({"task_id": task.id, "message": "Translation started."}), 202
@v1_bp.route("/pdf-ai/extract-tables", methods=["POST"])
@limiter.limit("10/minute")
def extract_tables_route():
"""Extract tables from a PDF using AI."""
actor, err = _resolve_and_check()
if err:
return err
if "file" not in request.files:
return jsonify({"error": "No file provided."}), 400
file = request.files["file"]
try:
original_filename, ext = validate_actor_file(file, allowed_types=["pdf"], actor=actor)
except FileValidationError as e:
return jsonify({"error": e.message}), e.code
task_id, input_path = generate_safe_path(ext, folder_type="upload")
file.save(input_path)
task = extract_tables_task.delay(
input_path, task_id, original_filename,
**build_task_tracking_kwargs(actor),
)
record_accepted_usage(actor, "extract-tables", task.id)
return jsonify({"task_id": task.id, "message": "Table extraction started."}), 202
# ---------------------------------------------------------------------------
# PDF to Excel — POST /api/v1/convert/pdf-to-excel
# ---------------------------------------------------------------------------
@v1_bp.route("/convert/pdf-to-excel", methods=["POST"])
@limiter.limit("10/minute")
def pdf_to_excel_route():
"""Convert a PDF to Excel."""
actor, err = _resolve_and_check()
if err:
return err
if "file" not in request.files:
return jsonify({"error": "No file provided."}), 400
file = request.files["file"]
try:
original_filename, ext = validate_actor_file(file, allowed_types=["pdf"], actor=actor)
except FileValidationError as e:
return jsonify({"error": e.message}), e.code
task_id, input_path = generate_safe_path(ext, folder_type="upload")
file.save(input_path)
task = pdf_to_excel_task.delay(
input_path, task_id, original_filename,
**build_task_tracking_kwargs(actor),
)
record_accepted_usage(actor, "pdf-to-excel", task.id)
return jsonify({"task_id": task.id, "message": "Conversion started."}), 202
# ---------------------------------------------------------------------------
# HTML to PDF — POST /api/v1/convert/html-to-pdf
# ---------------------------------------------------------------------------
@v1_bp.route("/convert/html-to-pdf", methods=["POST"])
@limiter.limit("10/minute")
def html_to_pdf_route():
"""Convert HTML to PDF."""
actor, err = _resolve_and_check()
if err:
return err
if "file" not in request.files:
return jsonify({"error": "No file provided."}), 400
file = request.files["file"]
try:
original_filename, ext = validate_actor_file(
file, allowed_types=["html", "htm"], actor=actor
)
except FileValidationError as e:
return jsonify({"error": e.message}), e.code
task_id, input_path = generate_safe_path(ext, folder_type="upload")
file.save(input_path)
task = html_to_pdf_task.delay(
input_path, task_id, original_filename,
**build_task_tracking_kwargs(actor),
)
record_accepted_usage(actor, "html-to-pdf", task.id)
return jsonify({"task_id": task.id, "message": "Conversion started."}), 202
# ---------------------------------------------------------------------------
# QR Code — POST /api/v1/qrcode/generate
# ---------------------------------------------------------------------------
@v1_bp.route("/qrcode/generate", methods=["POST"])
@limiter.limit("20/minute")
def generate_qr_route():
"""Generate a QR code."""
actor, err = _resolve_and_check()
if err:
return err
if request.is_json:
body = request.get_json()
data = body.get("data", "")
size = body.get("size", 300)
else:
data = request.form.get("data", "")
size = request.form.get("size", 300)
if not str(data).strip():
return jsonify({"error": "QR code data is required."}), 400
try:
size = max(100, min(2000, int(size)))
except (ValueError, TypeError):
size = 300
task_id = str(uuid.uuid4())
task = generate_qr_task.delay(
task_id, str(data).strip(), size, "png",
**build_task_tracking_kwargs(actor),
)
record_accepted_usage(actor, "qr-code", task.id)
return jsonify({"task_id": task.id, "message": "QR code generation started."}), 202
# ===========================================================================
# Phase 2: New tools
# ===========================================================================
# ---------------------------------------------------------------------------
# PDF to PowerPoint — POST /api/v1/convert/pdf-to-pptx
# ---------------------------------------------------------------------------
@v1_bp.route("/convert/pdf-to-pptx", methods=["POST"])
@limiter.limit("10/minute")
def v1_pdf_to_pptx_route():
"""Convert a PDF to PowerPoint."""
actor, err = _resolve_and_check()
if err:
return err
if "file" not in request.files:
return jsonify({"error": "No file provided."}), 400
file = request.files["file"]
try:
original_filename, ext = validate_actor_file(file, allowed_types=["pdf"], actor=actor)
except FileValidationError as e:
return jsonify({"error": e.message}), e.code
task_id, input_path = generate_safe_path(ext, folder_type="upload")
file.save(input_path)
task = pdf_to_pptx_task.delay(
input_path, task_id, original_filename,
**build_task_tracking_kwargs(actor),
)
record_accepted_usage(actor, "pdf-to-pptx", task.id)
return jsonify({"task_id": task.id, "message": "Conversion started."}), 202
# ---------------------------------------------------------------------------
# Excel to PDF — POST /api/v1/convert/excel-to-pdf
# ---------------------------------------------------------------------------
@v1_bp.route("/convert/excel-to-pdf", methods=["POST"])
@limiter.limit("10/minute")
def v1_excel_to_pdf_route():
"""Convert an Excel file to PDF."""
actor, err = _resolve_and_check()
if err:
return err
if "file" not in request.files:
return jsonify({"error": "No file provided."}), 400
file = request.files["file"]
try:
original_filename, ext = validate_actor_file(
file, allowed_types=["xlsx", "xls"], actor=actor
)
except FileValidationError as e:
return jsonify({"error": e.message}), e.code
task_id, input_path = generate_safe_path(ext, folder_type="upload")
file.save(input_path)
task = excel_to_pdf_task.delay(
input_path, task_id, original_filename,
**build_task_tracking_kwargs(actor),
)
record_accepted_usage(actor, "excel-to-pdf", task.id)
return jsonify({"task_id": task.id, "message": "Conversion started."}), 202
# ---------------------------------------------------------------------------
# PowerPoint to PDF — POST /api/v1/convert/pptx-to-pdf
# ---------------------------------------------------------------------------
@v1_bp.route("/convert/pptx-to-pdf", methods=["POST"])
@limiter.limit("10/minute")
def v1_pptx_to_pdf_route():
"""Convert a PowerPoint file to PDF."""
actor, err = _resolve_and_check()
if err:
return err
if "file" not in request.files:
return jsonify({"error": "No file provided."}), 400
file = request.files["file"]
try:
original_filename, ext = validate_actor_file(
file, allowed_types=["pptx", "ppt"], actor=actor
)
except FileValidationError as e:
return jsonify({"error": e.message}), e.code
task_id, input_path = generate_safe_path(ext, folder_type="upload")
file.save(input_path)
task = pptx_to_pdf_task.delay(
input_path, task_id, original_filename,
**build_task_tracking_kwargs(actor),
)
record_accepted_usage(actor, "pptx-to-pdf", task.id)
return jsonify({"task_id": task.id, "message": "Conversion started."}), 202
# ---------------------------------------------------------------------------
# Sign PDF — POST /api/v1/pdf-tools/sign
# ---------------------------------------------------------------------------
@v1_bp.route("/pdf-tools/sign", methods=["POST"])
@limiter.limit("10/minute")
def v1_sign_pdf_route():
"""Sign a PDF with a signature image."""
actor, err = _resolve_and_check()
if err:
return err
if "file" not in request.files:
return jsonify({"error": "No PDF file provided."}), 400
if "signature" not in request.files:
return jsonify({"error": "No signature image provided."}), 400
pdf_file = request.files["file"]
sig_file = request.files["signature"]
try:
original_filename, ext = validate_actor_file(pdf_file, allowed_types=["pdf"], actor=actor)
except FileValidationError as e:
return jsonify({"error": e.message}), e.code
try:
_, sig_ext = validate_actor_file(sig_file, allowed_types=ALLOWED_IMAGE_TYPES, actor=actor)
except FileValidationError as e:
return jsonify({"error": f"Signature: {e.message}"}), e.code
try:
page = max(1, int(request.form.get("page", 1))) - 1
x = float(request.form.get("x", 100))
y = float(request.form.get("y", 100))
width = float(request.form.get("width", 200))
height = float(request.form.get("height", 80))
except (ValueError, TypeError):
return jsonify({"error": "Invalid position parameters."}), 400
task_id = str(uuid.uuid4())
upload_dir = os.path.join(current_app.config["UPLOAD_FOLDER"], task_id)
os.makedirs(upload_dir, exist_ok=True)
input_path = os.path.join(upload_dir, f"{uuid.uuid4()}.pdf")
pdf_file.save(input_path)
signature_path = os.path.join(upload_dir, f"{uuid.uuid4()}.{sig_ext}")
sig_file.save(signature_path)
task = sign_pdf_task.delay(
input_path, signature_path, task_id, original_filename,
page, x, y, width, height,
**build_task_tracking_kwargs(actor),
)
record_accepted_usage(actor, "sign-pdf", task.id)
return jsonify({"task_id": task.id, "message": "Signing started."}), 202
# ---------------------------------------------------------------------------
# Crop PDF — POST /api/v1/pdf-tools/crop
# ---------------------------------------------------------------------------
@v1_bp.route("/pdf-tools/crop", methods=["POST"])
@limiter.limit("10/minute")
def v1_crop_pdf_route():
"""Crop margins from a PDF."""
actor, err = _resolve_and_check()
if err:
return err
if "file" not in request.files:
return jsonify({"error": "No file provided."}), 400
file = request.files["file"]
try:
margin_left = float(request.form.get("margin_left", 0))
margin_right = float(request.form.get("margin_right", 0))
margin_top = float(request.form.get("margin_top", 0))
margin_bottom = float(request.form.get("margin_bottom", 0))
except (ValueError, TypeError):
return jsonify({"error": "Margin values must be numbers."}), 400
pages = request.form.get("pages", "all")
try:
original_filename, ext = validate_actor_file(file, allowed_types=["pdf"], actor=actor)
except FileValidationError as e:
return jsonify({"error": e.message}), e.code
task_id, input_path = generate_safe_path(ext, folder_type="upload")
file.save(input_path)
task = crop_pdf_task.delay(
input_path, task_id, original_filename,
margin_left, margin_right, margin_top, margin_bottom, pages,
**build_task_tracking_kwargs(actor),
)
record_accepted_usage(actor, "crop-pdf", task.id)
return jsonify({"task_id": task.id, "message": "Cropping started."}), 202
# ---------------------------------------------------------------------------
# Flatten PDF — POST /api/v1/pdf-tools/flatten
# ---------------------------------------------------------------------------
@v1_bp.route("/pdf-tools/flatten", methods=["POST"])
@limiter.limit("10/minute")
def v1_flatten_pdf_route():
"""Flatten a PDF."""
actor, err = _resolve_and_check()
if err:
return err
if "file" not in request.files:
return jsonify({"error": "No file provided."}), 400
file = request.files["file"]
try:
original_filename, ext = validate_actor_file(file, allowed_types=["pdf"], actor=actor)
except FileValidationError as e:
return jsonify({"error": e.message}), e.code
task_id, input_path = generate_safe_path(ext, folder_type="upload")
file.save(input_path)
task = flatten_pdf_task.delay(
input_path, task_id, original_filename,
**build_task_tracking_kwargs(actor),
)
record_accepted_usage(actor, "flatten-pdf", task.id)
return jsonify({"task_id": task.id, "message": "Flattening started."}), 202
# ---------------------------------------------------------------------------
# Repair PDF — POST /api/v1/pdf-tools/repair
# ---------------------------------------------------------------------------
@v1_bp.route("/pdf-tools/repair", methods=["POST"])
@limiter.limit("10/minute")
def v1_repair_pdf_route():
"""Repair a damaged PDF."""
actor, err = _resolve_and_check()
if err:
return err
if "file" not in request.files:
return jsonify({"error": "No file provided."}), 400
file = request.files["file"]
try:
original_filename, ext = validate_actor_file(file, allowed_types=["pdf"], actor=actor)
except FileValidationError as e:
return jsonify({"error": e.message}), e.code
task_id, input_path = generate_safe_path(ext, folder_type="upload")
file.save(input_path)
task = repair_pdf_task.delay(
input_path, task_id, original_filename,
**build_task_tracking_kwargs(actor),
)
record_accepted_usage(actor, "repair-pdf", task.id)
return jsonify({"task_id": task.id, "message": "Repair started."}), 202
# ---------------------------------------------------------------------------
# Edit PDF Metadata — POST /api/v1/pdf-tools/metadata
# ---------------------------------------------------------------------------
@v1_bp.route("/pdf-tools/metadata", methods=["POST"])
@limiter.limit("10/minute")
def v1_edit_metadata_route():
"""Edit PDF metadata."""
actor, err = _resolve_and_check()
if err:
return err
if "file" not in request.files:
return jsonify({"error": "No file provided."}), 400
file = request.files["file"]
title = request.form.get("title")
author = request.form.get("author")
subject = request.form.get("subject")
keywords = request.form.get("keywords")
creator = request.form.get("creator")
if not any([title, author, subject, keywords, creator]):
return jsonify({"error": "At least one metadata field required."}), 400
try:
original_filename, ext = validate_actor_file(file, allowed_types=["pdf"], actor=actor)
except FileValidationError as e:
return jsonify({"error": e.message}), e.code
task_id, input_path = generate_safe_path(ext, folder_type="upload")
file.save(input_path)
task = edit_metadata_task.delay(
input_path, task_id, original_filename,
title, author, subject, keywords, creator,
**build_task_tracking_kwargs(actor),
)
record_accepted_usage(actor, "edit-metadata", task.id)
return jsonify({"task_id": task.id, "message": "Metadata editing started."}), 202
# ---------------------------------------------------------------------------
# Image Crop — POST /api/v1/image/crop
# ---------------------------------------------------------------------------
@v1_bp.route("/image/crop", methods=["POST"])
@limiter.limit("10/minute")
def v1_crop_image_route():
"""Crop an image."""
actor, err = _resolve_and_check()
if err:
return err
if "file" not in request.files:
return jsonify({"error": "No file provided."}), 400
file = request.files["file"]
try:
left = int(request.form.get("left", 0))
top = int(request.form.get("top", 0))
right = int(request.form.get("right", 0))
bottom = int(request.form.get("bottom", 0))
except (ValueError, TypeError):
return jsonify({"error": "Crop dimensions must be integers."}), 400
if right <= left or bottom <= top:
return jsonify({"error": "Invalid crop area."}), 400
try:
original_filename, ext = validate_actor_file(
file, allowed_types=ALLOWED_IMAGE_TYPES, actor=actor
)
except FileValidationError as e:
return jsonify({"error": e.message}), e.code
task_id, input_path = generate_safe_path(ext, folder_type="upload")
file.save(input_path)
task = crop_image_task.delay(
input_path, task_id, original_filename,
left, top, right, bottom,
**build_task_tracking_kwargs(actor),
)
record_accepted_usage(actor, "image-crop", task.id)
return jsonify({"task_id": task.id, "message": "Cropping started."}), 202
# ---------------------------------------------------------------------------
# Image Rotate/Flip — POST /api/v1/image/rotate-flip
# ---------------------------------------------------------------------------
@v1_bp.route("/image/rotate-flip", methods=["POST"])
@limiter.limit("10/minute")
def v1_rotate_flip_image_route():
"""Rotate and/or flip an image."""
actor, err = _resolve_and_check()
if err:
return err
if "file" not in request.files:
return jsonify({"error": "No file provided."}), 400
file = request.files["file"]
try:
rotation = int(request.form.get("rotation", 0))
except ValueError:
rotation = 0
if rotation not in (0, 90, 180, 270):
return jsonify({"error": "Rotation must be 0, 90, 180, or 270."}), 400
flip_horizontal = request.form.get("flip_horizontal", "false").lower() == "true"
flip_vertical = request.form.get("flip_vertical", "false").lower() == "true"
try:
original_filename, ext = validate_actor_file(
file, allowed_types=ALLOWED_IMAGE_TYPES, actor=actor
)
except FileValidationError as e:
return jsonify({"error": e.message}), e.code
task_id, input_path = generate_safe_path(ext, folder_type="upload")
file.save(input_path)
task = rotate_flip_image_task.delay(
input_path, task_id, original_filename,
rotation, flip_horizontal, flip_vertical,
**build_task_tracking_kwargs(actor),
)
record_accepted_usage(actor, "image-rotate-flip", task.id)
return jsonify({"task_id": task.id, "message": "Transformation started."}), 202
# ---------------------------------------------------------------------------
# Barcode — POST /api/v1/barcode/generate
# ---------------------------------------------------------------------------
@v1_bp.route("/barcode/generate", methods=["POST"])
@limiter.limit("20/minute")
def v1_generate_barcode_route():
"""Generate a barcode."""
actor, err = _resolve_and_check()
if err:
return err
if request.is_json:
body = request.get_json()
data = body.get("data", "").strip()
barcode_type = body.get("type", "code128").lower()
output_format = body.get("format", "png").lower()
else:
data = request.form.get("data", "").strip()
barcode_type = request.form.get("type", "code128").lower()
output_format = request.form.get("format", "png").lower()
if not data:
return jsonify({"error": "Barcode data is required."}), 400
if barcode_type not in SUPPORTED_BARCODE_TYPES:
return jsonify({"error": f"Unsupported type. Supported: {', '.join(SUPPORTED_BARCODE_TYPES)}"}), 400
if output_format not in ("png", "svg"):
output_format = "png"
task_id = str(uuid.uuid4())
task = generate_barcode_task.delay(
data, barcode_type, task_id, output_format,
**build_task_tracking_kwargs(actor),
)
record_accepted_usage(actor, "barcode", task.id)
return jsonify({"task_id": task.id, "message": "Barcode generation started."}), 202