Browse Source

Merge branch 'V8-7323-servir-les-images-rognes' into release/2.5

Olivier Massot 6 months ago
parent
commit
15d38a177d

+ 12 - 3
components/Ui/Image.vue

@@ -36,9 +36,10 @@ Permet d'afficher une image par défaut si l'image demandée n'est pas disponibl
 </template>
 
 <script setup lang="ts">
-import type { WatchStopHandle, Ref } from 'vue'
 import { useImageFetch } from '~/composables/data/useImageFetch'
 import ImageManager from '~/services/data/imageManager'
+import type { WatchStopHandle } from '@vue/runtime-core'
+import { IMAGE_SIZE } from '~/types/enum/enums'
 
 const props = defineProps({
   /**
@@ -70,6 +71,14 @@ const props = defineProps({
     type: Number,
     required: false,
   },
+  /**
+   * Taille de l'image fetchée depuis l'API (prédimensionnement)
+   */
+  size: {
+    type: String as PropType<IMAGE_SIZE>,
+    required: false,
+    default: IMAGE_SIZE.MD
+  },
   /**
    * Icône à afficher en overlay au survol de la souris
    */
@@ -92,7 +101,7 @@ const {
   data: imageSrc,
   pending,
   refresh: refreshImage,
-} = (await fetch(fileId, defaultImagePath, props.height, props.width)) as any
+} = (await fetch(fileId, props.size, defaultImagePath)) as any
 
 const refresh = () => {
   refreshImage()
@@ -104,7 +113,7 @@ defineExpose({ refresh })
  */
 const unwatch: WatchStopHandle = watch(
   () => props.imageId,
-  async (value, oldValue) => {
+  async () => {
     refresh()
   },
 )

+ 5 - 3
components/Ui/Input/Image.vue

@@ -100,9 +100,10 @@ import File from '~/models/Core/File'
 import type { PropType } from '@vue/runtime-core'
 import { useEntityManager } from '~/composables/data/useEntityManager'
 import { useImageManager } from '~/composables/data/useImageManager'
-import { FILE_VISIBILITY, TYPE_ALERT } from '~/types/enum/enums'
+import { FILE_VISIBILITY, IMAGE_SIZE, TYPE_ALERT } from '~/types/enum/enums'
 import { usePageStore } from '~/stores/page'
 import FileUtils from '~/services/utils/fileUtils'
+import { useImageFetch } from '~/composables/data/useImageFetch'
 
 const props = defineProps({
   /**
@@ -257,7 +258,7 @@ const loadImage = async (fileId: number) => {
 
   currentImage.value.name = file.value.name
   currentImage.value.id = file.value.id
-  currentImage.value.src = (await imageManager.get(fileId)) as string
+  currentImage.value.src = await imageManager.get(fileId, IMAGE_SIZE.RAW)
 }
 
 /**
@@ -414,13 +415,14 @@ const save = async () => {
   } else if (currentImage.value.id) {
     // L'image existante a été modifiée
     await saveExistingImage()
-    uiImage.value.refresh()
   } else {
     // On a reset l'image
     emit('update:modelValue', null)
   }
 
   showModal.value = false
+
+  uiImage.value.refresh()
   pageStore.loading = false
 }
 

+ 5 - 6
composables/data/useImageFetch.ts

@@ -2,13 +2,13 @@ import type { AsyncData } from '#app'
 import { v4 as uuid4 } from 'uuid'
 import type { Ref } from 'vue'
 import { useImageManager } from '~/composables/data/useImageManager'
+import { IMAGE_SIZE } from '~/types/enum/enums'
 
 interface useImageFetchReturnType {
   fetch: (
     id: Ref<number | null>,
+    size?: IMAGE_SIZE,
     defaultImage?: string | null,
-    height?: number,
-    width?: number,
   ) => AsyncData<string | ArrayBuffer | null, Error | null>
 }
 
@@ -20,13 +20,12 @@ export const useImageFetch = (): useImageFetchReturnType => {
 
   const fetch = (
     id: Ref<number | null>, // If id is null, fetch shall return the default image url
-    defaultImage: string | null = null,
-    height: number = 0,
-    width: number = 0,
+    size: IMAGE_SIZE = IMAGE_SIZE.MD,
+    defaultImage: string | null = null
   ) =>
     useAsyncData(
       'img' + (id ?? defaultImage ?? 0) + '_' + uuid4(),
-      () => imageManager.get(id.value, defaultImage, height, width),
+      () => imageManager.get(id.value, size, defaultImage),
       { lazy: true, server: false }, // Always fetch images client-side
     )
 

+ 1 - 1
pages/parameters/general_parameters.vue

@@ -77,7 +77,7 @@
                 v-model="parameters.qrCode"
                 field="qrCode"
                 :width="120"
-                :cropping-enabled="false"
+                :cropping-enabled="true"
               />
             </div>
           </v-col>

+ 70 - 21
services/data/imageManager.ts

@@ -1,6 +1,8 @@
 import ApiRequestService from './apiRequestService'
 import FileUtils from '~/services/utils/fileUtils'
-import { FILE_TYPE, FILE_VISIBILITY } from '~/types/enum/enums'
+import { FILE_TYPE, FILE_VISIBILITY, IMAGE_SIZE } from '~/types/enum/enums'
+import type { AssociativeArray } from '~/types/data'
+import UrlUtils from '~/services/utils/urlUtils'
 
 /**
  * Permet le requêtage, l'upload et la manipulation des images via l'API Opentalent
@@ -14,50 +16,97 @@ class ImageManager {
   }
 
   /**
-   * Retourne l'image correspondante sous forme d'un blob encodé au format base64,
-   * ou l'url d'une image par défaut si l'image est introuvable ou si l'id passé en paramètre est null
+   * Retourne l'image correspondante sous forme soit d'une URL, soit d'un blob encodé au format base64.
    *
-   * Attention, les dimensions (hauteur / largeur) ne s'appliqueront pas à l'image par défaut, il est nécessaire de
-   * les redéfinir dans le composant lui-même.
+   * Retourne l'url de l'image par défaut si l'image est introuvable
+   * ou si l'id passé en paramètre est null.
    *
    * @param id  The id of the image; if null, the url to the default image is returned
+   * @param size
    * @param defaultImage The path of an image in the 'public' folder, default: '/images/default/picture.jpeg'
-   * @param height  Height of the image (does not apply to default image)
-   * @param width   Width of the image (does not apply to default image)
    */
   public async get(
-    id: number | null,
+    id: number | string | null,
+    size: IMAGE_SIZE = IMAGE_SIZE.MD,
     defaultImage: string | null = null,
-    height: number = 0,
-    width: number = 0,
-  ): Promise<string | ArrayBuffer> {
+  ): Promise<string> {
     const defaultUrl = defaultImage ?? ImageManager.defaultImage
 
     if (id === null) {
       return defaultUrl
     }
 
-    const imageUrl = `api/file/download/${id}`
+    const matches = id.toString().match(/\/api\/files\/(\d+)(?:\/\w+)?/)
+    if (matches) {
+      // Lors de l'enregistrement d'une entité, les ids des objets liés sont
+      // temporairement convertis en IRI. Avec la réactivité, ceci peut
+      // générer une erreur temporaire avec les liens des images, d'où ce patch.
+      id = parseInt(matches[1])
+    }
+
+    if (!(typeof id === 'number' && Number.isInteger(id))) {
+      throw new Error('Error: image ' + id + ' is invalid')
+    }
+
+    try {
+      return size === IMAGE_SIZE.RAW ?
+        this.getRaw(id) :
+        this.getProcessed(id, size)
 
-    // Set requested size if needed
-    if (height > 0 || width > 0) {
-      // @see https://thumbor.readthedocs.io/en/latest/crop_and_resize_algorithms.html
-      // TODO: ajouter le support de ces options dans ap2i
-      // url = UrlUtils.join(url, `${height}x${width}`)
+    } catch (error) {
+      console.error(error)
+      return defaultUrl
     }
+  }
+
+  /**
+   * Retourne une image dimensionnée et cropped depuis le cache Liip.
+   *
+   * @param id  The id of the image; if null, the url to the default image is returned
+   * @param size
+   */
+  private async getProcessed(
+    id: number | null,
+    size: IMAGE_SIZE = IMAGE_SIZE.MD
+  ): Promise<string> {
+
+    let imageUrl = `api/image/download/${id}/${size}`
+
+    // Une image doit toujours avoir le time en options pour éviter les problèmes de cache
+    const query: AssociativeArray = {0: this.getCacheKey()}
+
+    const response = await this.apiRequestService.get(imageUrl, query);
+
+    const cachedImageUrl = response.toString()
+
+    if (!cachedImageUrl) {
+      throw new Error('Error: image ' + id + ' not found');
+    }
+
+    return UrlUtils.addQuery(cachedImageUrl, query)
+  }
+
+  /**
+   * Retourne l'image non traitée. Utilisé entre autres pour le
+   * cropper du UiInputImage.
+   *
+   * @param id
+   * @private
+   */
+  private async getRaw(id: number | null): Promise<string> {
+
+    const imageUrl = `api/file/download/${id}`
 
     // Une image doit toujours avoir le time en options pour éviter les problèmes de cache
     const query = [this.getCacheKey()]
 
     const blobPart = await this.apiRequestService.get(imageUrl, query)
     if (!blobPart) {
-      console.error('Error: image ' + id + ' not found')
-      return defaultUrl
+      throw new Error('Error: image ' + id + ' not found');
     }
 
     if (!(blobPart instanceof Blob) || blobPart.size === 0) {
-      console.error('Error: image ' + id + ' is invalid')
-      return defaultUrl
+      throw new Error('Error: image ' + id + ' is invalid');
     }
 
     return await this.toBase64(blobPart)

+ 7 - 0
types/enum/enums.ts

@@ -111,3 +111,10 @@ export const enum TABLE_ACTION {
   DELETE = 'delete',
   ADD = 'add',
 }
+
+export const enum IMAGE_SIZE {
+  SM = 'sm',
+  MD = 'md',
+  LG = 'lg',
+  RAW = 'raw'
+}