Creating a simple image processing API with Python and Flask

Image processing APIs enable developers to programmatically manipulate and transform images. In this tutorial, we'll build a straightforward yet functional image processing API using Python and Flask, demonstrating how to handle common operations like resizing and format conversion. This demo also showcases robust error handling, rate limiting, and secure file validation practices.
Introduction to image processing APIs
An image processing API provides endpoints that accept image files as input, perform specified operations, and return the processed images. These APIs are essential for applications that need to handle user-uploaded images, generate thumbnails, or convert between different image formats.
Setting up the development environment
Begin by setting up your development environment:
- Create a new directory for your project and a requirements.txt file:
mkdir flask-image-api
cd flask-image-api
touch requirements.txt
- Add the following dependencies to requirements.txt:
Flask==3.1.0
Pillow==11.1.0
gunicorn==23.0.0
flask-cors==4.0.0
flask-limiter==3.5.0
- Set up a virtual environment and install dependencies:
python3 -m venv venv
source venv/bin/activate # On Windows use `venv\Scripts\activate`
pip install -r requirements.txt
Creating a basic Flask application
Create a file named app.py
with the following content:
from flask import Flask, request, send_file
from flask_cors import CORS
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
from PIL import Image
import io
app = Flask(__name__)
CORS(app)
limiter = Limiter(app, key_func=get_remote_address)
# Configure maximum file size (16 mb)
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024
# Allowed file extensions
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@app.route('/')
def index():
return 'Welcome to the Image Processing API!'
if __name__ == '__main__':
app.run(debug=True)
Integrating pillow for image processing
Create a helper function to load and validate images. This function checks both the file extension and its MIME type, ensuring only valid image files are processed:
def load_image(file):
try:
if not allowed_file(file.filename) or not file.mimetype.startswith('image/'):
return None
img = Image.open(file)
img.verify() # Verify image integrity
file.seek(0) # Reset file pointer after verification
return Image.open(file)
except Exception:
return None
Implementing image resizing endpoint
Add an endpoint for resizing images with appropriate error handling and validation:
@app.route('/resize', methods=['POST'])
@limiter.limit('100 per day')
def resize_image():
try:
if 'file' not in request.files:
return {'error': 'No file part'}, 400
file = request.files['file']
if file.filename == '':
return {'error': 'No selected file'}, 400
img = load_image(file)
if img is None:
return {'error': 'Invalid image file'}, 400
width = int(request.form.get('width', 100))
height = int(request.form.get('height', 100))
if width <= 0 or height <= 0:
return {'error': 'Invalid dimensions'}, 400
resized_img = img.resize((width, height))
img_io = io.BytesIO()
resized_img.save(img_io, format=img.format or 'JPEG')
img_io.seek(0)
return send_file(
img_io,
mimetype=f'image/{img.format.lower() if img.format else "jpeg"}'
)
except Exception as e:
return {'error': str(e)}, 500
Adding image format conversion endpoint
Include another endpoint to handle image format conversion. This endpoint accepts a target format, converts the image accordingly, and handles any potential errors:
@app.route('/convert', methods=['POST'])
@limiter.limit('100 per day')
def convert_image():
try:
if 'file' not in request.files:
return {'error': 'No file part'}, 400
file = request.files['file']
if file.filename == '':
return {'error': 'No selected file'}, 400
img = load_image(file)
if img is None:
return {'error': 'Invalid image file'}, 400
target_format = request.form.get('format', 'PNG').upper()
if target_format not in ['PNG', 'JPEG', 'GIF']:
return {'error': 'Unsupported format'}, 400
img_io = io.BytesIO()
if target_format == 'JPEG':
img = img.convert('RGB') # Remove alpha channel for JPEG compatibility
img.save(img_io, format=target_format)
img_io.seek(0)
return send_file(img_io, mimetype=f'image/{target_format.lower()}')
except Exception as e:
return {'error': str(e)}, 500
Testing the API with sample requests
Use cURL to test the endpoints. The following examples demonstrate how to call the resizing and conversion endpoints:
# Resize an image
curl -fsSL -X POST -F "file=@path/to/image.jpg" -F "width=300" -F "height=200" \
http://localhost:5000/resize -o resized_image.jpg
# Convert an image to PNG
curl -fsSL -X POST -F "file=@path/to/image.jpg" -F "format=PNG" \
http://localhost:5000/convert -o converted_image.png
Error handling and input validation
For production, comprehensive testing is essential. Create a file named test_app.py
with the
following tests to validate the API's functionality:
import pytest
from app import app
import io
from PIL import Image
@pytest.fixture
def client():
app.config['TESTING'] = True
with app.test_client() as client:
yield client
def test_resize_endpoint(client):
# Create a test image in memory
img_io = io.BytesIO()
Image.new('RGB', (100, 100)).save(img_io, 'JPEG')
img_io.seek(0)
data = {
'file': (img_io, 'test.jpg'),
'width': '50',
'height': '50'
}
response = client.post('/resize', data=data)
assert response.status_code == 200
def test_invalid_file(client):
data = {
'file': (io.BytesIO(b'invalid'), 'test.txt')
}
response = client.post('/resize', data=data)
assert response.status_code == 400
Deploying the API to a server
For production deployment, use Gunicorn along with proper configuration. Create a Gunicorn
configuration file named gunicorn_config.py
:
import multiprocessing
workers = multiprocessing.cpu_count() * 2 + 1
bind = '127.0.0.1:8000'
keepalive = 120
errorlog = '/var/log/gunicorn/error.log'
accesslog = '/var/log/gunicorn/access.log'
worker_class = 'sync'
max_requests = 1000
max_requests_jitter = 50
timeout = 120
Then, create a systemd service file at /etc/systemd/system/flask-image-api.service
:
[Unit]
Description=Flask Image Processing API
After=network.target
[Service]
User=www-data
WorkingDirectory=/path/to/flask-image-api
Environment="PATH=/path/to/flask-image-api/venv/bin"
ExecStart=/path/to/flask-image-api/venv/bin/gunicorn -c gunicorn_config.py app:app
[Install]
WantedBy=multi-user.target
Enable and start the service:
sudo systemctl enable flask-image-api
sudo systemctl start flask-image-api
Conclusion and next steps
We have built a secure, efficient, and scalable image processing API using Python, Flask, and Pillow. The API incorporates essential features like file validation, error handling, rate limiting, and CORS support. To extend this solution, you might consider options such as image compression, user authentication, batch processing, cloud storage integration, or metadata extraction.
For a comprehensive solution with advanced file processing capabilities, consider exploring Transloadit, which offers a robust platform for handling and transforming files in your applications.