Files
chore/api/image_api.py
2025-12-02 17:02:20 -05:00

107 lines
4.1 KiB
Python

import os
import uuid
from io import BytesIO
from flask import Blueprint, request, jsonify, send_file
from db.db import image_db
from models.image import Image
from tinydb import Query
from PIL import Image as PILImage, UnidentifiedImageError
image_api = Blueprint('image_api', __name__)
UPLOAD_FOLDER = './resources/images'
ALLOWED_EXTENSIONS = {'jpg', 'jpeg', 'png'}
IMAGE_TYPE_PROFILE = 1
IMAGE_TYPE_ICON = 2
MAX_DIMENSION = 512
def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@image_api.route('/image/upload', methods=['POST'])
def upload():
if 'file' not in request.files:
return jsonify({'error': 'No file part in the request'}), 400
file = request.files['file']
if file.filename == '':
return jsonify({'error': 'No selected file'}), 400
image_type = request.form.get('type', None)
if not image_type:
return jsonify({'error': 'Image type is required'}), 400
try:
image_type = int(image_type)
if image_type not in [IMAGE_TYPE_PROFILE, IMAGE_TYPE_ICON]:
return jsonify({'error': 'Invalid image type. Must be 1 or 2'}), 400
except ValueError:
return jsonify({'error': 'Image type must be an integer'}), 400
perm = request.form.get('permanent', "false").lower() == 'true'
if not file or not allowed_file(file.filename):
return jsonify({'error': 'Invalid file type or no file selected'}), 400
try:
pil_image = PILImage.open(file.stream)
original_format = pil_image.format # store before convert
pil_image.verify() # quick integrity check
file.stream.seek(0)
pil_image = PILImage.open(file.stream).convert('RGBA' if pil_image.mode in ('RGBA', 'LA') else 'RGB')
except UnidentifiedImageError:
return jsonify({'error': 'Uploaded file is not a valid image'}), 400
except Exception:
return jsonify({'error': 'Failed to process image'}), 400
if original_format not in ('JPEG', 'PNG'):
return jsonify({'error': 'Only JPEG and PNG images are allowed'}), 400
# Resize preserving aspect ratio (in-place thumbnail)
pil_image.thumbnail((MAX_DIMENSION, MAX_DIMENSION), PILImage.Resampling.LANCZOS)
format_extension_map = {'JPEG': '.jpg', 'PNG': '.png'}
extension = format_extension_map.get(original_format, '.png')
image_record = Image(extension=extension, permanent=perm, type=image_type)
filename = image_record.id + extension
filepath = os.path.abspath(os.path.join(UPLOAD_FOLDER, filename))
try:
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
# Save with appropriate format
save_params = {}
if pil_image.format == 'JPEG':
save_params['quality'] = 90
save_params['optimize'] = True
pil_image.save(filepath, format=pil_image.format, **save_params)
except Exception:
return jsonify({'error': 'Failed to save processed image'}), 500
image_db.insert(image_record.to_dict())
return jsonify({'message': 'Image uploaded successfully', 'filename': filename, 'id': image_record.id}), 200
@image_api.route('/image/request/<id>', methods=['GET'])
def request_image(id):
ImageQuery = Query()
image = image_db.get(ImageQuery.id == id)
if not image:
return jsonify({'error': 'Image not found'}), 404
filename = f"{image['id']}{image['extension']}"
filepath = os.path.abspath(os.path.join(UPLOAD_FOLDER, filename))
if not os.path.exists(filepath):
return jsonify({'error': 'File not found'}), 404
return send_file(filepath)
@image_api.route('/image/list', methods=['GET'])
def list_images():
image_type = request.args.get('type', type=int)
ImageQuery = Query()
if image_type is not None:
if image_type not in [IMAGE_TYPE_PROFILE, IMAGE_TYPE_ICON]:
return jsonify({'error': 'Invalid image type'}), 400
images = image_db.search(ImageQuery.type == image_type)
else:
images = image_db.all()
image_ids = [img['id'] for img in images]
return jsonify({'ids': image_ids, 'count': len(image_ids)}), 200