All files / src/lib png.ts

100% Statements 35/35
100% Branches 15/15
100% Functions 8/8
100% Lines 32/32

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 1193x                   1x   1x 1x 5x 1x 1x   1x                       2x                 7x 6x     6x   6x 5x                 1x                               2x                       1x 1x 1x 1x   1x 2x 2x         2x     2x   2x 2x 2x 2x   2x       1x     3x            
import { PNG } from 'pngjs'
import RGBAColor from '../types/RGBAColor'
 
/**
 * Renders a PNG Blob stream to a base64 PNG.
 *
 * @param {PNG} png
 * @returns {Promise<string>} base64 representation
 */
function blobToBase64 (png: PNG): Promise<string> {
  const chunks: Uint8Array[] = []
 
  return new Promise((resolve) => {
    png.pack()
    png.on('data', (c: Uint8Array) => chunks.push(c))
    png.on('end', () => {
      const result = Buffer.concat(chunks)
 
      resolve('data:image/png;base64,' + result.toString('base64'))
    })
  })
}
 
/**
 * Writes the PNG instance to a buffer.
 *
 * @param {PNG} png - image instance
 * @returns {string}
 */
function getBuffer (png: PNG): Buffer {
  return PNG.sync.write(png)
}
 
/**
 * Converts the given hexadecimal number to RGBA.
 *
 * @param {string} hex - 6-digit or 8-digit RGB(A) representation in hex
 * @returns {RGBAColor} RGBA
 */
function getRgbaColor (hex = '000000FF'): RGBAColor {
  const colors = [
    ...hex.match(/^([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})?$/i) ?? []
  ]
  const alpha = parseInt(colors[4], 16)
 
  if (colors.length >= 3) {
    return {
      red: parseInt(colors[1], 16),
      green: parseInt(colors[2], 16),
      blue: parseInt(colors[3], 16),
      alpha: !isNaN(alpha) ? alpha : 255
    }
  }
 
  // default to solid black if color is invalid
  return {
    red: 0,
    green: 0,
    blue: 0,
    alpha: 255
  }
}
 
/**
 * Returns true if two RGB colors are equal.
 *
 * @param {RGBAColor} a
 * @param {RGBAColor} b
 * @returns {boolean}
 */
function isEqualColor (a: RGBAColor, b: RGBAColor): boolean {
  return a.red === b.red && a.green === b.green && a.blue === b.blue
}
 
/**
 * Renders RGB 24 bitmap into an image instance of PNG
 *
 * @param {number[]} bitmap - containing RGB values
 * @param {number} width - width of bitmap
 * @param {number} height  height of bitmap
 * @returns {PNG} instance of PNG
 */
function render (bitmap: number[], width: number, height: number, backgroundColor?: string, foregroundColor?: string): PNG {
  const png = new PNG({ width, height })
  const backgroundColorRgba = getRgbaColor(backgroundColor)
  const foregroundColorRgba = getRgbaColor(foregroundColor)
  let i = 0
 
  for (let y = 0; y < height; y++) {
    for (let x = 0; x < width; x++) {
      const color: RGBAColor = {
        red: bitmap[i],
        green: bitmap[i + 1],
        blue: bitmap[i + 2]
      }
      const rgba = isEqualColor(color, backgroundColorRgba)
        ? backgroundColorRgba
        : foregroundColorRgba
      const pos = (png.width * y + x) << 2
 
      png.data[pos] = rgba.red
      png.data[pos + 1] = rgba.green
      png.data[pos + 2] = rgba.blue
      png.data[pos + 3] = rgba.alpha || 0
 
      i += 3
    }
  }
 
  return png
}
 
export default {
  blobToBase64,
  getBuffer,
  getRgbaColor,
  render
}