ساخت سرویس تبدیل تصویر به WebP و AVIF با Python؛ API بهینهسازی تصویر روی VPS
اگر وبسایت، فروشگاه اینترنتی، اپلیکیشن یا سرویس آپلود تصویر دارید، نگهداری همه تصاویر با فرمت JPEG و PNG میتواند حجم ترافیک و زمان بارگذاری صفحات را افزایش دهد. یکی از راههای عملی برای کاهش حجم تصاویر، تبدیل خودکار آنها به WebP و AVIF هنگام آپلود است.
در این آموزش یک Image Conversion API شخصی روی سرور مجازی لینوکس میسازیم. کاربر تصویر را به API ارسال میکند، عرض، ارتفاع، کیفیت و فرمت خروجی را مشخص میکند و سرور نسخه WebP یا AVIF را تحویل میدهد. برای پردازش از Python، FastAPI، pyvips و libvips استفاده میکنیم؛ ترکیبی که برای پردازش تصاویر بزرگ و تعداد درخواست بیشتر، انتخاب مناسبی برای یک سرویس سمت سرور است.
📌 خروجی پروژه:
🔹 API تبدیل JPEG و PNG به WebP
🔹 تبدیل تصویر به AVIF با AV1
🔹 Resize خودکار تصویر
🔹 حفظ نسبت طول و عرض
🔹 جلوگیری از Upscale ناخواسته
🔹 تنظیم Quality از طریق API
🔹 محدودیت حجم Upload
🔹 محدودیت تعداد Pixel برای تصاویر بسیار بزرگ
🔹 احراز هویت API با API Key
🔹 اجرا پشت Nginx و HTTPS
WebP و AVIF چه تفاوتی دارند؟
WebP فرمت تصویری مدرنی است که Lossy و Lossless Compression، Transparency و Animation را پشتیبانی میکند و برای استفاده عمومی وب انتخاب بسیار مناسبی است.
AVIF از فشردهسازی AV1 داخل ساختار HEIF استفاده میکند و در بسیاری از تصاویر میتواند با کیفیت بصری مشابه، فایل کوچکتری تولید کند؛ اما Encoding آن معمولاً CPU بیشتری نسبت به WebP مصرف میکند.
| ویژگی | WebP | AVIF |
|---|---|---|
| سرعت Encoding | معمولاً سریعتر | معمولاً سنگینتر |
| Lossy | بله | بله |
| Lossless | بله | بله |
| Alpha/Transparency | بله | بله |
| کاربرد پیشنهادی | خروجی سریع عمومی | کاهش بیشتر حجم در پروژههای مناسب |
چرا برای این پروژه libvips را انتخاب کردیم؟
برای پردازش تصویر با Python گزینههای مختلفی وجود دارد. Pillow برای پروژههای ساده بسیار مناسب است، اما وقتی هدف ساخت یک سرویس پردازش تصویر با فایلهای بزرگ و درخواستهای متعدد باشد، libvips انتخاب جذابی است.
pyvips Binding پایتون برای libvips است و اجازه میدهد Resize، Convert و عملیات تصویری را از داخل Python انجام دهیم، در حالی که بخش اصلی پردازش توسط کتابخانه native libvips انجام میشود.
نسخه فعلی هنگام نگارش: libvips 8.18.6
Binding مورد استفاده: pyvips
Backend AVIF: libheif + AV1 encoder
برای ساخت سرویس چه VPSای نیاز داریم؟
پردازش تصویر CPU مصرف میکند، اما نسبت به Video Transcoding سبکتر است. برای یک API کوچک میتوان با ۲ vCPU و ۲GB RAM شروع کرد. اگر AVIF زیاد تولید میکنید یا تعداد درخواستها بالا است، CPU بیشتر تأثیر مستقیمی روی ظرفیت سرویس خواهد داشت.
اجرای Image API روی سرور مجازی ایران
اگر تصاویر سایت یا اپلیکیشن شما عمدتاً برای کاربران داخل ایران پردازش میشوند، میتوانید این سرویس را روی سرور مجازی ایران اجرا کنید و Application اصلی تصاویر را برای تبدیل به این API ارسال کند.
مرحله اول: نصب Python و libvips روی Ubuntu 24.04
sudo apt update sudo apt upgrade -y sudo apt install python3 python3-venv python3-pip nginx libvips-tools libvips-dev libheif1 libheif-plugin-aomenc libheif-plugin-aomdec -y
نسخه libvips را بررسی کنید:
vips --version
همچنین بررسی کنید Writerهای WebP و HEIF در Build فعلی وجود داشته باشند:
vips -l | grep -E "webpsave|heifsave"
⚠️ نکته: AVIF در libvips از HEIF Writer با Compression نوع AV1 ساخته میشود. اگر libheif روی سیستم بدون AV1 Encoder نصب شده باشد، تولید AVIF کار نخواهد کرد. روی Ubuntu 24.04 بسته libheif-plugin-aomenc برای Encoder مبتنی بر AOM در دسترس است.
مرحله دوم: ساخت User و پروژه
sudo mkdir -p /opt/image-converter sudo useradd --system --home /opt/image-converter --shell /usr/sbin/nologin imageapi sudo chown -R imageapi:imageapi /opt/image-converter
مرحله سوم: نصب FastAPI و pyvips
sudo -u imageapi \ python3 -m venv \ /opt/image-converter/venv sudo -u imageapi /opt/image-converter/venv/bin/pip install --upgrade pip sudo -u imageapi /opt/image-converter/venv/bin/pip install fastapi uvicorn python-multipart pyvips
مرحله چهارم: کد کامل API تبدیل WebP و AVIF
فایل برنامه را ایجاد کنید:
sudo nano /opt/image-converter/app.py
کد زیر را قرار دهید:
import os
import secrets
import tempfile
from pathlib import Path
from typing import Literal
import pyvips
from fastapi import (
FastAPI,
File,
Form,
Header,
HTTPException,
UploadFile,
)
from fastapi.responses import Response
app = FastAPI(
title="Iranica Image Converter"
)
API_KEY = os.getenv(
"IMAGE_API_KEY",
""
)
MAX_UPLOAD_BYTES = 25 * 1024 * 1024
MAX_PIXELS = 60_000_000
ALLOWED_TYPES = {
"image/jpeg",
"image/png",
"image/webp",
"image/avif",
"image/heic",
}
def check_api_key(value: str | None):
if not API_KEY:
return
if not secrets.compare_digest(
value or "",
API_KEY
):
raise HTTPException(
status_code=401,
detail="Invalid API key"
)
def resize_down(
image,
width: int | None,
height: int | None,
):
scales = []
if width:
scales.append(
width / image.width
)
if height:
scales.append(
height / image.height
)
if not scales:
return image
scale = min(scales)
# از بزرگ کردن تصویر جلوگیری میکنیم.
scale = min(scale, 1.0)
if scale >= 1.0:
return image
return image.resize(
scale,
kernel="lanczos3"
)
@app.get("/health")
def health():
return {
"status": "ok",
"libvips": pyvips.version(0),
}
@app.post("/convert")
async def convert_image(
file: UploadFile = File(...),
format: Literal["webp", "avif"] = Form("webp"),
quality: int = Form(80),
width: int | None = Form(None),
height: int | None = Form(None),
x_api_key: str | None = Header(None),
):
check_api_key(x_api_key)
if file.content_type not in ALLOWED_TYPES:
raise HTTPException(
status_code=415,
detail="Unsupported image type"
)
if quality < 20 or quality > 100:
raise HTTPException(
status_code=400,
detail="Quality must be 20-100"
)
if width is not None and (
width < 1 or width > 8000
):
raise HTTPException(
status_code=400,
detail="Invalid width"
)
if height is not None and (
height < 1 or height > 8000
):
raise HTTPException(
status_code=400,
detail="Invalid height"
)
suffix = Path(
file.filename or "upload.bin"
).suffix[:12]
temp_path = None
total = 0
try:
with tempfile.NamedTemporaryFile(
suffix=suffix,
delete=False
) as temp:
temp_path = temp.name
while True:
chunk = await file.read(
1024 * 1024
)
if not chunk:
break
total += len(chunk)
if total > MAX_UPLOAD_BYTES:
raise HTTPException(
status_code=413,
detail="Image too large"
)
temp.write(chunk)
try:
image = pyvips.Image.new_from_file(
temp_path,
access="sequential",
fail_on="error"
)
except pyvips.Error:
raise HTTPException(
status_code=422,
detail="Invalid image"
)
pixels = (
image.width
* image.height
)
if pixels > MAX_PIXELS:
raise HTTPException(
status_code=413,
detail="Too many pixels"
)
# EXIF Orientation را اعمال میکند.
image = image.autorot()
image = resize_down(
image,
width,
height
)
if format == "webp":
output = image.webpsave_buffer(
Q=quality,
effort=4,
smart_subsample=True
)
media_type = "image/webp"
extension = "webp"
else:
output = image.heifsave_buffer(
Q=quality,
compression="av1",
encoder="aom",
effort=4
)
media_type = "image/avif"
extension = "avif"
source_name = Path(
file.filename or "image"
).stem
response = Response(
content=bytes(output),
media_type=media_type
)
response.headers[
"Content-Disposition"
] = (
f'inline; filename="'
f'{source_name}.{extension}"'
)
response.headers[
"Cache-Control"
] = "no-store"
return response
finally:
await file.close()
if temp_path:
try:
os.unlink(temp_path)
except FileNotFoundError:
pass
این API چطور کار میکند؟
تصویر آپلودشده ابتدا به فایل موقت منتقل میشود. این کار باعث میشود مجبور نباشیم یک تصویر بزرگ را کامل داخل RAM برنامه نگه داریم.
بعد libvips فایل را باز میکند، تعداد Pixel بررسی میشود و Orientation ذخیرهشده در EXIF اعمال میشود. اگر width یا height ارسال شده باشد، تصویر بدون تغییر نسبت ابعاد کوچک میشود.
در خروجی WebP از webpsave_buffer و در خروجی AVIF از HEIF Writer با Compression نوع AV1 استفاده میکنیم.
چرا محدودیت Pixel مهم است؟
فقط بررسی حجم فایل کافی نیست. یک فایل فشرده کوچک ممکن است پس از Decode به تصویری با دهها یا صدها میلیون Pixel تبدیل شود و RAM یا CPU زیادی مصرف کند.
به همین دلیل علاوه بر ۲۵ مگابایت Upload، تعداد Pixel را نیز محدود کردهایم.
مرحله پنجم: ساخت API Key
openssl rand -hex 32
این کلید را بعداً در Environment سرویس قرار میدهیم. API عمومی تبدیل تصویر بدون Authentication میتواند توسط دیگران برای مصرف CPU شما استفاده شود.
مرحله ششم: اجرای API با systemd
sudo nano \ /etc/systemd/system/image-converter.service
[Unit]
Description=Image WebP AVIF Converter API
After=network.target
[Service]
User=imageapi
Group=imageapi
WorkingDirectory=/opt/image-converter
Environment="IMAGE_API_KEY=YOUR_SECRET_API_KEY"
Environment="VIPS_CONCURRENCY=2"
ExecStart=/opt/image-converter/venv/bin/uvicorn
app:app
--host 127.0.0.1
--port 8100
--workers 2
Restart=on-failure
RestartSec=3
NoNewPrivileges=true
PrivateTmp=true
ProtectHome=true
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload sudo systemctl enable --now image-converter sudo systemctl status image-converter
مرحله هفتم: قرار دادن Nginx جلوی API
server {
listen 80;
server_name image-api.example.com;
client_max_body_size 25m;
location / {
proxy_pass http://127.0.0.1:8100;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 120s;
}
}
سپس HTTPS را با Certbot فعال کنید. API Key و فایلهای تصویر نباید روی HTTP بدون رمزنگاری منتقل شوند.
تست تبدیل JPEG به WebP
curl -X POST \ https://image-api.example.com/convert \ -H "X-API-Key: YOUR_SECRET_API_KEY" \ -F "file=@photo.jpg" \ -F "format=webp" \ -F "quality=82" \ -F "width=1200" \ --output photo.webp
تبدیل تصویر به AVIF
curl -X POST \ https://image-api.example.com/convert \ -H "X-API-Key: YOUR_SECRET_API_KEY" \ -F "file=@photo.jpg" \ -F "format=avif" \ -F "quality=70" \ -F "width=1200" \ --output photo.avif
Quality مناسب WebP و AVIF چقدر است؟
یک عدد ثابت برای همه تصاویر وجود ندارد. عکس، Screenshot، Illustration و تصویر دارای Text رفتار متفاوتی دارند.
| کاربرد | WebP شروع پیشنهادی | AVIF شروع پیشنهادی |
|---|---|---|
| تصاویر وبلاگ | 75 تا 85 | 55 تا 70 |
| تصویر محصول | 80 تا 90 | 65 تا 80 |
| Thumbnail | 70 تا 80 | 50 تا 65 |
این اعداد نقطه شروع هستند؛ برای پروژه واقعی نمونه تصاویر خودتان را از نظر حجم و کیفیت بصری آزمایش کنید.
AVIF چرا CPU بیشتری مصرف میکند؟
AVIF از فشردهسازی AV1 استفاده میکند. افزایش effort میتواند زمان Encoding را بیشتر کند و در مقابل روی Compression اثر بگذارد. برای API بلادرنگ نباید بیشترین Effort را صرفاً برای چند درصد کاهش حجم انتخاب کرد.
در نمونه ما effort روی ۴ قرار دارد تا تعادل مناسبتری میان سرعت و حجم ایجاد شود.
چطور این سرویس را به WordPress وصل کنیم؟
میتوانید Plugin یا Hook بسازید که هنگام Upload تصویر، فایل اصلی را برای API ارسال کند و خروجی WebP/AVIF را کنار تصویر اصلی ذخیره کند.
روش دیگر این است که Application اصلی فقط تصاویر Original را نگه دارد و نسخههای Optimized را داخل Object Storage یا CDN ذخیره کند.
اگر تعداد زیادی Size برای هر تصویر نیاز دارید، معماری Image CDN که Resize را براساس URL انجام میدهد از تبدیل دستی همه نسخهها منعطفتر خواهد بود.
امنیت سرویس تبدیل تصویر
✅ API را بدون API Key عمومی نکنید.
✅ حجم Upload را محدود کنید.
✅ تعداد Pixel تصویر را محدود کنید.
✅ سیستم و libvips را بهروز نگه دارید.
✅ فایل ورودی را با نام اصلی در مسیر عمومی ذخیره نکنید.
✅ Uvicorn را مستقیماً روی اینترنت باز نکنید.
✅ HTTPS را فعال کنید.
✅ Rate Limit روی Nginx یا API Gateway اضافه کنید.
چرا در این API دریافت تصویر از URL خارجی نگذاشتیم؟
قابلیتهایی مانند:
POST /convert?url=https://example.com/photo.jpg
در ظاهر راحت هستند، اما اگر بدون محدودیت پیادهسازی شوند میتوانند سرور را در معرض SSRF قرار دهند؛ یعنی مهاجم API را مجبور کند به IPهای داخلی، localhost یا سرویسهای Metadata متصل شود.
اگر بعداً Source URL اضافه میکنید، Domain Allowlist، محدودیت Redirect، محدودیت IP خصوصی و Timeout مناسب ضروری است.
خطاهای رایج
heifsave پیدا نمیشود
Build نصبشده libvips یا dependencyهای HEIF را بررسی کنید:
vips -l | grep heifsave
AVIF Encoder پیدا نمیشود
روی Ubuntu 24.04 بسته AOM Encoder را بررسی کنید:
dpkg -l | grep libheif-plugin-aomenc
API هنگام AVIF کند است
AVIF Encoding CPU-intensive است. effort را کاهش دهید، Workerها را متناسب با CPU تنظیم کنید و تبدیلهای پرتکرار را Cache کنید.
Memory مصرفی زیاد میشود
تعداد Uvicorn Worker، VIPS_CONCURRENCY، حداکثر Pixel و تعداد درخواستهای همزمان را بررسی کنید. بالا بردن Worker بدون توجه به RAM همیشه Performance را بهتر نمیکند.
سؤالات متداول
برای تبدیل تصویر WebP در Python چه کتابخانهای مناسب است؟
برای پروژههای ساده Pillow مناسب است؛ برای سرویس سمت سرور با پردازش بیشتر، pyvips/libvips گزینه قدرتمندی است.
آیا libvips میتواند AVIF بسازد؟
بله. HEIF Writer در libvips از Compression نوع AV1 پشتیبانی میکند، به شرط اینکه libheif دارای AV1 Encoder مناسب باشد.
WebP بهتر است یا AVIF؟
برای خروجی سریع و سازگاری گسترده WebP انتخاب سادهای است. AVIF میتواند Compression بسیار خوبی ایجاد کند اما معمولاً Encoding سنگینتری دارد.
آیا میتوان این API را روی VPS اجرا کرد؟
بله. برای سرویس کوچک میتوان با VPS لینوکس چند هستهای شروع کرد و براساس تعداد Conversion منابع را افزایش داد.
آیا Image Conversion API همان Image CDN است؟
خیر. این API تصویر را هنگام درخواست آپلود و تبدیل میکند. Image CDN معمولاً Source ثابت، Transformation مبتنی بر URL و Cache گسترده دارد.
جمعبندی
با Python، FastAPI و libvips میتوان بدون سرویس خارجی یک API شخصی برای تبدیل و Resize تصویر ساخت. WebP برای تبدیل سریع و AVIF برای سناریوهایی که Compression بیشتر ارزش مصرف CPU را دارد، قابل استفاده هستند.
برای محیط Production مهمترین بخشها فقط کد Conversion نیستند؛ محدودیت حجم و Pixel، Rate Limit، API Key، HTTPS و بهروزرسانی libvips نیز باید در طراحی لحاظ شوند.
در مرحله بعد میتوان همین API را به Object Storage، CDN شخصی، WordPress، Laravel یا اپ موبایل متصل کرد تا بهینهسازی تصاویر کاملاً خودکار شود.






دیدگاهها (۰)