Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions fs_image_thumbnail/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
**This file is going to be generated by oca-gen-addon-readme.**

*Manual changes will be overwritten.*

Please provide content in the ``readme`` directory:

* **DESCRIPTION.rst** (required)
* INSTALL.rst (optional)
* CONFIGURE.rst (optional)
* **USAGE.rst** (optional, highly recommended)
* DEVELOP.rst (optional)
* ROADMAP.rst (optional)
* HISTORY.rst (optional, recommended)
* **CONTRIBUTORS.rst** (optional, highly recommended)
* CREDITS.rst (optional)

Content of this README will also be drawn from the addon manifest,
from keys such as name, authors, maintainers, development_status,
and license.

A good, one sentence summary in the manifest is also highly recommended.


Automatic changelog generation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

`HISTORY.rst` can be auto generated using `towncrier <https://pypi.org/project/towncrier>`_.

Just put towncrier compatible changelog fragments into `readme/newsfragments`
and the changelog file will be automatically generated and updated when a new fragment is added.

Please refer to `towncrier` documentation to know more.

NOTE: the changelog will be automatically generated when using `/ocabot merge $option`.
If you need to run it manually, refer to `OCA/maintainer-tools README <https://github.com/OCA/maintainer-tools>`_.
1 change: 1 addition & 0 deletions fs_image_thumbnail/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
23 changes: 23 additions & 0 deletions fs_image_thumbnail/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Copyright 2023 ACSONE SA/NV
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

{
"name": "Fs Image Thumbnail",
"summary": """
Generate and store thumbnail for images""",
"version": "16.0.1.0.0",
"license": "AGPL-3",
"author": "ACSONE SA/NV,Odoo Community Association (OCA)",
"website": "https://github.com/OCA/storage",
"depends": ["fs_image", "base_partition"],
"data": [
"views/ir_attachment.xml",
"security/fs_thumbnail.xml",
"views/fs_image_thumbnail_mixin.xml",
"views/fs_thumbnail.xml",
],
"demo": [],
"maintainers": ["lmignon"],
"development_status": "Alpha",
"external_dependencies": {"python": ["python_slugify"]},
}
3 changes: 3 additions & 0 deletions fs_image_thumbnail/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from . import fs_image_thumbnail_mixin
from . import fs_thumbnail
from . import ir_attachment
238 changes: 238 additions & 0 deletions fs_image_thumbnail/models/fs_image_thumbnail_mixin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
# Copyright 2023 ACSONE SA/NV
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from slugify import slugify

from odoo import _, api, fields, models
from odoo.exceptions import UserError

from odoo.addons.fs_image.fields import FSImage, FSImageValue


class FsImageThumbnailMixin(models.AbstractModel):
"""Mixin defining what is a thumbnail image and providing a
method to generate a thumbnail image from an image.

"""

_name = "fs.image.thumbnail.mixin"
_description = "Fs Image Thumbnail Mixin"

image = FSImage("Image", required=True)
original_image = FSImage("Original Image", compute="_compute_original_image")
size_x = fields.Integer("X size", required=True)
size_y = fields.Integer("Y size", required=True)
base_name = fields.Char(
"The base name of the thumbnail image (without extension)",
required=True,
help="The thumbnail image will be named as base_name "
"+ _ + size_x + _ + size_y + . + extension.\n"
"If not set, the base name will be the name of the original image."
"This base name is used to find all existing thumbnail of an image generated "
"for the same base name.",
)

attachment_id = fields.Many2one(
comodel_name="ir.attachment",
string="Attachment",
help="Attachment containing the original image",
required=True,
)
name = fields.Char(
compute="_compute_name",
store=True,
)
mimetype = fields.Char(
compute="_compute_mimetype",
store=True,
)

@api.depends("image")
def _compute_name(self):
for record in self:
record.name = record.image.name if record.image else None

@api.depends("image")
def _compute_mimetype(self):
for record in self:
record.mimetype = record.image.mimetype if record.image else None

