High-Performance Cloud API: Streaming HEIC to AVIF with FastAPI¶
Modern mobile devices (iPhones, modern Android smartphones) capture images in HEIC or Ultra HDR AVIF by default. When building web backends, microservices often need to convert these incoming images on-the-fly for web delivery without saving temporary files to disk and without duplicating memory.
This tutorial demonstrates how to build an asynchronous, zero-disk-I/O FastAPI service using pylibheif's native streaming APIs (read_from_stream and write_to_memoryview).
Why Native Streaming?¶
| Operation | Traditional Pipeline | pylibheif Native Stream Pipeline |
|---|---|---|
| File I/O | Writes temp file to /tmp, reads back |
Direct in-memory stream read from socket/buffer |
| Decoding | Intermediate copies into RGB arrays | Direct planar decoding with zero memory copy |
| Encoding Export | Copies C++ vector -> Python bytes |
Returns memoryview backed by C++ RAII capsule |
| Disk Operations | High SSD write wear & OS disk contention | 0 disk operations |
Complete FastAPI Service Implementation¶
import io
import asyncio
from fastapi import FastAPI, UploadFile, File, Query, HTTPException
from fastapi.responses import Response
import pylibheif
app = FastAPI(title="pylibheif Image Converter API")
@app.post("/convert/avif")
async def convert_to_avif(
file: UploadFile = File(...),
quality: int = Query(80, ge=1, le=100, description="Target AVIF quality"),
preset: str = Query("fast", regex="^(ultrafast|fast|balanced|quality)$")
):
"""
Ingest a HEIC/JPEG file stream from an upload and return an optimized AVIF image.
Uses in-memory streaming and zero-copy memoryview export.
"""
if not file.filename.lower().endswith((".heic", ".heif", ".jpg", ".jpeg", ".png")):
raise HTTPException(status_code=400, detail="Unsupported input image format.")
# 1. Read input stream into an in-memory buffer (or spool)
contents = await file.read()
input_stream = io.BytesIO(contents)
# 2. Decode HEIF container asynchronously without blocking event loop
loop = asyncio.get_running_loop()
def _process_image():
# Open context directly from Python file-like stream
ctx = pylibheif.HeifContext()
ctx.read_from_stream(input_stream)
handle = ctx.get_primary_image_handle()
# Decode to planar RGB
image = handle.decode(
pylibheif.HeifColorspace.RGB,
pylibheif.HeifChroma.InterleavedRGB24
)
# 3. Setup destination context and AVIF encoder
out_ctx = pylibheif.HeifContext()
encoder = pylibheif.HeifEncoder(pylibheif.HeifCompressionFormat.AV1)
encoder.apply_preset(preset)
encoder.quality = quality
# Preserve color profile if present
profile_type = handle.get_color_profile_type()
if profile_type == pylibheif.HeifColorProfileType.Prof:
icc_data = handle.get_raw_color_profile()
out_ctx.set_color_profile_icc(icc_data)
elif profile_type == pylibheif.HeifColorProfileType.Nclx:
nclx = handle.get_nclx_color_profile()
out_ctx.set_color_profile_nclx(nclx)
# Encode image into out_ctx
out_ctx.encode_image(image, encoder)
# 4. Zero-copy export as memoryview backed by native C++ buffer
return bytes(out_ctx.write_to_memoryview())
# Offload heavy native encoding to worker thread pool
avif_bytes = await loop.run_in_executor(None, _process_image)
return Response(
content=avif_bytes,
media_type="image/avif",
headers={
"Content-Disposition": f'inline; filename="{file.filename.rsplit(".", 1)[0]}.avif"',
"Cache-Control": "public, max-age=31536000, immutable"
}
)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
Production Optimizations¶
1. Concurrency Budgeting¶
In multi-core server environments, internal codec threads (e.g. dav1d, aom) can contend with web worker processes. Set default threads globally on startup:
@app.on_event("startup")
def setup_codecs():
# Limit internal threads per encode task to prevent CPU oversubscription
pylibheif.set_default_num_threads(2)
2. S3 / Cloudflare R2 Direct Streaming¶
When reading images directly from object storage (e.g. boto3 or httpx), pass the raw response stream directly into ctx.read_from_stream():