All files / Bandstand/src/lib resize-image.ts

0.98% Statements 1/102
100% Branches 0/0
0% Functions 0/3
0.98% Lines 1/102

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 119 120 121 122 1231x                                                                                                                                                                                                                                                    
import {AVATAR_SIZE} from './avatars'
 
/**
 * Browser only. Crops the middle square out of a photo and scales it to
 * AVATAR_SIZE, as a JPEG: a 12 MP phone photo becomes ~20 KB. Drawing
 * through an <img> applies the photo's EXIF rotation, and iOS hands HEIC
 * photos to file inputs as JPEG, so any photo from a phone works.
 */
export async function squareJpeg(file: Blob, size = AVATAR_SIZE) {
  const url = URL.createObjectURL(file)
  try {
    const img = new Image()
    img.src = url
    await img.decode()
    const side = Math.min(img.naturalWidth, img.naturalHeight)
    const canvas = document.createElement('canvas')
    canvas.width = canvas.height = size
    const ctx = canvas.getContext('2d')
    if (!ctx) throw new Error('No canvas')
    ctx.imageSmoothingQuality = 'high'
    ctx.drawImage(
      img,
      (img.naturalWidth - side) / 2,
      (img.naturalHeight - side) / 2,
      side,
      side,
      0,
      0,
      size,
      size,
    )
    return await new Promise<Blob>((resolve, reject) =>
      canvas.toBlob(
        (b) => (b ? resolve(b) : reject(new Error('Could not encode'))),
        'image/jpeg',
        0.85,
      ),
    )
  } finally {
    URL.revokeObjectURL(url)
  }
}
 
/**
 * Browser only. A home-screen icon: the whole image (never cropped), scaled
 * to `scale` of a `size` square and centred on `background`. Phones put
 * transparent icons on black, so a logo with no background gets one here.
 */
export async function squareIcon(
  file: Blob,
  {
    size = 512,
    background = '#f2b134',
    scale = 0.74,
  }: {size?: number; background?: string | null; scale?: number} = {},
) {
  const url = URL.createObjectURL(file)
  try {
    const img = new Image()
    img.src = url
    await img.decode()
    const canvas = document.createElement('canvas')
    canvas.width = canvas.height = size
    const ctx = canvas.getContext('2d')
    if (!ctx) throw new Error('No canvas')
    if (background) {
      ctx.fillStyle = background
      ctx.fillRect(0, 0, size, size)
    }
    ctx.imageSmoothingQuality = 'high'
    const box = size * scale
    const k = Math.min(box / img.naturalWidth, box / img.naturalHeight)
    const w = img.naturalWidth * k
    const h = img.naturalHeight * k
    ctx.drawImage(img, (size - w) / 2, (size - h) / 2, w, h)
    return await new Promise<Blob>((resolve, reject) =>
      canvas.toBlob(
        (b) => (b ? resolve(b) : reject(new Error('Could not encode'))),
        'image/png',
      ),
    )
  } finally {
    URL.revokeObjectURL(url)
  }
}
 
/**
 * Browser only. A picture for a cue: no wider than `max` pixels. A PNG (a
 * screenshot of sheet music) stays PNG so the notes stay crisp; a photo
 * becomes a JPEG.
 */
export async function fitImage(file: Blob, max = 1600) {
  const url = URL.createObjectURL(file)
  try {
    const img = new Image()
    img.src = url
    await img.decode()
    const k = Math.min(1, max / img.naturalWidth)
    const canvas = document.createElement('canvas')
    canvas.width = Math.round(img.naturalWidth * k)
    canvas.height = Math.round(img.naturalHeight * k)
    const ctx = canvas.getContext('2d')
    if (!ctx) throw new Error('No canvas')
    const png = file.type === 'image/png'
    if (!png) {
      // JPEG has no transparency: put it on white, like paper
      ctx.fillStyle = '#fff'
      ctx.fillRect(0, 0, canvas.width, canvas.height)
    }
    ctx.imageSmoothingQuality = 'high'
    ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
    return await new Promise<Blob>((resolve, reject) =>
      canvas.toBlob(
        (b) => (b ? resolve(b) : reject(new Error('Could not encode'))),
        png ? 'image/png' : 'image/jpeg',
        0.88,
      ),
    )
  } finally {
    URL.revokeObjectURL(url)
  }
}