@api.depends("attachment_id")
def _compute_original_image(self):
original_image_field = self._fields["original_image"]
for record in self:
value = None
if record.attachment_id:
value = original_image_field._convert_attachment_to_cache(
record.attachment_id
)
record.original_image = value

@api.model
def _resize(self, image: FSImage, size_x: int, size_y: int, fmt: str = "") -> bytes:
"""Resize the given image to the given size.

:param image: the image to resize
:param size_x: the new width of the image
:param size_y: the new height of the image
:param fmt: the output format of the image. Can be PNG, JPEG, GIF, or ICO.
Default to the format of the original image. BMP is converted to
PNG, other formats than those mentioned above are converted to JPEG.
:return: the resized image
"""
# image_process only accept PNG, JPEG, GIF, or ICO as output format
# in uppercase. Remove the dot if present and convert to uppercase.
fmt = fmt.upper().replace(".", "")
return image.image_process(size=(size_x, size_y), output_format=fmt)

@api.model
def _get_resize_format(self, image: FSImage) -> str:
"""Get the format to use to resize an image.

:return: the format to use to resize an image
"""
fmt = (
self.env["ir.config_parameter"]
.sudo()
.get_param("fs_image_thumbnail.resize_format")
)
return fmt or image.extension

@api.model
def _prepare_tumbnail(
self, image: FSImage, size_x: int, size_y: int, base_name: str
) -> dict:
"""Prepare the values to create a thumbnail image from the given image.

:param image: the image to resize
:param size_x: the new width of the image
:param size_y: the new height of the image
:param base_name: the base name of the thumbnail image (without extension)
:return: the values to create a thumbnail image
"""
fmt = self._get_resize_format(image)
extension = fmt
# Add a dot before the extension if needed and convert to lowercase.
extension = extension.lower()
if extension and not extension.startswith("."):
extension = "." + extension
new_image = FSImageValue(
value=self._resize(image, size_x, size_y, fmt),
name="%s_%s_%s%s" % (base_name, size_x, size_y, extension),
alt_text=image.alt_text,
)
return {
"image": new_image,
"size_x": size_x,
"size_y": size_y,
"base_name": base_name,
"attachment_id": image.attachment.id,
}

@api.model
def _slugify_base_name(self, base_name: str) -> str:
"""Slugify the given base name.

:param base_name: the base name to slugify
:return: the slugified base name
"""
return slugify(base_name) if base_name else base_name

@api.model
def _get_existing_thumbnail_domain(
self, *images: tuple[FSImageValue], base_name: str = ""
) -> list:
"""Get the domain to find existing thumbnail images from the given image.

:param images: a list of images we want to find existing thumbnails
:param base_name: the base name of the thumbnail image (without extension)
The base name must be set when multiple images are given.
:return: the domain to find existing thumbnail images
"""
attachment_ids = []
for image in images:
if image.attachment:
attachment_ids.append(image.attachment.id)
else:
raise UserError(
_(
"The image %(name)s must be attached to an attachment",
name=image.name,
)
)
base_name = self._get_slugified_base_name(*images, base_name=base_name)
return [
("attachment_id", "in", attachment_ids),
("base_name", "=", base_name),
]

@api.model
def get_thumbnails(
self, *images: tuple[FSImageValue], base_name: str = ""
) -> list["FsImageThumbnailMixin"]:
"""Get existing thumbnail images from the given image.

:param images: a list of images we want to find existing thumbnails
:param base_name: the base name of the thumbnail image (without extension)
The base name must be set when multiple images are given.
:return: a recordset of thumbnail images
"""
domain = self._get_existing_thumbnail_domain(*images, base_name=base_name)
return self.search(domain)

