JS image processing, various file format conversion, base64 file download, image rotation, image setting transparency, modify image dpi

import changeDpi from './changeDpi'

/**
 * Convert base64 to blob
 * @param {*} base64Data
 * @param {*} type file type
 * @returns
 */
export function base64ToBlob(base64Data, type) {
  const bytes = window.atob(base64Data.split(',')[1])
  const array = []
  for (let i = 0; i < bytes. length; i ++ ) {
    array.push(bytes.charCodeAt(i))
  }
  return new Blob([new Uint8Array(array)], { type: type })
}

/**
 * Convert Blob to file
 * @param {*} theBlob blob file stream
 * @param {*} fileName file name
 * @param {*} type file type
 * @returns
 */
export function blobToFile(theBlob, fileName, type) {
  theBlob. lastModifiedDate = new Date()
  return new File([theBlob], fileName, { type: type })
}

/**
 * base64 to file
 * @param {*} base64Data
 * @param {*} fileName
 * @param {*} type file type
 * @returns
 */
export function base64ToFile(base64Data, fileName, type) {
  const bytes = window.atob(base64Data.split(',')[1])
  const array = []
  for (let i = 0; i < bytes. length; i ++ ) {
    array.push(bytes.charCodeAt(i))
  }
  const theBlob = new Blob([new Uint8Array(array)], { type: type })
  theBlob. lastModifiedDate = new Date()
  return new File([theBlob], fileName, { type: type })
}

/**
 * base64 file download
 * @param {*} base64Data
 * @param {*} fileName
 * @param {*} type
 */
export async function base64DownloadFile(base64Data, fileName, type) {
  const blob = base64ToBlob(base64Data, type)
  const link = document. createElement('a')
  link.href = window.URL.createObjectURL(blob)
  link.download = fileName

  document.body.appendChild(link)
  const evt = document. createEvent('MouseEvents')
  evt.initEvent('click', false, false)
  link. dispatchEvent(evt)
  document.body.removeChild(link)
}

/**
 * Image rotation
 * @param {*} imgUrl base64 file
 * @param {*} angle rotation angle
 * @returns
 */
export async function imageRotate(imgUrl, angle) {
  return new Promise((resolve, reject) => {
    try {
      const canvas = document. createElement('canvas')
      const ctx = canvas. getContext('2d')
      const img = new Image()
      // Allow image URLs to cross-domain
      img.crossOrigin = 'anonymous'
      img.src = imgUrl
      img.onload = function() {
        // Set the width and height of the canvas to be equal to the width and height of the image
        canvas.width = this.width
        canvas.height = this.height
        // The center point of the canvas (also the starting point) is translated to the center (0,0)->(x,y)
        ctx. translate(canvas. width / 2, canvas. height / 2)
        // canvas rotation
        ctx.rotate(angle * Math.PI / 180)
        // Draw the image. The starting point of the image needs to be offset by a negative width and height, and the starting point of the image
        ctx.drawImage(img, -this.width/2, -this.height/2)
        // Return the result (base64)
        const base64 = canvas.toDataURL('image/png')
        // The default canvas is 96dpi, change to 72dpi
        const url = changeDpi.changeDpiDataUrl(base64, 72)
        resolve(url)
      }
    } catch (error) {
      reject(error)
    }
  })
}

/**
 * Image set transparency
 * @param {*} imgUrl base64 file
 * @param {*} transparency 0-1
 * @returns
 */
export async function setAlpha(imgUrl, transparency) {
  return new Promise((resolve, reject) => {
    try {
      const canvas = document. createElement('canvas')
      const ctx = canvas. getContext('2d')
      const img = new Image()
      // Allow image URLs to cross-domain
      img.crossOrigin = 'anonymous'
      img.src = imgUrl
      img.onload = function() {
      // Set the width and height of the canvas, which is equal to the width and height of the image
        canvas.width = this.width
        canvas.height = this.height
        // set transparency
        ctx.globalAlpha = transparency
        ctx. drawImage(img, 0, 0, this. width, this. height)
        const base64 = canvas.toDataURL('image/png')
        // The default canvas is 96dpi, change to 72dpi
        const url = changeDpi.changeDpiDataUrl(base64, 72)
        resolve(url)
      }
    } catch (error) {
      reject(error)
    }
  })
}

/**
 * Image setting transparency, image rotation
 * @param {*} imgUrl base64 file
 * @param {*} transparency 0-1
 * @returns
 */
