Make image crop target size configurable
This commit is contained in:
@@ -20,6 +20,24 @@ def create_app():
|
|||||||
app.config.setdefault("SQLALCHEMY_TRACK_MODIFICATIONS", False)
|
app.config.setdefault("SQLALCHEMY_TRACK_MODIFICATIONS", False)
|
||||||
app.config.setdefault("UPLOAD_FOLDER", os.path.join(app.root_path, "static", "uploads"))
|
app.config.setdefault("UPLOAD_FOLDER", os.path.join(app.root_path, "static", "uploads"))
|
||||||
|
|
||||||
|
# Target output resolution for cropped images.
|
||||||
|
# This is used by the client-side cropper (to generate an upload) and by the server-side
|
||||||
|
# image processing (to cap the resulting WEBP size).
|
||||||
|
#
|
||||||
|
# Defaults to Full HD landscape (1920x1080). Portrait is derived by swapping.
|
||||||
|
# Override via env vars, e.g.:
|
||||||
|
# IMAGE_CROP_TARGET_W=1920
|
||||||
|
# IMAGE_CROP_TARGET_H=1080
|
||||||
|
def _env_int(name: str, default: int) -> int:
|
||||||
|
try:
|
||||||
|
v = int(os.environ.get(name, "") or default)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
v = default
|
||||||
|
return max(1, v)
|
||||||
|
|
||||||
|
app.config.setdefault("IMAGE_CROP_TARGET_W", _env_int("IMAGE_CROP_TARGET_W", 1920))
|
||||||
|
app.config.setdefault("IMAGE_CROP_TARGET_H", _env_int("IMAGE_CROP_TARGET_H", 1080))
|
||||||
|
|
||||||
# NOTE: Videos should be max 250MB.
|
# NOTE: Videos should be max 250MB.
|
||||||
# Flask's MAX_CONTENT_LENGTH applies to the full request payload (multipart includes overhead).
|
# Flask's MAX_CONTENT_LENGTH applies to the full request payload (multipart includes overhead).
|
||||||
# We set this slightly above 250MB to allow for multipart/form fields overhead, while still
|
# We set this slightly above 250MB to allow for multipart/form fields overhead, while still
|
||||||
|
|||||||
@@ -147,15 +147,22 @@ def _save_compressed_image(
|
|||||||
img = img.convert("RGB")
|
img = img.convert("RGB")
|
||||||
|
|
||||||
# Optional crop
|
# Optional crop
|
||||||
|
# NOTE: The front-end may already upload a cropped image (canvas export), but we still
|
||||||
|
# enforce aspect + maximum output size here for consistency.
|
||||||
|
target_w = int(current_app.config.get("IMAGE_CROP_TARGET_W", 1920) or 1920)
|
||||||
|
target_h = int(current_app.config.get("IMAGE_CROP_TARGET_H", 1080) or 1080)
|
||||||
|
target_w = max(1, target_w)
|
||||||
|
target_h = max(1, target_h)
|
||||||
|
|
||||||
if cm == "16:9":
|
if cm == "16:9":
|
||||||
img = _center_crop_to_aspect(img, 16, 9)
|
img = _center_crop_to_aspect(img, 16, 9)
|
||||||
max_box = (1920, 1080)
|
max_box = (target_w, target_h)
|
||||||
elif cm == "9:16":
|
elif cm == "9:16":
|
||||||
img = _center_crop_to_aspect(img, 9, 16)
|
img = _center_crop_to_aspect(img, 9, 16)
|
||||||
max_box = (1080, 1920)
|
max_box = (target_h, target_w)
|
||||||
else:
|
else:
|
||||||
# No crop: allow both portrait and landscape up to 1920px on the longest side.
|
# No crop: allow both portrait and landscape up to target_w/target_h on the longest side.
|
||||||
max_box = (1920, 1920)
|
max_box = (max(target_w, target_h),) * 2
|
||||||
|
|
||||||
# Resize down if very large (keeps aspect ratio)
|
# Resize down if very large (keeps aspect ratio)
|
||||||
img.thumbnail(max_box)
|
img.thumbnail(max_box)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1 class="page-title">Welcome{% if current_user and current_user.email %}, {{ current_user.email }}{% endif %}!</h1>
|
<h1 class="page-title">Dashboard</h1>
|
||||||
|
|
||||||
<div class="row mt-4">
|
<div class="row mt-4">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
{# Expose server-side crop target sizes to the JS without embedding Jinja inside JS #}
|
||||||
|
<div
|
||||||
|
id="page-config"
|
||||||
|
class="d-none"
|
||||||
|
data-image-crop-target-w="{{ config.get('IMAGE_CROP_TARGET_W', 1920) }}"
|
||||||
|
data-image-crop-target-h="{{ config.get('IMAGE_CROP_TARGET_H', 1080) }}"
|
||||||
|
></div>
|
||||||
{# Cropper.js (used for image cropping) #}
|
{# Cropper.js (used for image cropping) #}
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/cropperjs@1.6.2/dist/cropper.min.css" />
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/cropperjs@1.6.2/dist/cropper.min.css" />
|
||||||
<style>
|
<style>
|
||||||
@@ -912,9 +919,15 @@
|
|||||||
|
|
||||||
cropStatus.textContent = 'Preparing cropped image…';
|
cropStatus.textContent = 'Preparing cropped image…';
|
||||||
const isPortrait = cm === '9:16';
|
const isPortrait = cm === '9:16';
|
||||||
|
// Export at Full HD by default (or whatever the server config says).
|
||||||
|
// We still enforce a server-side max output size in _save_compressed_image.
|
||||||
|
const cfg = document.getElementById('page-config');
|
||||||
|
const TARGET_W = parseInt(cfg?.dataset?.imageCropTargetW || '1920', 10) || 1920;
|
||||||
|
const TARGET_H = parseInt(cfg?.dataset?.imageCropTargetH || '1080', 10) || 1080;
|
||||||
|
|
||||||
const canvas = cropper.getCroppedCanvas({
|
const canvas = cropper.getCroppedCanvas({
|
||||||
width: isPortrait ? 720 : 1280,
|
width: isPortrait ? TARGET_H : TARGET_W,
|
||||||
height: isPortrait ? 1280 : 720,
|
height: isPortrait ? TARGET_W : TARGET_H,
|
||||||
imageSmoothingQuality: 'high',
|
imageSmoothingQuality: 'high',
|
||||||
});
|
});
|
||||||
const blob = await new Promise((resolve) => canvas.toBlob(resolve, 'image/png'));
|
const blob = await new Promise((resolve) => canvas.toBlob(resolve, 'image/png'));
|
||||||
|
|||||||
Reference in New Issue
Block a user