مجله ایرانیکاسرورآموزش سرور، هاست، وردپرس و شبکه
لینوکس

ساخت سرویس تبدیل تصویر به WebP و AVIF با Python روی VPS

ساخت سرویس تبدیل تصویر به WebP و AVIF با Python؛ API بهینه‌سازی تصویر روی VPS اگر وب‌سایت، فروشگاه اینترنتی، اپلیکیشن یا سرویس آپلود تصویر دارید، نگهداری همه تصاویر با فرمت JPEG و PNG می‌تواند…

ساخت سرویس تبدیل تصویر به WebP و AVIF با Python روی VPS
خلاصه سریع این آموزش

ساخت سرویس تبدیل تصویر به WebP و AVIF با Python؛ API بهینه‌سازی تصویر روی VPS اگر وب‌سایت، فروشگاه اینترنتی، اپلیکیشن یا سرویس آپلود تصویر دارید، نگهداری همه تصاویر با فرمت…

۱۱ دقیقه مطالعه ۰ دیدگاه ۱۰ مهر ۱۴۰۵ آخرین به‌روزرسانی

فهرست مطالب

ساخت سرویس تبدیل تصویر به 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 یا اپ موبایل متصل کرد تا بهینه‌سازی تصاویر کاملاً خودکار شود.

اشتراک‌گذاری مقاله
گفت‌وگو درباره آموزش

دیدگاه‌ها (۰)

هنوز دیدگاهی ثبت نشده است.

دیدگاه یا سؤال خود را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *