Bootcamp backendontwikkeling met FastAPI · Les

Asynchrone beeld- en documenttransformatie

Verwerk thumbnails, formaatwijzigingen en formaatconversies in background workers om requestlatentie laag te houden.

Les 4 van 413 stappen

Asynchrone beeld- en documenttransformatie is een gratis Bootcamp backendontwikkeling met FastAPI-les op CoddyKit. Dit is les 4 van 4. Je kunt 3 lessen uit dit leerpad gratis volledig lezen — daarna ontgrendelt CoddyKit PRO alle lessen, plus praktische oefeningen met een ingebouwde code-editor en een AI-tutor die 24/7 beschikbaar is. Deze les maakt deel uit van het leertraject Bootcamp backendontwikkeling met FastAPI. Je voortgang wordt gesynchroniseerd op het web en in de CoddyKit-app. De cursus Bootcamp backendontwikkeling met FastAPI bevat in totaal 4 lessen.

Waarom mediaverwerking uitbesteden

Het verkleinen van een afbeelding of converteren van een PDF kan honderden milliseconden tot enkele seconden duren. Als je dat werk binnen de aanvraagafhandeling uitvoert, wacht de client en wordt je workerproces geblokkeerd.

Het patroon voor FastAPI-services op B2-niveau is:

  • Accepteer de upload en sla het origineel snel op
  • Geef een 202 Accepted met een taak-id terug
  • Maak miniaturen, verklein afbeeldingen en converteer indelingen in een achtergrondworker

Zo blijft de latentie van aanvragen laag en kan zwaar CPU-werk onafhankelijk worden opgeschaald.

BackgroundTasks versus een echte wachtrij

FastAPI levert BackgroundTasks, waarmee een functie wordt uitgevoerd nadat het antwoord is verzonden, maar nog steeds binnen hetzelfde proces. Dit is prima voor goedkope, snelle vervolgacties, zoals een e-mail versturen of een logregel schrijven.

Voor CPU-intensieve mediabewerkingen is dit het verkeerde hulpmiddel: het concurreert met je eventlus en stopt als het proces opnieuw wordt gestart. Gebruik liever een speciale taakwachtrij (Celery, RQ, Dramatiq of arq) met Redis als backend, zodat werk deploys overleeft en horizontaal kan worden opgeschaald.

from fastapi import FastAPI, BackgroundTasks

app = FastAPI()

def log_upload(filename: str) -> None:
    # cheap follow-up work only
    print(f"received {filename}")

@app.post("/upload")
async def upload(background: BackgroundTasks):
    background.add_task(log_upload, "photo.png")
    return {"status": "accepted"}

Snel accepteren, later verwerken

Het eindpunt moet het minimale doen: het bestand valideren, het naar opslag streamen, een taakrecord aanmaken en een taak in de wachtrij zetten. Let erop dat we de upload in delen lezen, zodat een groot bestand nooit volledig in het geheugen wordt geladen.

  • await file.read(chunk) voorkomt enorme pieken in het geheugengebruik
  • We geven een job_id terug waarop de client kan peilen
  • De daadwerkelijke bewerking gebeurt in process_image.delay(...)
import uuid, aiofiles
from fastapi import FastAPI, UploadFile, status

app = FastAPI()

@app.post("/images", status_code=status.HTTP_202_ACCEPTED)
async def create_image(file: UploadFile):
    job_id = str(uuid.uuid4())
    dest = f"/data/originals/{job_id}_{file.filename}"
    async with aiofiles.open(dest, "wb") as out:
        while chunk := await file.read(1024 * 1024):
            await out.write(chunk)
    process_image.delay(job_id, dest)  # enqueue
    return {"job_id": job_id, "status": "queued"}

Miniaturen genereren met Pillow

Image.thumbnail() van Pillow verkleint op zijn plaats, behoudt de beeldverhouding en schaalt nooit op. Dit is de juiste basisbewerking voor miniaturen, omdat het resultaat binnen het opgegeven kader past.

Gebruik Image.LANCZOS-resampling voor scherpe verkleiningen en roep img.convert("RGB") aan voordat je als JPEG opslaat, zodat afbeeldingen met alfakanalen (PNG) de encoder niet laten crashen.

from PIL import Image

def make_thumbnail(src: str, dst: str, box=(256, 256)) -> None:
    with Image.open(src) as img:
        img = img.convert("RGB")
        img.thumbnail(box, Image.LANCZOS)
        img.save(dst, "JPEG", quality=85, optimize=True)

if __name__ == "__main__":
    print("thumbnail helper ready")

Een Celery-worker-taak

Elke bewerking wordt een Celery-taak. De taak is een gewone functie met de decorator @app.task; de wachtrij handelt nieuwe pogingen, bevestigingen en gelijktijdigheid af.

  • Genereer meerdere formaten in één taak om het decoderen van de afbeelding over meerdere resultaten te verdelen
  • Werk de taakstatus bij wanneer je klaar bent, zodat de API voortgang kan melden
  • Stel autoretry_for in, zodat tijdelijke I/O-fouten automatisch opnieuw worden geprobeerd
from celery import Celery
from PIL import Image

celery_app = Celery("media", broker="redis://localhost:6379/0")

SIZES = {"thumb": (256, 256), "medium": (1024, 1024)}

@celery_app.task(autoretry_for=(OSError,), retry_backoff=True, max_retries=3)
def process_image(job_id: str, src: str) -> dict:
    outputs = {}
    with Image.open(src) as base:
        base = base.convert("RGB")
        for name, box in SIZES.items():
            img = base.copy()
            img.thumbnail(box, Image.LANCZOS)
            dst = f"/data/derived/{job_id}_{name}.jpg"
            img.save(dst, "JPEG", quality=85, optimize=True)
            outputs[name] = dst
    return {"job_id": job_id, "outputs": outputs}

Indelingen converteren: PNG en WebP

WebP aanbieden in plaats van JPEG/PNG verkleint de payload met 25–35% bij vergelijkbare kwaliteit, waardoor het bandbreedtegebruik daalt en pagina's sneller laden.

Pillow converteert door eenvoudig de uitvoerindeling in save() te kiezen. Bewaar ook een kopie in de oorspronkelijke indeling, omdat sommige oudere clients WebP niet kunnen decoderen. Het onderstaande voorbeeld maakt vanuit één decodering zowel een JPEG als een WebP.

from PIL import Image

def to_jpeg_and_webp(src: str, stem: str) -> dict:
    with Image.open(src) as img:
        rgb = img.convert("RGB")
        jpeg_path = f"{stem}.jpg"
        webp_path = f"{stem}.webp"
        rgb.save(jpeg_path, "JPEG", quality=85, optimize=True)
        rgb.save(webp_path, "WEBP", quality=80, method=6)
    return {"jpeg": jpeg_path, "webp": webp_path}

if __name__ == "__main__":
    print(to_jpeg_and_webp.__name__)

De taakstatus volgen

Clients moeten weten wanneer hun afgeleide bestanden klaar zijn. Sla een klein statusrecord op (in Redis of je database), geïndexeerd op job_id, en bied een eindpunt om te peilen.

Levenscyclusstatussen zijn meestal queued -> processing -> done of failed. De worker werkt het record bij aan het begin en einde van de taak; de API leest het alleen.

import json, redis

r = redis.Redis()

def set_status(job_id: str, state: str, **extra) -> None:
    payload = {"state": state, **extra}
    r.set(f"job:{job_id}", json.dumps(payload), ex=86400)

def get_status(job_id: str) -> dict | None:
    raw = r.get(f"job:{job_id}")
    return json.loads(raw) if raw else None

Eindpunt voor peilen en resultaat-URL's

Het statuseindpunt geeft de huidige status terug en, zodra de taak klaar is, de URL's van de gegenereerde bestanden. Geef 404 terug voor een onbekende taak en anders 200 met de status.

Een gebruikelijke verbetering is om voor elk afgeleid bestand een vooraf ondertekende S3-URL terug te geven, zodat de client het rechtstreeks uit object storage downloadt in plaats van via je API.

from fastapi import FastAPI, HTTPException

app = FastAPI()

@app.get("/images/{job_id}")
async def image_status(job_id: str):
    status = get_status(job_id)
    if status is None:
        raise HTTPException(status_code=404, detail="job not found")
    return {"job_id": job_id, **status}

De eventlus vrijhouden

Ook buiten een worker moet je soms een blokkerende bibliotheek (Pillow, een PDF-hulpmiddel) aanroepen vanuit een asynchroon eindpunt. Als je dit rechtstreeks doet, blokkeer je de eventlus en lopen alle gelijktijdige aanvragen vast.

