utils.ts 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. export const createImage = (url: string) =>
  2. new Promise<HTMLImageElement>((resolve, reject) => {
  3. const image = new Image()
  4. image.addEventListener('load', () => resolve(image))
  5. image.addEventListener('error', error => reject(error))
  6. image.setAttribute('crossOrigin', 'anonymous') // needed to avoid cross-origin issues on CodeSandbox
  7. image.src = url
  8. })
  9. export function getRadianAngle(degreeValue: number) {
  10. return (degreeValue * Math.PI) / 180
  11. }
  12. export function getMimeType(fileName: string): string {
  13. const extension = fileName.split('.').pop()?.toLowerCase()
  14. switch (extension) {
  15. case 'png':
  16. return 'image/png'
  17. case 'jpg':
  18. case 'jpeg':
  19. return 'image/jpeg'
  20. case 'gif':
  21. return 'image/gif'
  22. case 'webp':
  23. return 'image/webp'
  24. default:
  25. return 'image/jpeg'
  26. }
  27. }
  28. /**
  29. * Returns the new bounding area of a rotated rectangle.
  30. */
  31. export function rotateSize(width: number, height: number, rotation: number) {
  32. const rotRad = getRadianAngle(rotation)
  33. return {
  34. width:
  35. Math.abs(Math.cos(rotRad) * width) + Math.abs(Math.sin(rotRad) * height),
  36. height:
  37. Math.abs(Math.sin(rotRad) * width) + Math.abs(Math.cos(rotRad) * height),
  38. }
  39. }
  40. /**
  41. * This function was adapted from the one in the ReadMe of https://github.com/DominicTobias/react-image-crop
  42. */
  43. export default async function getCroppedImg(
  44. imageSrc: string,
  45. pixelCrop: { x: number; y: number; width: number; height: number },
  46. fileName: string,
  47. rotation = 0,
  48. flip = { horizontal: false, vertical: false },
  49. ): Promise<Blob> {
  50. const image = await createImage(imageSrc)
  51. const canvas = document.createElement('canvas')
  52. const ctx = canvas.getContext('2d')
  53. const mimeType = getMimeType(fileName)
  54. if (!ctx)
  55. throw new Error('Could not create a canvas context')
  56. const rotRad = getRadianAngle(rotation)
  57. // calculate bounding box of the rotated image
  58. const { width: bBoxWidth, height: bBoxHeight } = rotateSize(
  59. image.width,
  60. image.height,
  61. rotation,
  62. )
  63. // set canvas size to match the bounding box
  64. canvas.width = bBoxWidth
  65. canvas.height = bBoxHeight
  66. // translate canvas context to a central location to allow rotating and flipping around the center
  67. ctx.translate(bBoxWidth / 2, bBoxHeight / 2)
  68. ctx.rotate(rotRad)
  69. ctx.scale(flip.horizontal ? -1 : 1, flip.vertical ? -1 : 1)
  70. ctx.translate(-image.width / 2, -image.height / 2)
  71. // draw rotated image
  72. ctx.drawImage(image, 0, 0)
  73. const croppedCanvas = document.createElement('canvas')
  74. const croppedCtx = croppedCanvas.getContext('2d')
  75. if (!croppedCtx)
  76. throw new Error('Could not create a canvas context')
  77. // Set the size of the cropped canvas
  78. croppedCanvas.width = pixelCrop.width
  79. croppedCanvas.height = pixelCrop.height
  80. // Draw the cropped image onto the new canvas
  81. croppedCtx.drawImage(
  82. canvas,
  83. pixelCrop.x,
  84. pixelCrop.y,
  85. pixelCrop.width,
  86. pixelCrop.height,
  87. 0,
  88. 0,
  89. pixelCrop.width,
  90. pixelCrop.height,
  91. )
  92. return new Promise((resolve, reject) => {
  93. croppedCanvas.toBlob((file) => {
  94. if (file)
  95. resolve(file)
  96. else
  97. reject(new Error('Could not create a blob'))
  98. }, mimeType)
  99. })
  100. }
  101. export function checkIsAnimatedImage(file) {
  102. return new Promise((resolve, reject) => {
  103. const fileReader = new FileReader()
  104. fileReader.onload = function (e) {
  105. const arr = new Uint8Array(e.target.result)
  106. // Check file extension
  107. const fileName = file.name.toLowerCase()
  108. if (fileName.endsWith('.gif')) {
  109. // If file is a GIF, assume it's animated
  110. resolve(true)
  111. }
  112. // Check for WebP signature (RIFF and WEBP)
  113. else if (isWebP(arr)) {
  114. resolve(checkWebPAnimation(arr)) // Check if it's animated
  115. }
  116. else {
  117. resolve(false) // Not a GIF or WebP
  118. }
  119. }
  120. fileReader.onerror = function (err) {
  121. reject(err) // Reject the promise on error
  122. }
  123. // Read the file as an array buffer
  124. fileReader.readAsArrayBuffer(file)
  125. })
  126. }
  127. // Function to check for WebP signature
  128. function isWebP(arr) {
  129. return (
  130. arr[0] === 0x52 && arr[1] === 0x49 && arr[2] === 0x46 && arr[3] === 0x46
  131. && arr[8] === 0x57 && arr[9] === 0x45 && arr[10] === 0x42 && arr[11] === 0x50
  132. ) // "WEBP"
  133. }
  134. // Function to check if the WebP is animated (contains ANIM chunk)
  135. function checkWebPAnimation(arr) {
  136. // Search for the ANIM chunk in WebP to determine if it's animated
  137. for (let i = 12; i < arr.length - 4; i++) {
  138. if (arr[i] === 0x41 && arr[i + 1] === 0x4E && arr[i + 2] === 0x49 && arr[i + 3] === 0x4D)
  139. return true // Found animation
  140. }
  141. return false // No animation chunk found
  142. }