diff --git a/fs_image_thumbnail/README.rst b/fs_image_thumbnail/README.rst new file mode 100644 index 0000000000..8e8c1f692c --- /dev/null +++ b/fs_image_thumbnail/README.rst @@ -0,0 +1,199 @@ +================== +Fs Image Thumbnail +================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:1d1af8339aa4aee943dad48c165b12fca74f0dd16f574469fb90c58c8c073a47 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png + :target: https://odoo-community.org/page/development-status + :alt: Alpha +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fstorage-lightgray.png?logo=github + :target: https://github.com/OCA/storage/tree/18.0/fs_image_thumbnail + :alt: OCA/storage +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/storage-18-0/storage-18-0-fs_image_thumbnail + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/storage&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +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. + +.. IMPORTANT:: + This is an alpha version, the data model and design can change at any time without warning. + Only for development or testing purpose, do not use in production. + `More details on development status `_ + +**Table of contents** + +.. contents:: + :local: + +Use Cases / Context +=================== + +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 `__ +module to store the thumbnails in a filesystem storage. + +The +`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. + +Usage +===== + +This addon provides a convenient way to get and create if not exists +image thumbnails. All the logic is implemented by the abstract model +fs.image.thumbnail.mixin. The main method is get_or_create_thumbnails +which accepts a *FSImageValue* instance, a list of thumbnail sizes and a +base name. + +When the method is called, it will check if the thumbnail exists for the +given sizes and base name. If not, it will create it. + +The fs.thumbnail model provided by this addon is a concrete +implementation of the abstract model fs.image.thumbnail.mixin. The +motivation to implement all the logic in an abstract model is to allow +developers to create their own thumbnail models. This could be useful if +you want to store the thumbnails in a different storage since you can +specify the storage to use by model on the fs.storage form view. + +Creating / retrieving thumbnails is as simple as: + +.. code:: python + + from odoo.addons.fs_image.fields import FSImageValue + + # create an attachment with a image file + attachment = self.env['ir.attachment'].create({ + 'name': 'test', + 'datas': base64.b64encode(open('test.png', 'rb').read()), + 'datas_fname': 'test.png', + }) + + # create a FSImageValue instance for the attachment + image_value = FSImageValue(attachment) + + # get or create the thumbnails + thumbnails = self.env['fs.thumbnail'].get_or_create_thumbnails(image_value, sizes=[(800,600), (400, 200)], base_name='my base name') + +If you've a model with a *FSImage* field, the call to +get_or_create_thumbnails is even simpler: + +.. code:: python + + from odoo import models + from odoo.addons.fs_image.fields import FSImage + + class MyModel(models.Model): + _name = 'my.model' + + image = FSImage('Image') + + my_record = cls.env['my.model'].create({ + 'image': open('test.png', 'rb'), + }) + + # get or create the thumbnails + thumbnails = record.image.get_or_create_thumbnails(my_record.image, + sizes=[(800,600), (400, 200)], base_name='my base name') + +Changelog +========= + +16.0.1.0.1 (2023-10-04) +----------------------- + +**Bugfixes** + +- The call to the method *get_or_create_thumbnails* on the + *fs.image.thumbnail.mixin* class returns now an ordered dictionary + where the key is the original image and the value is a recordset of + thumbnail images. The order of the dict is the order of the images + passed to the method. This ensures that when you process the result + of the method you can be sure that the order of the images is the + same as the order of the images passed to the method. + (`#282 `__) + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* ACSONE SA/NV + +Contributors +------------ + +- Laurent Mignon (https://acsone.eu) +- Do Anh Duy (https://trobz.com) + +Other credits +------------- + +The development of this module has been financially supported by: + +- `Alcyon Belux `__ + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-lmignon| image:: https://github.com/lmignon.png?size=40px + :target: https://github.com/lmignon + :alt: lmignon + +Current `maintainer `__: + +|maintainer-lmignon| + +This module is part of the `OCA/storage `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/fs_image_thumbnail/__init__.py b/fs_image_thumbnail/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /dev/null +++ b/fs_image_thumbnail/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/fs_image_thumbnail/__manifest__.py b/fs_image_thumbnail/__manifest__.py new file mode 100644 index 0000000000..c1bbd6e536 --- /dev/null +++ b/fs_image_thumbnail/__manifest__.py @@ -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": "18.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"]}, +} diff --git a/fs_image_thumbnail/i18n/es.po b/fs_image_thumbnail/i18n/es.po new file mode 100644 index 0000000000..fe8e152507 --- /dev/null +++ b/fs_image_thumbnail/i18n/es.po @@ -0,0 +1,180 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_image_thumbnail +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2023-10-29 00:15+0000\n" +"Last-Translator: Ivorra78 \n" +"Language-Team: none\n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.17\n" + +#. module: fs_image_thumbnail +#: model:ir.model,name:fs_image_thumbnail.model_ir_attachment +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__attachment_id +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__attachment_id +#: model_terms:ir.ui.view,arch_db:fs_image_thumbnail.fs_image_thumbnail_mixin_search_view +msgid "Attachment" +msgstr "Archivo Adjunto" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,help:fs_image_thumbnail.field_fs_image_thumbnail_mixin__attachment_id +#: model:ir.model.fields,help:fs_image_thumbnail.field_fs_thumbnail__attachment_id +msgid "Attachment containing the original image" +msgstr "Archivo adjunto con la imagen original" + +#. module: fs_image_thumbnail +#: model_terms:ir.ui.view,arch_db:fs_image_thumbnail.fs_image_thumbnail_mixin_search_view +msgid "Base Name" +msgstr "Nombre de la Base" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__create_uid +#: model_terms:ir.ui.view,arch_db:fs_image_thumbnail.fs_thumbnail_search_view +msgid "Created by" +msgstr "Creado por" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__create_date +msgid "Created on" +msgstr "Creado el" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__display_name +msgid "Display Name" +msgstr "Mostrar Nombre" + +#. module: fs_image_thumbnail +#: model:ir.model,name:fs_image_thumbnail.model_fs_image_thumbnail_mixin +msgid "Fs Image Thumbnail Mixin" +msgstr "Mezcla de Miniaturas de imágenes Fs" + +#. module: fs_image_thumbnail +#: model:ir.ui.menu,name:fs_image_thumbnail.fs_thumbnail_menu +msgid "Fs Image Thumbnails" +msgstr "Miniaturas de imágenes Fs" + +#. module: fs_image_thumbnail +#: model:ir.actions.act_window,name:fs_image_thumbnail.fs_thumbnail_act_window +msgid "Fs Thumbnail" +msgstr "Miniatura Fs" + +#. module: fs_image_thumbnail +#: model_terms:ir.ui.view,arch_db:fs_image_thumbnail.fs_image_thumbnail_mixin_search_view +msgid "Group By" +msgstr "Agrupado Por" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__id +msgid "ID" +msgstr "ID (identificación)" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__image +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__image +msgid "Image" +msgstr "Imagen" + +#. module: fs_image_thumbnail +#: model:ir.model,name:fs_image_thumbnail.model_fs_thumbnail +msgid "Image Thumbnail" +msgstr "Imagen en Miniatura" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__write_uid +msgid "Last Updated by" +msgstr "Última Actualización por" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__write_date +msgid "Last Updated on" +msgstr "Última Actualización el" + +#. module: fs_image_thumbnail +#: model_terms:ir.ui.view,arch_db:fs_image_thumbnail.fs_image_thumbnail_mixin_search_view +msgid "MimeType" +msgstr "Tipo Mimo" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__mimetype +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__mimetype +msgid "Mimetype" +msgstr "Tipo Mimo" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__name +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__name +#: model_terms:ir.ui.view,arch_db:fs_image_thumbnail.fs_image_thumbnail_mixin_search_view +msgid "Name" +msgstr "Nombre" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__original_image +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__original_image +msgid "Original Image" +msgstr "Imagen Original" + +#. module: fs_image_thumbnail +#. odoo-python +#: code:addons/fs_image_thumbnail/models/fs_image_thumbnail_mixin.py:0 +#, python-format +msgid "The base name must be set when multiple images are given" +msgstr "El nombre base debe establecerse cuando se dan varias imágenes" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__base_name +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__base_name +msgid "The base name of the thumbnail image (without extension)" +msgstr "El nombre base de la imagen en miniatura (sin extensión)" + +#. module: fs_image_thumbnail +#. odoo-python +#: code:addons/fs_image_thumbnail/models/fs_image_thumbnail_mixin.py:0 +#, python-format +msgid "The image %(name)s must be attached to an attachment" +msgstr "La imagen %(name)s debe adjuntarse a un archivo adjunto" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,help:fs_image_thumbnail.field_fs_image_thumbnail_mixin__base_name +#: model:ir.model.fields,help:fs_image_thumbnail.field_fs_thumbnail__base_name +msgid "" +"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." +msgstr "" +"La imagen en miniatura se denominará como nombre_base + _ + tamaño_x + _ + " +"tamaño_y + . + extensión.\n" +"Si no se establece, el nombre base será el nombre de la imagen original. " +"Este nombre base se utiliza para encontrar todas las miniaturas existentes " +"de una imagen generada para el mismo nombre base." + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_ir_attachment__thumbnail_ids +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_product_document__thumbnail_ids +#: model_terms:ir.ui.view,arch_db:fs_image_thumbnail.ir_attachment_form_view +msgid "Thumbnails" +msgstr "Miniaturas" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__size_x +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__size_x +msgid "X size" +msgstr "Tamaño X" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__size_y +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__size_y +msgid "Y size" +msgstr "Talla Y" + +#~ msgid "Last Modified on" +#~ msgstr "Última Modificación el" diff --git a/fs_image_thumbnail/i18n/fs_image_thumbnail.pot b/fs_image_thumbnail/i18n/fs_image_thumbnail.pot new file mode 100644 index 0000000000..3ed95f232a --- /dev/null +++ b/fs_image_thumbnail/i18n/fs_image_thumbnail.pot @@ -0,0 +1,166 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_image_thumbnail +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: fs_image_thumbnail +#: model:ir.model,name:fs_image_thumbnail.model_ir_attachment +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__attachment_id +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__attachment_id +#: model_terms:ir.ui.view,arch_db:fs_image_thumbnail.fs_image_thumbnail_mixin_search_view +msgid "Attachment" +msgstr "" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,help:fs_image_thumbnail.field_fs_image_thumbnail_mixin__attachment_id +#: model:ir.model.fields,help:fs_image_thumbnail.field_fs_thumbnail__attachment_id +msgid "Attachment containing the original image" +msgstr "" + +#. module: fs_image_thumbnail +#: model_terms:ir.ui.view,arch_db:fs_image_thumbnail.fs_image_thumbnail_mixin_search_view +msgid "Base Name" +msgstr "" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__create_uid +#: model_terms:ir.ui.view,arch_db:fs_image_thumbnail.fs_thumbnail_search_view +msgid "Created by" +msgstr "" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__create_date +msgid "Created on" +msgstr "" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__display_name +msgid "Display Name" +msgstr "" + +#. module: fs_image_thumbnail +#: model:ir.model,name:fs_image_thumbnail.model_fs_image_thumbnail_mixin +msgid "Fs Image Thumbnail Mixin" +msgstr "" + +#. module: fs_image_thumbnail +#: model:ir.ui.menu,name:fs_image_thumbnail.fs_thumbnail_menu +msgid "Fs Image Thumbnails" +msgstr "" + +#. module: fs_image_thumbnail +#: model:ir.actions.act_window,name:fs_image_thumbnail.fs_thumbnail_act_window +msgid "Fs Thumbnail" +msgstr "" + +#. module: fs_image_thumbnail +#: model_terms:ir.ui.view,arch_db:fs_image_thumbnail.fs_image_thumbnail_mixin_search_view +msgid "Group By" +msgstr "" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__id +msgid "ID" +msgstr "" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__image +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__image +msgid "Image" +msgstr "" + +#. module: fs_image_thumbnail +#: model:ir.model,name:fs_image_thumbnail.model_fs_thumbnail +msgid "Image Thumbnail" +msgstr "" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__write_date +msgid "Last Updated on" +msgstr "" + +#. module: fs_image_thumbnail +#: model_terms:ir.ui.view,arch_db:fs_image_thumbnail.fs_image_thumbnail_mixin_search_view +msgid "MimeType" +msgstr "" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__mimetype +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__mimetype +msgid "Mimetype" +msgstr "" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__name +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__name +#: model_terms:ir.ui.view,arch_db:fs_image_thumbnail.fs_image_thumbnail_mixin_search_view +msgid "Name" +msgstr "" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__original_image +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__original_image +msgid "Original Image" +msgstr "" + +#. module: fs_image_thumbnail +#. odoo-python +#: code:addons/fs_image_thumbnail/models/fs_image_thumbnail_mixin.py:0 +#, python-format +msgid "The base name must be set when multiple images are given" +msgstr "" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__base_name +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__base_name +msgid "The base name of the thumbnail image (without extension)" +msgstr "" + +#. module: fs_image_thumbnail +#. odoo-python +#: code:addons/fs_image_thumbnail/models/fs_image_thumbnail_mixin.py:0 +#, python-format +msgid "The image %(name)s must be attached to an attachment" +msgstr "" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,help:fs_image_thumbnail.field_fs_image_thumbnail_mixin__base_name +#: model:ir.model.fields,help:fs_image_thumbnail.field_fs_thumbnail__base_name +msgid "" +"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." +msgstr "" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_ir_attachment__thumbnail_ids +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_product_document__thumbnail_ids +#: model_terms:ir.ui.view,arch_db:fs_image_thumbnail.ir_attachment_form_view +msgid "Thumbnails" +msgstr "" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__size_x +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__size_x +msgid "X size" +msgstr "" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__size_y +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__size_y +msgid "Y size" +msgstr "" diff --git a/fs_image_thumbnail/i18n/it.po b/fs_image_thumbnail/i18n/it.po new file mode 100644 index 0000000000..267c55e745 --- /dev/null +++ b/fs_image_thumbnail/i18n/it.po @@ -0,0 +1,180 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_image_thumbnail +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2023-12-12 11:33+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.17\n" + +#. module: fs_image_thumbnail +#: model:ir.model,name:fs_image_thumbnail.model_ir_attachment +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__attachment_id +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__attachment_id +#: model_terms:ir.ui.view,arch_db:fs_image_thumbnail.fs_image_thumbnail_mixin_search_view +msgid "Attachment" +msgstr "Allegato" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,help:fs_image_thumbnail.field_fs_image_thumbnail_mixin__attachment_id +#: model:ir.model.fields,help:fs_image_thumbnail.field_fs_thumbnail__attachment_id +msgid "Attachment containing the original image" +msgstr "Allegato contenente l'immagine originale" + +#. module: fs_image_thumbnail +#: model_terms:ir.ui.view,arch_db:fs_image_thumbnail.fs_image_thumbnail_mixin_search_view +msgid "Base Name" +msgstr "Nome base" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__create_uid +#: model_terms:ir.ui.view,arch_db:fs_image_thumbnail.fs_thumbnail_search_view +msgid "Created by" +msgstr "Creato da" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__create_date +msgid "Created on" +msgstr "Creato il" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__display_name +msgid "Display Name" +msgstr "Nome visualizzato" + +#. module: fs_image_thumbnail +#: model:ir.model,name:fs_image_thumbnail.model_fs_image_thumbnail_mixin +msgid "Fs Image Thumbnail Mixin" +msgstr "Mixin anteprima immagine FS" + +#. module: fs_image_thumbnail +#: model:ir.ui.menu,name:fs_image_thumbnail.fs_thumbnail_menu +msgid "Fs Image Thumbnails" +msgstr "Anteprima immagine FS" + +#. module: fs_image_thumbnail +#: model:ir.actions.act_window,name:fs_image_thumbnail.fs_thumbnail_act_window +msgid "Fs Thumbnail" +msgstr "Anteprima FS" + +#. module: fs_image_thumbnail +#: model_terms:ir.ui.view,arch_db:fs_image_thumbnail.fs_image_thumbnail_mixin_search_view +msgid "Group By" +msgstr "Raggruppa per" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__id +msgid "ID" +msgstr "ID" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__image +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__image +msgid "Image" +msgstr "Immagine" + +#. module: fs_image_thumbnail +#: model:ir.model,name:fs_image_thumbnail.model_fs_thumbnail +msgid "Image Thumbnail" +msgstr "Anteprima immagine" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__write_uid +msgid "Last Updated by" +msgstr "Ultimo aggiornamento di" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__write_date +msgid "Last Updated on" +msgstr "Ultimo aggiornamento il" + +#. module: fs_image_thumbnail +#: model_terms:ir.ui.view,arch_db:fs_image_thumbnail.fs_image_thumbnail_mixin_search_view +msgid "MimeType" +msgstr "Tipo MIME" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__mimetype +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__mimetype +msgid "Mimetype" +msgstr "Tipo MIME" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__name +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__name +#: model_terms:ir.ui.view,arch_db:fs_image_thumbnail.fs_image_thumbnail_mixin_search_view +msgid "Name" +msgstr "Nome" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__original_image +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__original_image +msgid "Original Image" +msgstr "Immagine originale" + +#. module: fs_image_thumbnail +#. odoo-python +#: code:addons/fs_image_thumbnail/models/fs_image_thumbnail_mixin.py:0 +#, python-format +msgid "The base name must be set when multiple images are given" +msgstr "Il nome base deve essere impostato qando vengono fornite più immagini" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__base_name +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__base_name +msgid "The base name of the thumbnail image (without extension)" +msgstr "Il nome base dell'immagIne anteprima (senza estensione)" + +#. module: fs_image_thumbnail +#. odoo-python +#: code:addons/fs_image_thumbnail/models/fs_image_thumbnail_mixin.py:0 +#, python-format +msgid "The image %(name)s must be attached to an attachment" +msgstr "L'immagine %(name)s deve essere collegata ad una allegato" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,help:fs_image_thumbnail.field_fs_image_thumbnail_mixin__base_name +#: model:ir.model.fields,help:fs_image_thumbnail.field_fs_thumbnail__base_name +msgid "" +"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." +msgstr "" +"L'immagine anteprima verrà denominata come nome_bae+_+dimensione_x" +"+dimensione_y+.+estensione.\n" +"Se non impostato, il nome base sarà il nome dell'immagine originale. Questo " +"nome base è utilizzato per trovare tutte le anteprime di una immagine " +"generate per lo stesso nome base." + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_ir_attachment__thumbnail_ids +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_product_document__thumbnail_ids +#: model_terms:ir.ui.view,arch_db:fs_image_thumbnail.ir_attachment_form_view +msgid "Thumbnails" +msgstr "Anteprime" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__size_x +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__size_x +msgid "X size" +msgstr "Dimensione X" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__size_y +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__size_y +msgid "Y size" +msgstr "Dimensione Y" + +#~ msgid "Last Modified on" +#~ msgstr "Ultima modifica il" diff --git a/fs_image_thumbnail/models/__init__.py b/fs_image_thumbnail/models/__init__.py new file mode 100644 index 0000000000..0ef2d28a1d --- /dev/null +++ b/fs_image_thumbnail/models/__init__.py @@ -0,0 +1,3 @@ +from . import fs_image_thumbnail_mixin +from . import fs_thumbnail +from . import ir_attachment diff --git a/fs_image_thumbnail/models/fs_image_thumbnail_mixin.py b/fs_image_thumbnail/models/fs_image_thumbnail_mixin.py new file mode 100644 index 0000000000..30aaaa1aea --- /dev/null +++ b/fs_image_thumbnail/models/fs_image_thumbnail_mixin.py @@ -0,0 +1,244 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from collections import OrderedDict + +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, + ondelete="cascade", + ) + 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_thumbnail( + 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=f"{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( + self.env._( + "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 = "", + ) -> OrderedDict[FSImageValue, 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: an ordered dictionary where the key is the original image and + the value is a recordset of thumbnail images. The order of the dict + is the order of the images passed to the method. + """ + 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 = OrderedDict[FSImageValue, list["FsImageThumbnailMixin"]]() + 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_thumbnail(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( + self.env._( + "The base name must be set when multiple images are given" + ) + ) + base_name = images[0].name + return self._slugify_base_name(base_name) diff --git a/fs_image_thumbnail/models/fs_thumbnail.py b/fs_image_thumbnail/models/fs_thumbnail.py new file mode 100644 index 0000000000..462fa46e2f --- /dev/null +++ b/fs_image_thumbnail/models/fs_thumbnail.py @@ -0,0 +1,10 @@ +# 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" diff --git a/fs_image_thumbnail/models/ir_attachment.py b/fs_image_thumbnail/models/ir_attachment.py new file mode 100644 index 0000000000..972efd91ed --- /dev/null +++ b/fs_image_thumbnail/models/ir_attachment.py @@ -0,0 +1,14 @@ +# 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", + ) diff --git a/fs_image_thumbnail/pyproject.toml b/fs_image_thumbnail/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/fs_image_thumbnail/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/fs_image_thumbnail/readme/CONTEXT.md b/fs_image_thumbnail/readme/CONTEXT.md new file mode 100644 index 0000000000..3ffc22bee9 --- /dev/null +++ b/fs_image_thumbnail/readme/CONTEXT.md @@ -0,0 +1,18 @@ +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. diff --git a/fs_image_thumbnail/readme/CONTRIBUTORS.md b/fs_image_thumbnail/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..6e58b1e4ec --- /dev/null +++ b/fs_image_thumbnail/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- Laurent Mignon \<\> () +- Do Anh Duy \<\> () diff --git a/fs_image_thumbnail/readme/CREDITS.md b/fs_image_thumbnail/readme/CREDITS.md new file mode 100644 index 0000000000..77743cd65f --- /dev/null +++ b/fs_image_thumbnail/readme/CREDITS.md @@ -0,0 +1,3 @@ +The development of this module has been financially supported by: + +- [Alcyon Belux](https://www.alcyonbelux.be/) diff --git a/fs_image_thumbnail/readme/DESCRIPTION.md b/fs_image_thumbnail/readme/DESCRIPTION.md new file mode 100644 index 0000000000..39466d487b --- /dev/null +++ b/fs_image_thumbnail/readme/DESCRIPTION.md @@ -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. diff --git a/fs_image_thumbnail/readme/HISTORY.md b/fs_image_thumbnail/readme/HISTORY.md new file mode 100644 index 0000000000..23dc6efaf0 --- /dev/null +++ b/fs_image_thumbnail/readme/HISTORY.md @@ -0,0 +1,12 @@ +## 16.0.1.0.1 (2023-10-04) + +**Bugfixes** + +- The call to the method *get_or_create_thumbnails* on the + *fs.image.thumbnail.mixin* class returns now an ordered dictionary + where the key is the original image and the value is a recordset of + thumbnail images. The order of the dict is the order of the images + passed to the method. This ensures that when you process the result of + the method you can be sure that the order of the images is the same as + the order of the images passed to the method. + ([\#282](https://github.com/OCA/storage/issues/282)) diff --git a/fs_image_thumbnail/readme/USAGE.md b/fs_image_thumbnail/readme/USAGE.md new file mode 100644 index 0000000000..2c86fcf005 --- /dev/null +++ b/fs_image_thumbnail/readme/USAGE.md @@ -0,0 +1,55 @@ +This addon provides a convenient way to get and create if not exists +image thumbnails. All the logic is implemented by the abstract model +fs.image.thumbnail.mixin. The main method is get_or_create_thumbnails +which accepts a *FSImageValue* instance, a list of thumbnail sizes and a +base name. + +When the method is called, it will check if the thumbnail exists for the +given sizes and base name. If not, it will create it. + +The fs.thumbnail model provided by this addon is a concrete +implementation of the abstract model fs.image.thumbnail.mixin. The +motivation to implement all the logic in an abstract model is to allow +developers to create their own thumbnail models. This could be useful if +you want to store the thumbnails in a different storage since you can +specify the storage to use by model on the fs.storage form view. + +Creating / retrieving thumbnails is as simple as: + +``` python +from odoo.addons.fs_image.fields import FSImageValue + +# create an attachment with a image file +attachment = self.env['ir.attachment'].create({ + 'name': 'test', + 'datas': base64.b64encode(open('test.png', 'rb').read()), + 'datas_fname': 'test.png', +}) + +# create a FSImageValue instance for the attachment +image_value = FSImageValue(attachment) + +# get or create the thumbnails +thumbnails = self.env['fs.thumbnail'].get_or_create_thumbnails(image_value, sizes=[(800,600), (400, 200)], base_name='my base name') +``` + +If you've a model with a *FSImage* field, the call to +get_or_create_thumbnails is even simpler: + +``` python +from odoo import models +from odoo.addons.fs_image.fields import FSImage + +class MyModel(models.Model): + _name = 'my.model' + + image = FSImage('Image') + +my_record = cls.env['my.model'].create({ + 'image': open('test.png', 'rb'), +}) + +# get or create the thumbnails +thumbnails = record.image.get_or_create_thumbnails(my_record.image, + sizes=[(800,600), (400, 200)], base_name='my base name') +``` diff --git a/fs_image_thumbnail/readme/newsfragments/.gitignore b/fs_image_thumbnail/readme/newsfragments/.gitignore new file mode 100644 index 0000000000..e69de29bb2 diff --git a/fs_image_thumbnail/security/fs_thumbnail.xml b/fs_image_thumbnail/security/fs_thumbnail.xml new file mode 100644 index 0000000000..dc10b5852f --- /dev/null +++ b/fs_image_thumbnail/security/fs_thumbnail.xml @@ -0,0 +1,14 @@ + + + + + fs.thumbnail access read + + + + + + + + diff --git a/fs_image_thumbnail/static/description/icon.png b/fs_image_thumbnail/static/description/icon.png new file mode 100644 index 0000000000..3a0328b516 Binary files /dev/null and b/fs_image_thumbnail/static/description/icon.png differ diff --git a/fs_image_thumbnail/static/description/index.html b/fs_image_thumbnail/static/description/index.html new file mode 100644 index 0000000000..1c7c53f89c --- /dev/null +++ b/fs_image_thumbnail/static/description/index.html @@ -0,0 +1,537 @@ + + + + + +Fs Image Thumbnail + + + +
+

