Skip to content

Odoo Image Handling and Optimization: Resize, Compress, and Serve

DeployMonkey Team · March 23, 2026 10 min read

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.

StorageProsCons
Binary field (DB)Simple, transactional, backed up with DBBloats database, slow for large files
Attachment (filestore)Efficient, filesystem-cached, CDN-friendlySeparate 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_1920

When 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 page
  • image_512: List/grid view cards
  • image_256: Thumbnails
  • image_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 = False

image_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.mixin for models with product/profile images — it handles resizing automatically
  • Set attachment=True on 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