@api.model
def get_or_create_thumbnails(
self,
*images: tuple[FSImageValue],
sizes: list[tuple[int, int]],
base_name: str = ""
) -> list["FsImageThumbnailMixin"]:
"""Get or create a thumbnail images from the given image.

:param images: the list of images we want to get or create thumbnails
:param sizes: the list of sizes to use to resize the image
(list of tuple (size_x, size_y))
:param base_name: the base name of the thumbnail image (without extension)
The base name must be set when multiple images are given.
:return: a dictionary where the key is the original image and the value is
a recordset of thumbnail images
"""
base_name = self._get_slugified_base_name(*images, base_name=base_name)
thumbnails = self.get_thumbnails(*images, base_name=base_name)
thumbnails_by_attachment_id = thumbnails.partition("attachment_id")
ret = {}
for image in images:
thumbnails_by_size = {
(thumbnail.size_x, thumbnail.size_y): thumbnail
for thumbnail in thumbnails_by_attachment_id.get(image.attachment, [])
}
ids_to_return = []
for size_x, size_y in sizes:
thumbnail = thumbnails_by_size.get((size_x, size_y))
if not thumbnail:
values = self._prepare_tumbnail(image, size_x, size_y, base_name)
# no creation possible outside of this method -> sudo() is
# required since no access rights defined on create
thumbnail = self.sudo().create(values)
ids_to_return.append(thumbnail.id)
# return the thumbnails browsed in the same security context as the method
# caller
ret[image] = self.browse(ids_to_return)
return ret

@api.model
def _get_slugified_base_name(
self, *images: tuple[FSImageValue], base_name: str
) -> str:
"""Get the base name of the thumbnail image (without extension).

:param images: the list of images we want to get the base name
:return: the base name of the thumbnail image
"""
if not base_name:
if len(images) > 1:
raise UserError(
_("The base name must be set when multiple images are given")
)
base_name = images[0].name
return self._slugify_base_name(base_name)
11 changes: 11 additions & 0 deletions fs_image_thumbnail/models/fs_thumbnail.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Copyright 2023 ACSONE SA/NV
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from odoo import models


class FsThumbnail(models.Model):

_name = "fs.thumbnail"
_inherit = "fs.image.thumbnail.mixin"
_description = "Image Thumbnail"
16 changes: 16 additions & 0 deletions fs_image_thumbnail/models/ir_attachment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Copyright 2023 ACSONE SA/NV
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from odoo import fields, models


class IrAttachment(models.Model):

_inherit = "ir.attachment"

thumbnail_ids = fields.One2many(
comodel_name="fs.thumbnail",
inverse_name="attachment_id",
string="Thumbnails",
readonly=True,
)
17 changes: 17 additions & 0 deletions fs_image_thumbnail/readme/CONTEXT.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
In some specific cases you may need to generate and store thumbnails of images in Odoo.
This is the case for example when you want to provide image in specific sizes for a website
or a mobile application.

This module provides a generic way to generate thumbnails of images and store them in a
specific filesystem storage. Indeed, you could need to store the thumbnails in a different
storage than the original image (eg: store the thumbnails in a CDN) to make sure the
thumbnails are served quickly when requested by an external application and to
avoid to expose the original image storage.

This module uses the `fs_image <https://github.com/oca/storage/blob/16.0/fs_image/README.rst>`_
module to store the thumbnails in a filesystem storage.

The `shopinvader_product_image <https://github.com/shopinvader/odoo-shopinvader/
blob/16.0/shopinvader_product_image>`_ addon uses this module to generate and
store the thumbnails of the images of the products and categories to be accessible
by the website.
1 change: 1 addition & 0 deletions fs_image_thumbnail/readme/CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Laurent Mignon <laurent.mignon@acsone.eu> (https://acsone.eu)
3 changes: 3 additions & 0 deletions fs_image_thumbnail/readme/CREDITS.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
The development of this module has been financially supported by:

* `Alcyon Belux <https://www.alcyonbelux.be/>`_
4 changes: 4 additions & 0 deletions fs_image_thumbnail/readme/DESCRIPTION.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
This module extends the **fs_image** addon to support the creation and the storage of
thumbnails for images. This module is a **technical module** and is not
meant to be installed by end-users. It only provides a mixin to be used
by other modules and a model to store the thumbnails.
Loading