How Odoo Stores Images
Odoo stores images in two ways: as Binary fields directly in the database or as ir.attachment records on the filesystem. Both approaches have tradeoffs that affect performance and storage.
| Storage | Pros | Cons |
|---|---|---|
| Binary field (DB) | Simple, transactional, backed up with DB | Bloats database, slow for large files |
| Attachment (filestore) | Efficient, filesystem-cached, CDN-friendly | Separate backup needed, migration complexity |
image.mixin: Automatic Resizing
Odoo's image.mixin provides automatic image resizing into multiple sizes:
class ProductTemplate(models.Model):
_name = 'product.template'
_inherit = ['image.mixin'] # adds image fields
# image.mixin provides:
# image_1920 = Binary (max 1920px) — the original
# image_1024 = Binary (max 1024px) — computed from image_1920
# image_512 = Binary (max 512px) — computed from image_1920
# image_256 = Binary (max 256px) — computed from image_1920
# image_128 = Binary (max 128px) — computed from image_1920When you upload an image to image_1920, all smaller versions are automatically generated. Use the appropriate size for each context:
image_1920: Full-size product pageimage_512: List/grid view cardsimage_256: Thumbnailsimage_128: Small icons, avatars
Custom Image Fields
For models that do not need full image.mixin, use Binary fields with max_width/max_height:
class BlogPost(models.Model):
_name = 'blog.post'
cover_image = fields.Binary(
string='Cover Image',
attachment=True, # store as attachment, not in DB
)
cover_image_thumbnail = fields.Binary(
string='Cover Thumbnail',
compute='_compute_thumbnail',
store=True,
attachment=True,
)
@api.depends('cover_image')
def _compute_thumbnail(self):
for record in self:
if record.cover_image:
record.cover_image_thumbnail = image_process(
record.cover_image, size=(256, 256)
)
else:
record.cover_image_thumbnail = Falseimage_process Utility
Odoo provides image_process for image manipulation:
from odoo.tools.image import image_process
# Resize to max 800x800 (maintains aspect ratio)
resized = image_process(image_base64, size=(800, 800))
# Resize and crop to exact dimensions
cropped = image_process(image_base64, size=(300, 300), crop='center')
# Convert to specific format
webp = image_process(image_base64, output_format='WEBP')
# Resize with quality control
compressed = image_process(image_base64, size=(1024, 1024), quality=80)attachment=True for Efficient Storage
Setting attachment=True on Binary fields stores the content in the filestore instead of the database:
class Document(models.Model):
_name = 'my.document'
# Stored in database — bloats DB
data_db = fields.Binary()
# Stored in filestore — efficient
data_file = fields.Binary(attachment=True)The filestore path is typically /var/lib/odoo/filestore/database_name/. Files are stored by SHA hash, so identical files are stored once (deduplication).
Serving Images Efficiently
Web Image Controller
Odoo serves images via /web/image:
<!-- Serve specific size -->
<img t-attf-src="/web/image/product.template/#{product.id}/image_512" />
<!-- With resize parameters -->
<img t-attf-src="/web/image/product.template/#{product.id}/image_1920/300x300" />
<!-- For attachments -->
<img t-attf-src="/web/image/#{attachment.id}" />Cache Headers
Odoo's image controller sets cache headers based on the image's write date. Images get long cache lifetimes when the URL includes a unique hash:
<img t-attf-src="/web/image/product.template/#{product.id}/image_256?unique=#{product.write_date}" />Reducing Database Bloat from Images
Large Binary fields without attachment=True bloat the database significantly. For existing modules:
# Migrate images from DB to filestore
def _post_init_hook(env):
products = env['product.template'].search([])
for product in products:
if product.image_1920:
# Force rewrite — triggers attachment storage
product.write({'image_1920': product.image_1920})Image Upload Validation
from odoo.tools.image import image_process, base64_to_image
from PIL import Image
import base64
def _validate_image(self, image_base64):
if not image_base64:
return image_base64
try:
image = base64_to_image(image_base64)
except Exception:
raise UserError('Invalid image file.')
# Check dimensions
if image.size[0] > 4096 or image.size[1] > 4096:
raise UserError('Image is too large. Maximum 4096x4096 pixels.')
# Check file size (base64 is ~33% larger than binary)
size_mb = len(image_base64) * 3 / 4 / (1024 * 1024)
if size_mb > 10:
raise UserError('Image file is too large. Maximum 10 MB.')
# Resize if needed
return image_process(image_base64, size=(1920, 1920), quality=85)Lazy Loading Images on Website
<!-- Lazy load images for better page performance -->
<img t-attf-src="/web/image/product.template/#{product.id}/image_512"
loading="lazy"
alt="Product image"
class="img-fluid"/>Handling Missing Images
<!-- Fallback for missing images -->
<t t-if="product.image_256">
<img t-att-src="image_data_uri(product.image_256)" class="img-fluid"/>
</t>
<t t-else="">
<div class="bg-light d-flex align-items-center justify-content-center"
style="width:100%;height:200px;">
<i class="fa fa-image fa-3x text-muted"/>
</div>
</t>Best Practices
- Use
image.mixinfor models with product/profile images — it handles resizing automatically - Set
attachment=Trueon Binary fields to avoid database bloat - Serve the smallest image size appropriate for the context (128px for icons, 512px for cards)
- Add
loading="lazy"to images below the fold for better page performance - Validate uploaded images: check dimensions, file size, and format
- Use
image_process()to resize and compress before storage - Include write_date in image URLs for proper cache busting