|
@@ -2,6 +2,7 @@
|
|
|
|
|
|
import type { FileUploaded } from '#shared/components/Form/fields/FieldFile/types.ts'
|
|
|
import { useApplicationStore } from '#shared/stores/application.ts'
|
|
|
+import log from './log.ts'
|
|
|
|
|
|
export interface ImageFileData {
|
|
|
name: string
|
|
@@ -9,6 +10,126 @@ export interface ImageFileData {
|
|
|
content: string
|
|
|
}
|
|
|
|
|
|
+interface CompressData {
|
|
|
+ x?: number | 'auto'
|
|
|
+ y?: number | 'auto'
|
|
|
+ scale?: number
|
|
|
+ type?: string
|
|
|
+ quality?: number | 'auto'
|
|
|
+}
|
|
|
+
|
|
|
+interface CompressOptions {
|
|
|
+ compress?: boolean
|
|
|
+ onCompress?(image: HTMLImageElement, type: string): CompressData
|
|
|
+}
|
|
|
+
|
|
|
+const allowCompressMime = ['image/jpeg', 'image/png']
|
|
|
+
|
|
|
+const getQuality = (x: number, y: number) => {
|
|
|
+ if (x < 200 && y < 200) return 1
|
|
|
+ if (x < 400 && y < 400) return 0.9
|
|
|
+ if (x < 600 && y < 600) return 0.8
|
|
|
+ if (x < 900 && y < 900) return 0.7
|
|
|
+ return 0.6
|
|
|
+}
|
|
|
+
|
|
|
+export const compressImage = (
|
|
|
+ imageSrc: string,
|
|
|
+ type: string,
|
|
|
+ options?: CompressOptions,
|
|
|
+) => {
|
|
|
+ const img = new Image()
|
|
|
+ // eslint-disable-next-line sonarjs/cognitive-complexity
|
|
|
+ const promise = new Promise<string>((resolve) => {
|
|
|
+ img.onload = () => {
|
|
|
+ const {
|
|
|
+ x: imgX = 'auto',
|
|
|
+ y: imgY = 'auto',
|
|
|
+ quality = 'auto',
|
|
|
+ scale = 1,
|
|
|
+ type: mimeType = type,
|
|
|
+ } = options?.onCompress?.(img, type) || {}
|
|
|
+
|
|
|
+ const imageWidth = img.width
|
|
|
+ const imageHeight = img.height
|
|
|
+
|
|
|
+ log.debug('[Image Service] Image is loaded', {
|
|
|
+ imageWidth,
|
|
|
+ imageHeight,
|
|
|
+ })
|
|
|
+
|
|
|
+ let x = imgX
|
|
|
+ let y = imgY
|
|
|
+
|
|
|
+ if (y === 'auto' && x === 'auto') {
|
|
|
+ x = imageWidth
|
|
|
+ y = imageHeight
|
|
|
+ }
|
|
|
+
|
|
|
+ // set max x/y
|
|
|
+ if (x !== 'auto' && x > imageWidth) x = imageWidth
|
|
|
+
|
|
|
+ if (y !== 'auto' && y > imageHeight) y = imageHeight
|
|
|
+
|
|
|
+ // get auto dimensions
|
|
|
+ if (y === 'auto') {
|
|
|
+ const factor = imageWidth / (x as number)
|
|
|
+ y = imageHeight / factor
|
|
|
+ }
|
|
|
+
|
|
|
+ if (x === 'auto') {
|
|
|
+ const factor = imageHeight / y
|
|
|
+ x = imageWidth / factor
|
|
|
+ }
|
|
|
+
|
|
|
+ const canvas = document.createElement('canvas')
|
|
|
+
|
|
|
+ if (
|
|
|
+ (x < imageWidth && x * scale < imageWidth) ||
|
|
|
+ (y < imageHeight && y * scale < imageHeight)
|
|
|
+ ) {
|
|
|
+ x *= scale
|
|
|
+ y *= scale
|
|
|
+
|
|
|
+ // set dimensions
|
|
|
+ canvas.width = x
|
|
|
+ canvas.height = y
|
|
|
+
|
|
|
+ // draw image on canvas and set image dimensions
|
|
|
+ const context = canvas.getContext('2d') as CanvasRenderingContext2D
|
|
|
+ context.drawImage(img, 0, 0, x, y)
|
|
|
+ } else {
|
|
|
+ canvas.width = imageWidth
|
|
|
+ canvas.height = imageHeight
|
|
|
+
|
|
|
+ const context = canvas.getContext('2d') as CanvasRenderingContext2D
|
|
|
+ context.drawImage(img, 0, 0, imageWidth, imageHeight)
|
|
|
+ }
|
|
|
+
|
|
|
+ const qualityValue =
|
|
|
+ quality === 'auto' ? getQuality(imageWidth, imageHeight) : quality
|
|
|
+
|
|
|
+ try {
|
|
|
+ const base64 = canvas.toDataURL(mimeType, qualityValue)
|
|
|
+ log.debug('[Image Service] Image is compressed', {
|
|
|
+ quality: qualityValue,
|
|
|
+ type: mimeType,
|
|
|
+ x,
|
|
|
+ y,
|
|
|
+ size: `${(base64.length * 0.75) / 1024 / 1024} Mb`,
|
|
|
+ })
|
|
|
+ resolve(base64)
|
|
|
+ } catch (e) {
|
|
|
+ log.debug('[Image Service] Failed to compress an image', e)
|
|
|
+ resolve(imageSrc)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ img.onerror = () => resolve(imageSrc)
|
|
|
+ })
|
|
|
+ img.src = imageSrc
|
|
|
+ return promise
|
|
|
+}
|
|
|
+
|
|
|
export const blobToBase64 = async (blob: Blob) =>
|
|
|
new Promise<string>((resolve, reject) => {
|
|
|
const reader = new FileReader()
|
|
@@ -18,15 +139,22 @@ export const blobToBase64 = async (blob: Blob) =>
|
|
|
})
|
|
|
|
|
|
export const convertFileList = async (
|
|
|
- filesList?: Maybe<FileList>,
|
|
|
+ filesList: Maybe<FileList | File[]>,
|
|
|
+ options: CompressOptions = {},
|
|
|
): Promise<ImageFileData[]> => {
|
|
|
const files = Array.from(filesList || [])
|
|
|
|
|
|
const promises = files.map(async (file) => {
|
|
|
+ let base64 = await blobToBase64(file)
|
|
|
+
|
|
|
+ if (options?.compress && allowCompressMime.includes(file.type)) {
|
|
|
+ base64 = await compressImage(base64, file.type, options)
|
|
|
+ }
|
|
|
+
|
|
|
return {
|
|
|
name: file.name,
|
|
|
type: file.type,
|
|
|
- content: await blobToBase64(file),
|
|
|
+ content: base64,
|
|
|
}
|
|
|
})
|
|
|
|
|
@@ -37,6 +165,7 @@ export const convertFileList = async (
|
|
|
|
|
|
export const loadImageIntoBase64 = async (
|
|
|
src: string,
|
|
|
+ type?: string,
|
|
|
alt?: string,
|
|
|
): Promise<string | null> => {
|
|
|
const img = new Image()
|
|
@@ -48,7 +177,8 @@ export const loadImageIntoBase64 = async (
|
|
|
canvas.height = img.height
|
|
|
const ctx = canvas.getContext('2d')
|
|
|
ctx?.drawImage(img, 0, 0, img.width, img.height)
|
|
|
- const mime = img.alt?.match(/\.(jpe?g)$/i) ? 'image/jpeg' : 'image/png'
|
|
|
+ const mime =
|
|
|
+ type || (img.alt?.match(/\.(jpe?g)$/i) ? 'image/jpeg' : 'image/png')
|
|
|
try {
|
|
|
const base64 = canvas.toDataURL(mime)
|
|
|
resolve(base64)
|
|
@@ -75,7 +205,7 @@ export const canPreviewFile = (type?: Maybe<string>) => {
|
|
|
const { config } = useApplicationStore()
|
|
|
|
|
|
const allowedPreviewContentTypes =
|
|
|
- (config['active_storage.web_image_content_types'] as string[]) || []
|
|
|
+ config['active_storage.web_image_content_types'] || []
|
|
|
|
|
|
return allowedPreviewContentTypes.includes(type)
|
|
|
}
|