export async function setRotateAndSetAlpha(imgUrl, angle, transparency) {
  return new Promise((resolve, reject) => {
    try {
      const canvas = document. createElement('canvas')
      const ctx = canvas. getContext('2d')
      const img = new Image()
      // Allow image URLs to cross-domain
      img.crossOrigin = 'anonymous'
      img.src = imgUrl
      img.onload = function() {
        // Set the width and height of the canvas to be equal to the width and height of the image
        canvas.width = this.width
        canvas.height = this.height
        // set transparency
        ctx.globalAlpha = transparency
        // The center point of the canvas (also the starting point) is translated to the center (0,0)->(x,y)
        ctx. translate(canvas. width / 2, canvas. height / 2)
        // canvas rotation
        ctx.rotate(angle * Math.PI / 180)
        // Draw the image. The starting point of the image needs to be offset by a negative width and height, and the starting point of the image
        ctx.drawImage(img, -this.width/2, -this.height/2)
        // Return the result (base64)
        const base64 = canvas.toDataURL('image/png')
        // The default canvas is 96dpi, change to 72dpi
        const url = changeDpi.changeDpiDataUrl(base64, 72)
        resolve(url)
      }
    } catch (error) {
      reject(error)
    }
  })
}

Modify the image dpi method:

Object.defineProperty(exports, '__esModule', {
  value: true
})
exports.changeDpiBlob = changeDpiBlob
exports.changeDpiDataUrl = changeDpiDataUrl

function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i ++ ) { arr2[i] = arr[ i] } return arr2 } else { return Array. from(arr) } }

function createPngDataTable() {
  /* Table of CRCs of all 8-bit messages. */
  var crcTable = new Int32Array(256)
  for (var n = 0; n < 256; n ++ ) {
    var c = n
    for (var k = 0; k < 8; k ++ ) {
      c = c & 1 ? 0xedb88320 ^ c >>> 1 : c >>> 1
    }
    crcTable[n] = c
  }
  return crcTable
}

function calcCrc(buf) {
  var c = -1
  if (!pngDataTable) pngDataTable = createPngDataTable()
  for (var n = 0; n < buf. length; n ++ ) {
    c = pngDataTable[(c ^ buf[n]) & 0xFF] ^ c >>> 8
  }
  return c ^ -1
}

var pngDataTable = void 0

var PNG = 'image/png'
var JPEG = 'image/jpeg'

// those are 3 possible signatures of the physBlock in base64.
// the PHYs signature block is preceded by the 4 bytes of lenght. The length of
// the block is always 9 bytes. So a phys block has always this signature:
// 0 0 0 9 p H Y s.
// However the data64 encoding aligns we will always find one of those 3 strings.
// this allows us to find this particular occurrence of the pHYs block without
// converting from b64 back to string
var b64PhysSignature1 = 'AAlwSFlz'
var b64PhysSignature2 = 'AAAJcEhZ'
var b64PhysSignature3 = 'AAAACXBI'

var _P = 'p'.charCodeAt(0)
var _H = 'H'.charCodeAt(0)
var _Y = 'Y'.charCodeAt(0)
var _S = 's'.charCodeAt(0)

function changeDpiBlob(blob, dpi) {
  // 33 bytes are ok for pngs and jpegs
  // to contain the information.
  var headerChunk = blob. slice(0, 33)
  return new Promise(function(resolve, reject) {
    var fileReader = new FileReader()
    fileReader.onload = function() {
      var dataArray = new Uint8Array(fileReader. result)
      var tail = blob. slice(33)
      var changedArray = changeDpiOnArray(dataArray, dpi, blob.type)
      resolve(new Blob([changedArray, tail], { type: blob.type }))
    }
    fileReader. readAsArrayBuffer(headerChunk)
  })
}

