index.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. const Plugin = require('../../core/Plugin')
  2. const Utils = require('../../core/Utils')
  3. /**
  4. * The Thumbnail Generator plugin
  5. *
  6. */
  7. module.exports = class ThumbnailGenerator extends Plugin {
  8. constructor (uppy, opts) {
  9. super(uppy, opts)
  10. this.type = 'thumbnail'
  11. this.id = this.opts.id || 'ThumbnailGenerator'
  12. this.title = 'Thumbnail Generator'
  13. this.queue = []
  14. this.queueProcessing = false
  15. const defaultOptions = {
  16. thumbnailWidth: 200
  17. }
  18. this.opts = Object.assign({}, defaultOptions, opts)
  19. this.addToQueue = this.addToQueue.bind(this)
  20. this.onRestored = this.onRestored.bind(this)
  21. }
  22. /**
  23. * Create a thumbnail for the given Uppy file object.
  24. *
  25. * @param {{data: Blob}} file
  26. * @param {number} width
  27. * @return {Promise}
  28. */
  29. createThumbnail (file, targetWidth) {
  30. const originalUrl = URL.createObjectURL(file.data)
  31. const onload = new Promise((resolve, reject) => {
  32. const image = new Image()
  33. image.src = originalUrl
  34. image.onload = () => {
  35. URL.revokeObjectURL(originalUrl)
  36. resolve(image)
  37. }
  38. image.onerror = () => {
  39. // The onerror event is totally useless unfortunately, as far as I know
  40. URL.revokeObjectURL(originalUrl)
  41. reject(new Error('Could not create thumbnail'))
  42. }
  43. })
  44. return onload
  45. .then(image => {
  46. const targetHeight = this.getProportionalHeight(image, targetWidth)
  47. const canvas = this.resizeImage(image, targetWidth, targetHeight)
  48. return this.canvasToBlob(canvas, 'image/png')
  49. })
  50. .then(blob => {
  51. return URL.createObjectURL(blob)
  52. })
  53. }
  54. /**
  55. * Make sure the image doesn’t exceed browser/device canvas limits.
  56. * For ios with 256 RAM and ie
  57. */
  58. protect (image) {
  59. // https://stackoverflow.com/questions/6081483/maximum-size-of-a-canvas-element
  60. var ratio = image.width / image.height
  61. var maxSquare = 5000000 // ios max canvas square
  62. var maxSize = 4096 // ie max canvas dimensions
  63. var maxW = Math.floor(Math.sqrt(maxSquare * ratio))
  64. var maxH = Math.floor(maxSquare / Math.sqrt(maxSquare * ratio))
  65. if (maxW > maxSize) {
  66. maxW = maxSize
  67. maxH = Math.round(maxW / ratio)
  68. }
  69. if (maxH > maxSize) {
  70. maxH = maxSize
  71. maxW = Math.round(ratio * maxH)
  72. }
  73. if (image.width > maxW) {
  74. var canvas = document.createElement('canvas')
  75. canvas.width = maxW
  76. canvas.height = maxH
  77. canvas.getContext('2d').drawImage(image, 0, 0, maxW, maxH)
  78. image.src = 'about:blank'
  79. image.width = 1
  80. image.height = 1
  81. image = canvas
  82. }
  83. return image
  84. }
  85. /**
  86. * Resize an image to the target `width` and `height`.
  87. *
  88. * Returns a Canvas with the resized image on it.
  89. */
  90. resizeImage (image, targetWidth, targetHeight) {
  91. // Resizing in steps refactored to use a solution from
  92. // https://blog.uploadcare.com/image-resize-in-browsers-is-broken-e38eed08df01
  93. image = this.protect(image)
  94. // Use the Polyfill for Math.log2() since IE doesn't support log2
  95. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/log2#Polyfill
  96. var steps = Math.ceil(Math.log(image.width / targetWidth) * Math.LOG2E)
  97. if (steps < 1) {
  98. steps = 1
  99. }
  100. var sW = targetWidth * Math.pow(2, steps - 1)
  101. var sH = targetHeight * Math.pow(2, steps - 1)
  102. var x = 2
  103. while (steps--) {
  104. var canvas = document.createElement('canvas')
  105. canvas.width = sW
  106. canvas.height = sH
  107. canvas.getContext('2d').drawImage(image, 0, 0, sW, sH)
  108. image = canvas
  109. sW = Math.round(sW / x)
  110. sH = Math.round(sH / x)
  111. }
  112. return image
  113. }
  114. /**
  115. * Save a <canvas> element's content to a Blob object.
  116. *
  117. * @param {HTMLCanvasElement} canvas
  118. * @return {Promise}
  119. */
  120. canvasToBlob (canvas, type, quality) {
  121. if (canvas.toBlob) {
  122. return new Promise(resolve => {
  123. canvas.toBlob(resolve, type, quality)
  124. })
  125. }
  126. return Promise.resolve().then(() => {
  127. return Utils.dataURItoBlob(canvas.toDataURL(type, quality), {})
  128. })
  129. }
  130. getProportionalHeight (img, width) {
  131. const aspect = img.width / img.height
  132. return Math.round(width / aspect)
  133. }
  134. /**
  135. * Set the preview URL for a file.
  136. */
  137. setPreviewURL (fileID, preview) {
  138. this.uppy.setFileState(fileID, {
  139. preview: preview
  140. })
  141. }
  142. addToQueue (item) {
  143. this.queue.push(item)
  144. if (this.queueProcessing === false) {
  145. this.processQueue()
  146. }
  147. }
  148. processQueue () {
  149. this.queueProcessing = true
  150. if (this.queue.length > 0) {
  151. const current = this.queue.shift()
  152. return this.requestThumbnail(current)
  153. .catch(err => {}) // eslint-disable-line handle-callback-err
  154. .then(() => this.processQueue())
  155. } else {
  156. this.queueProcessing = false
  157. }
  158. }
  159. requestThumbnail (file) {
  160. if (Utils.isPreviewSupported(file.type) && !file.isRemote) {
  161. return this.createThumbnail(file, this.opts.thumbnailWidth)
  162. .then(preview => {
  163. this.setPreviewURL(file.id, preview)
  164. })
  165. .catch(err => {
  166. console.warn(err.stack || err.message)
  167. })
  168. }
  169. return Promise.resolve()
  170. }
  171. onRestored () {
  172. const fileIDs = Object.keys(this.uppy.getState().files)
  173. fileIDs.forEach((fileID) => {
  174. const file = this.uppy.getFile(fileID)
  175. if (!file.isRestored) return
  176. // Only add blob URLs; they are likely invalid after being restored.
  177. if (!file.preview || /^blob:/.test(file.preview)) {
  178. this.addToQueue(file)
  179. }
  180. })
  181. }
  182. install () {
  183. this.uppy.on('file-added', this.addToQueue)
  184. this.uppy.on('restored', this.onRestored)
  185. }
  186. uninstall () {
  187. this.uppy.off('file-added', this.addToQueue)
  188. this.uppy.off('restored', this.onRestored)
  189. }
  190. }