Fs Image Thumbnail

+ + +

Alpha License: AGPL-3 OCA/storage Translate me on Weblate Try me on Runboat

+

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.

+
+

Important

+

This is an alpha version, the data model and design can change at any time without warning. +Only for development or testing purpose, do not use in production. +More details on development status

+
+

Table of contents

+ +
+

Use Cases / Context

+

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 +module to store the thumbnails in a filesystem storage.

+

The +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.

+
+
+

Usage

+

This addon provides a convenient way to get and create if not exists +image thumbnails. All the logic is implemented by the abstract model +fs.image.thumbnail.mixin. The main method is get_or_create_thumbnails +which accepts a FSImageValue instance, a list of thumbnail sizes and a +base name.

+

When the method is called, it will check if the thumbnail exists for the +given sizes and base name. If not, it will create it.

+

The fs.thumbnail model provided by this addon is a concrete +implementation of the abstract model fs.image.thumbnail.mixin. The +motivation to implement all the logic in an abstract model is to allow +developers to create their own thumbnail models. This could be useful if +you want to store the thumbnails in a different storage since you can +specify the storage to use by model on the fs.storage form view.

+

Creating / retrieving thumbnails is as simple as:

+
+from odoo.addons.fs_image.fields import FSImageValue
+
+# create an attachment with a image file
+attachment = self.env['ir.attachment'].create({
+    'name': 'test',
+    'datas': base64.b64encode(open('test.png', 'rb').read()),
+    'datas_fname': 'test.png',
+})
+
+# create a FSImageValue instance for the attachment
+image_value = FSImageValue(attachment)
+
+# get or create the thumbnails
+thumbnails = self.env['fs.thumbnail'].get_or_create_thumbnails(image_value, sizes=[(800,600), (400, 200)], base_name='my base name')
+
+