Verplaats dit naar een threadpool met asyncio.to_thread (of Starlette's run_in_threadpool). Voor CPU-gebonden batches over meerdere kernen voorkomt een ProcessPoolExecutor problemen door de GIL. Het uitvoerbare voorbeeld laat het patroon voor het uitbesteden aan een thread zien.

import asyncio, time

def blocking_resize(n: int) -> int:
    time.sleep(0.1)  # stand-in for Pillow work
    return n * n

async def handle(n: int) -> int:
    # runs blocking_resize in a worker thread, loop stays free
    return await asyncio.to_thread(blocking_resize, n)

async def main() -> None:
    results = await asyncio.gather(*(handle(i) for i in range(5)))
    print(results)

if __name__ == "__main__":
    asyncio.run(main())

Documenten converteren naar PDF/afbeeldingen

Documentbewerkingen (DOCX naar PDF, een PDF-pagina naar een PNG-miniatuur) roepen meestal externe hulpmiddelen aan, zoals LibreOffice (soffice --headless) of pdftoppm. Deze zijn zwaar en traag, dus ze horen in een workertaak en nooit in het aanvraagpad.

Voer ze altijd uit met een time-out en vang fouten op, omdat externe converters kunnen blijven hangen bij ongeldige invoer.

import subprocess

def docx_to_pdf(src: str, out_dir: str) -> str:
    subprocess.run(
        ["soffice", "--headless", "--convert-to", "pdf",
         "--outdir", out_dir, src],
        check=True, timeout=120,
    )
    return out_dir

@celery_app.task(autoretry_for=(subprocess.TimeoutExpired,), max_retries=2)
def convert_document(job_id: str, src: str) -> dict:
    out = docx_to_pdf(src, "/data/derived")
    set_status(job_id, "done", out_dir=out)
    return {"job_id": job_id, "out_dir": out}

Validatie, limieten en opruimen

Niet-vertrouwde media vormen een beveiligingsrisico. Beveilig de verwerkingsketen voordat er zwaar werk wordt uitgevoerd:

  • Controleer het type aan de hand van de inhoud (bijvoorbeeld met Image.open().verify() of magische bytes), niet alleen aan de hand van de bestandsextensie
  • Beperk de afmetingen om decompressiebom-afbeeldingen onschadelijk te maken; stel Image.MAX_IMAGE_PIXELS in
  • Handhaaf groottelimieten tijdens het streamen van de upload
  • Ruim op bij fouten of na een TTL: verwijder originelen en afgeleide bestanden

Wijs ongeldige invoer vroeg af, zodat een kwaadaardig bestand de worker nooit bereikt.

from PIL import Image, UnidentifiedImageError

Image.MAX_IMAGE_PIXELS = 50_000_000  # guard against decompression bombs

def is_safe_image(path: str) -> bool:
    try:
        with Image.open(path) as img:
            img.verify()  # checks integrity without full decode
        return True
    except (UnidentifiedImageError, OSError):
        return False

Snelle controle

Test je begrip van waar zwaar mediawerk thuishoort.

Samenvatting

Je hebt geleerd hoe je media-intensieve FastAPI-eindpunten snel houdt:

  • Snel accepteren, later verwerken: stream de upload naar opslag, geef 202 met een taak-id terug en zet het werk in de wachtrij
  • Gebruik een echte wachtrij (Celery/RQ/arq + Redis) voor CPU-intensieve bewerkingen; bewaar BackgroundTasks voor goedkope vervolgacties
  • Voer bewerkingen uit met Pillow: thumbnail() voor verkleiningen met behoud van de beeldverhouding, convert("RGB") vóór JPEG en WebP voor kleinere payloads
  • Documentconversie roept hulpmiddelen zoals LibreOffice aan met een time-out, altijd in een worker
  • Blokkeer de lus nooit: besteed losse blokkerende aanroepen uit met asyncio.to_thread
  • Beveilig de invoer: controleer het type, beperk het aantal pixels en de grootte en ruim afgeleide bestanden op
Gratis beginnen

Leer Bootcamp backendontwikkeling met FastAPI met een AI-tutor — gratis

Schrijf echte code en voer die uit in je browser, krijg direct hulp van een AI-tutor die 24/7 beschikbaar is en ga verder waar je gebleven bent op het web of in de app.

Cursussen
21
Lessen
84

Veelgestelde vragen

Is de les “Asynchrone beeld- en documenttransformatie” gratis?

Ja — je kunt hier op het web alle 3 lessen van het leerpad Bootcamp backendontwikkeling met FastAPI, waaronder “Asynchrone beeld- en documenttransformatie”, gratis volledig lezen. Daarna ontgrendelt CoddyKit PRO alle lessen, plus interactieve oefeningen met een ingebouwde code-editor en een AI-tutor die 24/7 beschikbaar is. De cursus Bootcamp backendontwikkeling met FastAPI bevat in totaal 4 lessen.

Wat leer ik in “Asynchrone beeld- en documenttransformatie”?

Verwerk thumbnails, formaatwijzigingen en formaatconversies in background workers om requestlatentie laag te houden. Je oefent met Bootcamp backendontwikkeling met FastAPI door code rechtstreeks in de browser uit te voeren. Een AI-begeleider die 24/7 beschikbaar is beantwoordt je vragen terwijl je de les doorwerkt.

Heb ik ervaring nodig om met Bootcamp backendontwikkeling met FastAPI te beginnen?

Ervaring vooraf is niet nodig. Bootcamp backendontwikkeling met FastAPI op CoddyKit is opgebouwd voor beginners tot gevorderden, zodat je hier of bij het begin kunt starten en in je eigen tempo kunt leren. Dit is les 4 van 4.

Hoe lang duurt de les “Asynchrone beeld- en documenttransformatie”?

De meeste lessen van CoddyKit duren ongeveer 5–10 minuten. Elke les is kort en interactief, zodat je gestaag vooruitgaat en op het web en in de app precies verdergaat waar je was gebleven.

Kan ik code schrijven en uitvoeren in deze les over Bootcamp backendontwikkeling met FastAPI?

Ja. Elke les over Bootcamp backendontwikkeling met FastAPI bevat een ingebouwde code-editor, zodat je rechtstreeks in je browser echte code kunt schrijven en uitvoeren en direct feedback van AI krijgt — lokale installatie is niet nodig.

Alle lessen in deze cursus

  1. Multipartuploads en contentvalidatie
  2. Streamingresponses en range requests
  3. Opslag uitbesteden aan S3-compatibele buckets
  4. Asynchrone beeld- en documenttransformatie
← Terug naar Bootcamp backendontwikkeling met FastAPI