function changeDpiDataUrl(base64Image, dpi) {
  var dataSplitted = base64Image. split(',')
  var format = dataSplitted[0]
  var body = dataSplitted[1]
  var type = void 0
  var headerLength = void 0
  var overwritepHYs = false
  if (format. indexOf(PNG) !== -1) {
    type = PNG
    var b64Index = detectPhysChunkFromDataUrl(body)
    // 28 bytes in dataUrl are 21bytes, length of phys chunk with everything inside.
    if (b64Index >= 0) {
      headerLength = Math.ceil((b64Index + 28) / 3) * 4
      overwritepHYs = true
    } else {
      headerLength = 33 / 3 * 4
    }
  }
  if (format. indexOf(JPEG) !== -1) {
    type = JPEG
    headerLength = 18 / 3 * 4
  }
  // 33 bytes are ok for pngs and jpegs
  // to contain the information.
  var stringHeader = body. substring(0, headerLength)
  var restOfData = body. substring(headerLength)
  var headerBytes = atob(stringHeader)
  var dataArray = new Uint8Array(headerBytes. length)
  for (var i = 0; i < dataArray. length; i ++ ) {
    dataArray[i] = headerBytes. charCodeAt(i)
  }
  var finalArray = changeDpiOnArray(dataArray, dpi, type, overwritepHYs)
  var base64Header = btoa(String. fromCharCode. apply(String, _toConsumableArray(finalArray)))
  return [format, ',', base64Header, restOfData].join('')
}

function detectPhysChunkFromDataUrl(data) {
  var b64index = data. indexOf(b64PhysSignature1)
  if (b64index === -1) {
    b64index = data.indexOf(b64PhysSignature2)
  }
  if (b64index === -1) {
    b64index = data.indexOf(b64PhysSignature3)
  }
  // if b64index === -1 chunk is not found
  return b64index
}

function searchStartOfPhys(data) {
  var length = data. length - 1
  // we check from the end since we cut the string in proximity of the header
  // the header is within 21 bytes from the end.
  for (var i = length; i >= 4; i--) {
    if (data[i - 4] === 9 & amp; & amp; data[i - 3] === _P & amp; & amp; data[i - 2] === _H & amp; & amp; data[i - 1] === _Y & amp; & amp; data[i] === _S) {
      return i - 3
    }
  }
}

function changeDpiOnArray(dataArray, dpi, format, overwritepHYs) {
  if (format === JPEG) {
    dataArray[13] = 1 // 1 pixel per inch or 2 pixel per cm
    dataArray[14] = dpi >> 8 // dpiX high byte
    dataArray[15] = dpi & amp; 0xff // dpiX low byte
    dataArray[16] = dpi >> 8 // dpiY high byte
    dataArray[17] = dpi & amp; 0xff // dpiY low byte
    return dataArray
  }
  if (format === PNG) {
    var physChunk = new Uint8Array(13)
    // chunk header PHYs
    // 9 bytes of data
    // 4 bytes of crc
    // this multiplication is because the standard is dpi per meter.
    dpi *= 39.3701
    physChunk[0] = _P
    physChunk[1] = _H
    physChunk[2] = _Y
    physChunk[3] = _S
    physChunk[4] = dpi >>> 24 // dpiX highest byte
    physChunk[5] = dpi >>> 16 // dpiX very high byte
    physChunk[6] = dpi >>> 8 // dpiX high byte
    physChunk[7] = dpi & amp; 0xff // dpiX low byte
    physChunk[8] = physChunk[4] // dpiY highest byte
    physChunk[9] = physChunk[5] // dpiY very high byte
    physChunk[10] = physChunk[6] // dpiY high byte
    physChunk[11] = physChunk[7] // dpiY low byte
    physChunk[12] = 1 // dot per meter....

    var crc = calcCrc(physChunk)

    var crcChunk = new Uint8Array(4)
    crcChunk[0] = crc >>> 24
    crcChunk[1] = crc >>> 16
    crcChunk[2] = crc >>> 8
    crcChunk[3] = crc & 0xff

    if (overwritepHYs) {
      var startingIndex = searchStartOfPhys(dataArray)
      dataArray.set(physChunk, startingIndex)
      dataArray.set(crcChunk, startingIndex + 13)
      return dataArray
    } else {
      // i need to give back an array of data that is divisible by 3 so that
      // dataurl encoding gives me integers, for luck this chunk is 17 + 4 = 21
      // if it was we could add a text chunk containing some info, untill desired
      // length is met.

      // chunk structure 4 bytes for length is 9
      var chunkLength = new Uint8Array(4)
      chunkLength[0] = 0
      chunkLength[1] = 0
      chunkLength[2] = 0
      chunkLength[3] = 9

      var finalHeader = new Uint8Array(54)
      finalHeader. set(dataArray, 0)
      finalHeader.set(chunkLength, 33)
      finalHeader.set(physChunk, 37)
      finalHeader.set(crcChunk, 50)
      return finalHeader
    }
  }
}

module.exports = {
  changeDpiDataUrl: changeDpiDataUrl,
  changeDpiBlob: changeDpiBlob
}