If you’ve a model with a FSImage field, the call to +get_or_create_thumbnails is even simpler:

+
+from odoo import models
+from odoo.addons.fs_image.fields import FSImage
+
+class MyModel(models.Model):
+    _name = 'my.model'
+
+    image = FSImage('Image')
+
+my_record = cls.env['my.model'].create({
+    'image': open('test.png', 'rb'),
+})
+
+# get or create the thumbnails
+thumbnails = record.image.get_or_create_thumbnails(my_record.image,
+    sizes=[(800,600), (400, 200)], base_name='my base name')
+
+
+
+

Changelog

+
+

16.0.1.0.1 (2023-10-04)

+

Bugfixes

+
    +
  • The call to the method get_or_create_thumbnails on the +fs.image.thumbnail.mixin class returns now an ordered dictionary +where the key is the original image and the value is a recordset of +thumbnail images. The order of the dict is the order of the images +passed to the method. This ensures that when you process the result +of the method you can be sure that the order of the images is the +same as the order of the images passed to the method. +(#282)
  • +
+
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • ACSONE SA/NV
  • +
+
+ +
+

Other credits

+

The development of this module has been financially supported by:

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

lmignon

+

This module is part of the OCA/storage project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/fs_image_thumbnail/tests/__init__.py b/fs_image_thumbnail/tests/__init__.py new file mode 100644 index 0000000000..919947aec7 --- /dev/null +++ b/fs_image_thumbnail/tests/__init__.py @@ -0,0 +1 @@ +from . import test_fs_image_thumbnail diff --git a/fs_image_thumbnail/tests/test_fs_image_thumbnail.py b/fs_image_thumbnail/tests/test_fs_image_thumbnail.py new file mode 100644 index 0000000000..a7143877df --- /dev/null +++ b/fs_image_thumbnail/tests/test_fs_image_thumbnail.py @@ -0,0 +1,81 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +import base64 +import io + +from PIL import Image + +from odoo.tests import tagged + +from odoo.addons.base.tests.common import BaseCommon +from odoo.addons.fs_image.fields import FSImageValue + + +@tagged("post_install", "-at_install") +class TestFsImageThumbnail(BaseCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.white_image = cls._create_image(32, 32, color="#FFFFFF") + + cls.image_attachment = cls.env["ir.attachment"].create( + { + "name": "Test Image", + "datas": base64.b64encode(cls.white_image), + "mimetype": "image/png", + } + ) + + cls.fs_image_value = FSImageValue(attachment=cls.image_attachment) + cls.fs_thumbnail_model = cls.env["fs.thumbnail"] + + cls.temp_dir = cls.env["fs.storage"].create( + { + "name": "Temp FS Storage", + "protocol": "memory", + "code": "mem_dir", + "directory_path": "/tmp/", + "model_xmlids": "fs_image_thumbnail.model_fs_thumbnail", + } + ) + + @classmethod + def _create_image(cls, width, height, color="#4169E1", img_format="PNG"): + f = io.BytesIO() + Image.new("RGB", (width, height), color).save(f, img_format) + f.seek(0) + return f.read() + + def assert_image_size(self, value: bytes, width, height): + self.assertEqual(Image.open(io.BytesIO(value)).size, (width, height)) + + def test_create_multi(self): + self.assertFalse(self.image_attachment.thumbnail_ids) + thumbnails = self.fs_thumbnail_model.get_or_create_thumbnails( + self.fs_image_value, sizes=[(16, 16), (8, 8)], base_name="My super test" + )[self.fs_image_value] + self.assertEqual(len(thumbnails), 2) + self.assertEqual(thumbnails[0].name, "my-super-test_16_16.png") + self.assert_image_size(thumbnails[0].image.getvalue(), 16, 16) + self.assertEqual(thumbnails[1].name, "my-super-test_8_8.png") + self.assert_image_size(thumbnails[1].image.getvalue(), 8, 8) + + self.assertEqual(self.image_attachment.thumbnail_ids, thumbnails) + + # if we call the method again for the same size, we should get the same + # thumbnail + new_thumbnails = self.fs_thumbnail_model.get_or_create_thumbnails( + self.fs_image_value, sizes=[(16, 16), (8, 8)], base_name="My super test" + )[self.fs_image_value] + self.assertEqual(new_thumbnails, thumbnails) + + def test_create_with_specific_format(self): + self.env["ir.config_parameter"].set_param( + "fs_image_thumbnail.resize_format", "JPEG" + ) + thumbnail = self.fs_thumbnail_model.get_or_create_thumbnails( + self.fs_image_value, sizes=[(8, 8)], base_name="My super test" + )[self.fs_image_value] + self.assertEqual(thumbnail[0].name, "my-super-test_8_8.jpeg") + self.assertEqual(thumbnail[0].mimetype, "image/jpeg") + self.assert_image_size(thumbnail[0].image.getvalue(), 8, 8) diff --git a/fs_image_thumbnail/views/fs_image_thumbnail_mixin.xml b/fs_image_thumbnail/views/fs_image_thumbnail_mixin.xml new file mode 100644 index 0000000000..24edba033b --- /dev/null +++ b/fs_image_thumbnail/views/fs_image_thumbnail_mixin.xml @@ -0,0 +1,82 @@ + + + + + fs.image.thumbnail.mixin.form (in fs_image_thumbnail) + fs.image.thumbnail.mixin + +
+
+ + + + + + + + fs.image.thumbnail.mixin.search (in fs_image_thumbnail) + fs.image.thumbnail.mixin + + + + + + + + + + + + + + + + fs.image.thumbnail.mixin.list (in fs_image_thumbnail) + fs.image.thumbnail.mixin + + + + + + + + + + + diff --git a/fs_image_thumbnail/views/fs_thumbnail.xml b/fs_image_thumbnail/views/fs_thumbnail.xml new file mode 100644 index 0000000000..a7847bdcfe --- /dev/null +++ b/fs_image_thumbnail/views/fs_thumbnail.xml @@ -0,0 +1,60 @@ + + + + + fs.thumbnail.form + fs.thumbnail + + primary + + + + + + + + + + fs.thumbnail.search + fs.thumbnail + + primary + + + + + + + + + fs.thumbnail.list + fs.thumbnail + + primary + + + + + + + + + + Fs Thumbnail + fs.thumbnail + list,form + [] + {} + + + + Fs Image Thumbnails + + + + diff --git a/fs_image_thumbnail/views/ir_attachment.xml b/fs_image_thumbnail/views/ir_attachment.xml new file mode 100644 index 0000000000..c90faeaee0 --- /dev/null +++ b/fs_image_thumbnail/views/ir_attachment.xml @@ -0,0 +1,17 @@ + + + + + ir.attachment.form (in fs_image_thumbnail) + ir.attachment + + + + + + + + + + diff --git a/test-requirements.txt b/test-requirements.txt index e63f876677..cd94a1fe2d 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -2,3 +2,4 @@ odoo_test_helper requests_mock vcrpy-unittest s3fs>=2025.3.0 +odoo-addon-fs-image @ git+https://github.com/OCA/storage.git@refs/pull/446/head#subdirectory=fs_image