123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324 |
- // Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
- import type { FileUploaded } from '#shared/components/Form/fields/FieldFile/types.ts'
- import { useApplicationStore } from '#shared/stores/application.ts'
- import log from './log.ts'
- import type { Except } from 'type-fest'
- export type FilePreview = 'image' | 'calendar'
- export interface ImageFileData {
- name: string
- type: string
- content: string
- }
- export interface ImageFileSource extends Except<ImageFileData, 'content'> {
- name: string
- type: string
- src: 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
- }
- interface ValidatedFile {
- file: File
- label: string
- maxSize: number
- allowedTypes: string[]
- }
- export interface AllowedFile {
- label: string
- types: string[]
- size: number
- }
- 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()
- reader.onload = () => resolve(reader.result as string)
- reader.onerror = () => reject(reader.error)
- reader.readAsDataURL(blob)
- })
- export const dataURLToBlob = (dataURL: string) => {
- const byteString = window.atob(dataURL.split(',')[1])
- const mimeString = dataURL.split(',')[0].split(':')[1].split(';')[0]
- // Create Uint8Array directly from the byteString
- const ia = Uint8Array.from(byteString, (c) => c.charCodeAt(0))
- return new Blob([ia], { type: mimeString })
- }
- export const convertFileList = async (
- 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: base64,
- }
- })
- const readFiles = await Promise.all(promises)
- return readFiles.filter((file) => file.content)
- }
- export const loadImageIntoBase64 = async (
- src: string,
- type?: string,
- alt?: string,
- ): Promise<string | null> => {
- const img = new Image()
- img.crossOrigin = 'anonymous'
- const promise = new Promise<string | null>((resolve) => {
- img.onload = () => {
- const canvas = document.createElement('canvas')
- canvas.width = img.width
- canvas.height = img.height
- const ctx = canvas.getContext('2d')
- ctx?.drawImage(img, 0, 0, img.width, img.height)
- const mime =
- type || (img.alt?.match(/\.(jpe?g)$/i) ? 'image/jpeg' : 'image/png')
- try {
- const base64 = canvas.toDataURL(mime)
- resolve(base64)
- } catch {
- resolve(null)
- }
- }
- img.onerror = () => {
- resolve(null)
- }
- })
- img.alt = alt || ''
- img.src = src
- return promise
- }
- export const canDownloadFile = (type?: Maybe<string>) => {
- return Boolean(type && type !== 'text/html')
- }
- export const allowedImageTypes = () => {
- const { config } = useApplicationStore()
- return config['active_storage.web_image_content_types'] || []
- }
- export const allowedImageTypesString = () => {
- return allowedImageTypes().join(',')
- }
- export const canPreviewFile = (type?: Maybe<string>): FilePreview | false => {
- if (!type) return false
- if (allowedImageTypes().includes(type)) return 'image'
- if (type === 'text/calendar') return 'calendar'
- return false
- }
- export const convertFilesToAttachmentInput = (
- formId: string,
- attachments?: FileUploaded[],
- ) => {
- const files = attachments?.map((file) => ({
- name: file.name,
- type: file.type,
- }))
- if (!files || !files.length) return null
- return {
- files,
- formId,
- }
- }
- /**
- * @param file - file Size is in bytes
- * @param allowedSize - allowed size in bytes
- * * */
- export const validateFileSizeLimit = (file: File, allowedSize: number) => {
- return file.size <= allowedSize
- }
- export const validateFileSizes = (
- files: File[],
- allowedFiles: AllowedFile[],
- ) => {
- const failedFiles: Omit<ValidatedFile, 'allowedTypes'>[] = []
- files.forEach((file) => {
- allowedFiles.forEach((allowedFile) => {
- if (!allowedFile.types.includes(file.type)) return
- if (!validateFileSizeLimit(file, allowedFile.size))
- failedFiles.push({
- file,
- label: allowedFile.label,
- maxSize: allowedFile.size,
- })
- })
- })
- return failedFiles
- }
- /**
- * @return {string} - A string of acceptable file types for input element.
- * * */
- export const getAcceptableFileTypesString = (
- allowedFiles: AllowedFile[],
- ): string => {
- const result: Set<string> = new Set([])
- allowedFiles.forEach((file) => {
- file.types.forEach((type) => {
- result.add(type)
- })
- })
- return Array.from(result).join(', ')
- }
- /**
- * @param size file size in bytes
- ** */
- export const humanizeFileSize = (size: number) => {
- if (size > 1024 * 1024 * 1024) {
- return `${Math.round((size * 10) / (1024 * 1024)) / 10} MB`
- }
- if (size > 1024) {
- return `${Math.round(size / 1024)} KB`
- }
- return `${size} Bytes`
- }
|