|
@@ -1,48 +1,83 @@
|
|
|
-import { UIPlugin } from '@uppy/core'
|
|
|
+import { UIPlugin, Uppy, type UIPluginOptions } from '@uppy/core'
|
|
|
import dataURItoBlob from '@uppy/utils/lib/dataURItoBlob'
|
|
|
import isObjectURL from '@uppy/utils/lib/isObjectURL'
|
|
|
import isPreviewSupported from '@uppy/utils/lib/isPreviewSupported'
|
|
|
+
|
|
|
+
|
|
|
import { rotation } from 'exifr/dist/mini.esm.mjs'
|
|
|
|
|
|
-import locale from './locale.js'
|
|
|
+import type { DefinePluginOpts } from '@uppy/core/lib/BasePlugin.ts'
|
|
|
+import type { Body, Meta, UppyFile } from '@uppy/utils/lib/UppyFile'
|
|
|
+import locale from './locale.ts'
|
|
|
+
|
|
|
+
|
|
|
import packageJson from '../package.json'
|
|
|
|
|
|
+declare module '@uppy/core' {
|
|
|
+
|
|
|
+ export interface UppyEventMap<M extends Meta, B extends Body> {
|
|
|
+ 'thumbnail:all-generated': () => void
|
|
|
+ 'thumbnail:generated': (file: UppyFile<M, B>, preview: string) => void
|
|
|
+ 'thumbnail:error': (file: UppyFile<M, B>, error: Error) => void
|
|
|
+ 'thumbnail:request': (file: UppyFile<M, B>) => void
|
|
|
+ 'thumbnail:cancel': (file: UppyFile<M, B>) => void
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+interface Rotation {
|
|
|
+ deg: number
|
|
|
+ rad: number
|
|
|
+ scaleX: number
|
|
|
+ scaleY: number
|
|
|
+ dimensionSwapped: boolean
|
|
|
+ css: boolean
|
|
|
+ canvas: boolean
|
|
|
+}
|
|
|
+
|
|
|
|
|
|
* Save a <canvas> element's content to a Blob object.
|
|
|
*
|
|
|
- * @param {HTMLCanvasElement} canvas
|
|
|
- * @returns {Promise}
|
|
|
*/
|
|
|
-function canvasToBlob (canvas, type, quality) {
|
|
|
+function canvasToBlob(
|
|
|
+ canvas: HTMLCanvasElement,
|
|
|
+ type: string,
|
|
|
+ quality: number,
|
|
|
+): Promise<Blob | File> {
|
|
|
try {
|
|
|
- canvas.getContext('2d').getImageData(0, 0, 1, 1)
|
|
|
+ canvas.getContext('2d')!.getImageData(0, 0, 1, 1)
|
|
|
} catch (err) {
|
|
|
if (err.code === 18) {
|
|
|
- return Promise.reject(new Error('cannot read image, probably an svg with external resources'))
|
|
|
+ return Promise.reject(
|
|
|
+ new Error('cannot read image, probably an svg with external resources'),
|
|
|
+ )
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if (canvas.toBlob) {
|
|
|
- return new Promise(resolve => {
|
|
|
+ return new Promise<Blob | null>((resolve) => {
|
|
|
canvas.toBlob(resolve, type, quality)
|
|
|
}).then((blob) => {
|
|
|
if (blob === null) {
|
|
|
- throw new Error('cannot read image, probably an svg with external resources')
|
|
|
+ throw new Error(
|
|
|
+ 'cannot read image, probably an svg with external resources',
|
|
|
+ )
|
|
|
}
|
|
|
return blob
|
|
|
})
|
|
|
}
|
|
|
- return Promise.resolve().then(() => {
|
|
|
- return dataURItoBlob(canvas.toDataURL(type, quality), {})
|
|
|
- }).then((blob) => {
|
|
|
- if (blob === null) {
|
|
|
- throw new Error('could not extract blob, probably an old browser')
|
|
|
- }
|
|
|
- return blob
|
|
|
- })
|
|
|
+ return Promise.resolve()
|
|
|
+ .then(() => {
|
|
|
+ return dataURItoBlob(canvas.toDataURL(type, quality), {})
|
|
|
+ })
|
|
|
+ .then((blob) => {
|
|
|
+ if (blob === null) {
|
|
|
+ throw new Error('could not extract blob, probably an old browser')
|
|
|
+ }
|
|
|
+ return blob
|
|
|
+ })
|
|
|
}
|
|
|
|
|
|
-function rotateImage (image, translate) {
|
|
|
+function rotateImage(image: HTMLImageElement, translate: Rotation) {
|
|
|
let w = image.width
|
|
|
let h = image.height
|
|
|
|
|
@@ -55,13 +90,19 @@ function rotateImage (image, translate) {
|
|
|
canvas.width = w
|
|
|
canvas.height = h
|
|
|
|
|
|
- const context = canvas.getContext('2d')
|
|
|
+ const context = canvas.getContext('2d')!
|
|
|
context.translate(w / 2, h / 2)
|
|
|
if (translate.canvas) {
|
|
|
context.rotate(translate.rad)
|
|
|
context.scale(translate.scaleX, translate.scaleY)
|
|
|
}
|
|
|
- context.drawImage(image, -image.width / 2, -image.height / 2, image.width, image.height)
|
|
|
+ context.drawImage(
|
|
|
+ image,
|
|
|
+ -image.width / 2,
|
|
|
+ -image.height / 2,
|
|
|
+ image.width,
|
|
|
+ image.height,
|
|
|
+ )
|
|
|
|
|
|
return canvas
|
|
|
}
|
|
@@ -70,7 +111,7 @@ function rotateImage (image, translate) {
|
|
|
* Make sure the image doesn’t exceed browser/device canvas limits.
|
|
|
* For ios with 256 RAM and ie
|
|
|
*/
|
|
|
-function protect (image) {
|
|
|
+function protect(image: HTMLCanvasElement): HTMLCanvasElement {
|
|
|
|
|
|
|
|
|
const ratio = image.width / image.height
|
|
@@ -92,59 +133,81 @@ function protect (image) {
|
|
|
const canvas = document.createElement('canvas')
|
|
|
canvas.width = maxW
|
|
|
canvas.height = maxH
|
|
|
- canvas.getContext('2d').drawImage(image, 0, 0, maxW, maxH)
|
|
|
+ canvas.getContext('2d')!.drawImage(image, 0, 0, maxW, maxH)
|
|
|
return canvas
|
|
|
}
|
|
|
|
|
|
return image
|
|
|
}
|
|
|
|
|
|
+export interface ThumbnailGeneratorOptions extends UIPluginOptions {
|
|
|
+ thumbnailWidth?: number | null
|
|
|
+ thumbnailHeight?: number | null
|
|
|
+ thumbnailType?: string
|
|
|
+ waitForThumbnailsBeforeUpload?: boolean
|
|
|
+ lazy?: boolean
|
|
|
+}
|
|
|
+
|
|
|
+const defaultOptions = {
|
|
|
+ thumbnailWidth: null,
|
|
|
+ thumbnailHeight: null,
|
|
|
+ thumbnailType: 'image/jpeg',
|
|
|
+ waitForThumbnailsBeforeUpload: false,
|
|
|
+ lazy: false,
|
|
|
+}
|
|
|
+
|
|
|
+type Opts = DefinePluginOpts<
|
|
|
+ ThumbnailGeneratorOptions,
|
|
|
+ keyof typeof defaultOptions
|
|
|
+>
|
|
|
+
|
|
|
|
|
|
* The Thumbnail Generator plugin
|
|
|
*/
|
|
|
|
|
|
-export default class ThumbnailGenerator extends UIPlugin {
|
|
|
+export default class ThumbnailGenerator<
|
|
|
+ M extends Meta,
|
|
|
+ B extends Body,
|
|
|
+> extends UIPlugin<Opts, M, B> {
|
|
|
static VERSION = packageJson.version
|
|
|
|
|
|
- constructor (uppy, opts) {
|
|
|
- super(uppy, opts)
|
|
|
+ queue: string[]
|
|
|
+
|
|
|
+ queueProcessing: boolean
|
|
|
+
|
|
|
+ defaultThumbnailDimension: number
|
|
|
+
|
|
|
+ thumbnailType: string
|
|
|
+
|
|
|
+ constructor(uppy: Uppy<M, B>, opts?: ThumbnailGeneratorOptions) {
|
|
|
+ super(uppy, { ...defaultOptions, ...opts })
|
|
|
this.type = 'modifier'
|
|
|
this.id = this.opts.id || 'ThumbnailGenerator'
|
|
|
this.title = 'Thumbnail Generator'
|
|
|
this.queue = []
|
|
|
this.queueProcessing = false
|
|
|
this.defaultThumbnailDimension = 200
|
|
|
- this.thumbnailType = this.opts.thumbnailType || 'image/jpeg'
|
|
|
+ this.thumbnailType = this.opts.thumbnailType
|
|
|
|
|
|
this.defaultLocale = locale
|
|
|
|
|
|
- const defaultOptions = {
|
|
|
- thumbnailWidth: null,
|
|
|
- thumbnailHeight: null,
|
|
|
- waitForThumbnailsBeforeUpload: false,
|
|
|
- lazy: false,
|
|
|
- }
|
|
|
-
|
|
|
- this.opts = { ...defaultOptions, ...opts }
|
|
|
this.i18nInit()
|
|
|
|
|
|
if (this.opts.lazy && this.opts.waitForThumbnailsBeforeUpload) {
|
|
|
- throw new Error('ThumbnailGenerator: The `lazy` and `waitForThumbnailsBeforeUpload` options are mutually exclusive. Please ensure at most one of them is set to `true`.')
|
|
|
+ throw new Error(
|
|
|
+ 'ThumbnailGenerator: The `lazy` and `waitForThumbnailsBeforeUpload` options are mutually exclusive. Please ensure at most one of them is set to `true`.',
|
|
|
+ )
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-
|
|
|
- * Create a thumbnail for the given Uppy file object.
|
|
|
- *
|
|
|
- * @param {{data: Blob}} file
|
|
|
- * @param {number} targetWidth
|
|
|
- * @param {number} targetHeight
|
|
|
- * @returns {Promise}
|
|
|
- */
|
|
|
- createThumbnail (file, targetWidth, targetHeight) {
|
|
|
+ createThumbnail(
|
|
|
+ file: UppyFile<M, B>,
|
|
|
+ targetWidth: number | null,
|
|
|
+ targetHeight: number | null,
|
|
|
+ ): Promise<string> {
|
|
|
const originalUrl = URL.createObjectURL(file.data)
|
|
|
|
|
|
- const onload = new Promise((resolve, reject) => {
|
|
|
+ const onload = new Promise<HTMLImageElement>((resolve, reject) => {
|
|
|
const image = new Image()
|
|
|
image.src = originalUrl
|
|
|
image.addEventListener('load', () => {
|
|
@@ -157,16 +220,27 @@ export default class ThumbnailGenerator extends UIPlugin {
|
|
|
})
|
|
|
})
|
|
|
|
|
|
- const orientationPromise = rotation(file.data).catch(() => 1)
|
|
|
+ const orientationPromise = rotation(file.data).catch(
|
|
|
+ () => 1,
|
|
|
+ ) as Promise<Rotation>
|
|
|
|
|
|
return Promise.all([onload, orientationPromise])
|
|
|
.then(([image, orientation]) => {
|
|
|
- const dimensions = this.getProportionalDimensions(image, targetWidth, targetHeight, orientation.deg)
|
|
|
+ const dimensions = this.getProportionalDimensions(
|
|
|
+ image,
|
|
|
+ targetWidth,
|
|
|
+ targetHeight,
|
|
|
+ orientation.deg,
|
|
|
+ )
|
|
|
const rotatedImage = rotateImage(image, orientation)
|
|
|
- const resizedImage = this.resizeImage(rotatedImage, dimensions.width, dimensions.height)
|
|
|
+ const resizedImage = this.resizeImage(
|
|
|
+ rotatedImage,
|
|
|
+ dimensions.width,
|
|
|
+ dimensions.height,
|
|
|
+ )
|
|
|
return canvasToBlob(resizedImage, this.thumbnailType, 80)
|
|
|
})
|
|
|
- .then(blob => {
|
|
|
+ .then((blob) => {
|
|
|
return URL.createObjectURL(blob)
|
|
|
})
|
|
|
}
|
|
@@ -177,9 +251,15 @@ export default class ThumbnailGenerator extends UIPlugin {
|
|
|
* account. If neither width nor height are given, the default dimension
|
|
|
* is used.
|
|
|
*/
|
|
|
- getProportionalDimensions (img, width, height, rotation) {
|
|
|
+ getProportionalDimensions(
|
|
|
+ img: HTMLImageElement,
|
|
|
+ width: number | null,
|
|
|
+ height: number | null,
|
|
|
+ deg: number,
|
|
|
+ ): { width: number; height: number } {
|
|
|
+
|
|
|
let aspect = img.width / img.height
|
|
|
- if (rotation === 90 || rotation === 270) {
|
|
|
+ if (deg === 90 || deg === 270) {
|
|
|
aspect = img.height / img.width
|
|
|
}
|
|
|
|
|
@@ -209,7 +289,11 @@ export default class ThumbnailGenerator extends UIPlugin {
|
|
|
* Returns a Canvas with the resized image on it.
|
|
|
*/
|
|
|
|
|
|
- resizeImage (image, targetWidth, targetHeight) {
|
|
|
+ resizeImage(
|
|
|
+ image: HTMLCanvasElement,
|
|
|
+ targetWidth: number,
|
|
|
+ targetHeight: number,
|
|
|
+ ): HTMLCanvasElement {
|
|
|
|
|
|
|
|
|
|
|
@@ -227,7 +311,7 @@ export default class ThumbnailGenerator extends UIPlugin {
|
|
|
const canvas = document.createElement('canvas')
|
|
|
canvas.width = sW
|
|
|
canvas.height = sH
|
|
|
- canvas.getContext('2d').drawImage(img, 0, 0, sW, sH)
|
|
|
+ canvas.getContext('2d')!.drawImage(img, 0, 0, sW, sH)
|
|
|
img = canvas
|
|
|
|
|
|
sW = Math.round(sW / x)
|
|
@@ -240,23 +324,26 @@ export default class ThumbnailGenerator extends UIPlugin {
|
|
|
|
|
|
* Set the preview URL for a file.
|
|
|
*/
|
|
|
- setPreviewURL (fileID, preview) {
|
|
|
+ setPreviewURL(fileID: string, preview: string): void {
|
|
|
this.uppy.setFileState(fileID, { preview })
|
|
|
}
|
|
|
|
|
|
- addToQueue (item) {
|
|
|
- this.queue.push(item)
|
|
|
+ addToQueue(fileID: string): void {
|
|
|
+ this.queue.push(fileID)
|
|
|
if (this.queueProcessing === false) {
|
|
|
this.processQueue()
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- processQueue () {
|
|
|
+ processQueue(): Promise<void> {
|
|
|
this.queueProcessing = true
|
|
|
if (this.queue.length > 0) {
|
|
|
- const current = this.uppy.getFile(this.queue.shift())
|
|
|
+ const current = this.uppy.getFile(this.queue.shift() as string)
|
|
|
if (!current) {
|
|
|
- this.uppy.log('[ThumbnailGenerator] file was removed before a thumbnail could be generated, but not removed from the queue. This is probably a bug', 'error')
|
|
|
+ this.uppy.log(
|
|
|
+ '[ThumbnailGenerator] file was removed before a thumbnail could be generated, but not removed from the queue. This is probably a bug',
|
|
|
+ 'error',
|
|
|
+ )
|
|
|
return Promise.resolve()
|
|
|
}
|
|
|
return this.requestThumbnail(current)
|
|
@@ -269,16 +356,29 @@ export default class ThumbnailGenerator extends UIPlugin {
|
|
|
return Promise.resolve()
|
|
|
}
|
|
|
|
|
|
- requestThumbnail (file) {
|
|
|
+ requestThumbnail(file: UppyFile<M, B>): Promise<void> {
|
|
|
if (isPreviewSupported(file.type) && !file.isRemote) {
|
|
|
- return this.createThumbnail(file, this.opts.thumbnailWidth, this.opts.thumbnailHeight)
|
|
|
- .then(preview => {
|
|
|
+ return this.createThumbnail(
|
|
|
+ file,
|
|
|
+ this.opts.thumbnailWidth,
|
|
|
+ this.opts.thumbnailHeight,
|
|
|
+ )
|
|
|
+ .then((preview) => {
|
|
|
this.setPreviewURL(file.id, preview)
|
|
|
- this.uppy.log(`[ThumbnailGenerator] Generated thumbnail for ${file.id}`)
|
|
|
- this.uppy.emit('thumbnail:generated', this.uppy.getFile(file.id), preview)
|
|
|
+ this.uppy.log(
|
|
|
+ `[ThumbnailGenerator] Generated thumbnail for ${file.id}`,
|
|
|
+ )
|
|
|
+ this.uppy.emit(
|
|
|
+ 'thumbnail:generated',
|
|
|
+ this.uppy.getFile(file.id),
|
|
|
+ preview,
|
|
|
+ )
|
|
|
})
|
|
|
- .catch(err => {
|
|
|
- this.uppy.log(`[ThumbnailGenerator] Failed thumbnail for ${file.id}:`, 'warning')
|
|
|
+ .catch((err) => {
|
|
|
+ this.uppy.log(
|
|
|
+ `[ThumbnailGenerator] Failed thumbnail for ${file.id}:`,
|
|
|
+ 'warning',
|
|
|
+ )
|
|
|
this.uppy.log(err, 'warning')
|
|
|
this.uppy.emit('thumbnail:error', this.uppy.getFile(file.id), err)
|
|
|
})
|
|
@@ -286,12 +386,12 @@ export default class ThumbnailGenerator extends UIPlugin {
|
|
|
return Promise.resolve()
|
|
|
}
|
|
|
|
|
|
- onFileAdded = (file) => {
|
|
|
+ onFileAdded = (file: UppyFile<M, B>): void => {
|
|
|
if (
|
|
|
- !file.preview
|
|
|
- && file.data
|
|
|
- && isPreviewSupported(file.type)
|
|
|
- && !file.isRemote
|
|
|
+ !file.preview &&
|
|
|
+ file.data &&
|
|
|
+ isPreviewSupported(file.type) &&
|
|
|
+ !file.isRemote
|
|
|
) {
|
|
|
this.addToQueue(file.id)
|
|
|
}
|
|
@@ -300,7 +400,7 @@ export default class ThumbnailGenerator extends UIPlugin {
|
|
|
|
|
|
* Cancel a lazy request for a thumbnail if the thumbnail has not yet been generated.
|
|
|
*/
|
|
|
- onCancelRequest = (file) => {
|
|
|
+ onCancelRequest = (file: UppyFile<M, B>): void => {
|
|
|
const index = this.queue.indexOf(file.id)
|
|
|
if (index !== -1) {
|
|
|
this.queue.splice(index, 1)
|
|
@@ -310,7 +410,7 @@ export default class ThumbnailGenerator extends UIPlugin {
|
|
|
|
|
|
* Clean up the thumbnail for a file. Cancel lazy requests and free the thumbnail URL.
|
|
|
*/
|
|
|
- onFileRemoved = (file) => {
|
|
|
+ onFileRemoved = (file: UppyFile<M, B>): void => {
|
|
|
const index = this.queue.indexOf(file.id)
|
|
|
if (index !== -1) {
|
|
|
this.queue.splice(index, 1)
|
|
@@ -322,8 +422,8 @@ export default class ThumbnailGenerator extends UIPlugin {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- onRestored = () => {
|
|
|
- const restoredFiles = this.uppy.getFiles().filter(file => file.isRestored)
|
|
|
+ onRestored = (): void => {
|
|
|
+ const restoredFiles = this.uppy.getFiles().filter((file) => file.isRestored)
|
|
|
restoredFiles.forEach((file) => {
|
|
|
|
|
|
if (!file.preview || isObjectURL(file.preview)) {
|
|
@@ -332,11 +432,11 @@ export default class ThumbnailGenerator extends UIPlugin {
|
|
|
})
|
|
|
}
|
|
|
|
|
|
- onAllFilesRemoved = () => {
|
|
|
+ onAllFilesRemoved = (): void => {
|
|
|
this.queue = []
|
|
|
}
|
|
|
|
|
|
- waitUntilAllProcessed = (fileIDs) => {
|
|
|
+ waitUntilAllProcessed = (fileIDs: string[]): Promise<void> => {
|
|
|
fileIDs.forEach((fileID) => {
|
|
|
const file = this.uppy.getFile(fileID)
|
|
|
this.uppy.emit('preprocess-progress', file, {
|
|
@@ -365,7 +465,7 @@ export default class ThumbnailGenerator extends UIPlugin {
|
|
|
})
|
|
|
}
|
|
|
|
|
|
- install () {
|
|
|
+ install(): void {
|
|
|
this.uppy.on('file-removed', this.onFileRemoved)
|
|
|
this.uppy.on('cancel-all', this.onAllFilesRemoved)
|
|
|
|
|
@@ -383,7 +483,7 @@ export default class ThumbnailGenerator extends UIPlugin {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- uninstall () {
|
|
|
+ uninstall(): void {
|
|
|
this.uppy.off('file-removed', this.onFileRemoved)
|
|
|
this.uppy.off('cancel-all', this.onAllFilesRemoved)
|
|
|
|