bsky-app/src/lib/media/manip.web.ts
Paul Frazee c72e24f841
[APP-716] Add 'save image' button to the lightbox (#926)
* Add 'save image' button to the lightbox

* Fix types

* Fix types
2023-06-30 11:34:04 -05:00

139 lines
3.3 KiB
TypeScript

import {Dimensions} from './types'
import {Image as RNImage} from 'react-native-image-crop-picker'
import {getDataUriSize, blobToDataUri} from './util'
export async function compressIfNeeded(
img: RNImage,
maxSize: number,
): Promise<RNImage> {
if (img.size < maxSize) {
return img
}
return await doResize(img.path, {
width: img.width,
height: img.height,
mode: 'stretch',
maxSize,
})
}
export interface DownloadAndResizeOpts {
uri: string
width: number
height: number
mode: 'contain' | 'cover' | 'stretch'
maxSize: number
timeout: number
}
export async function downloadAndResize(opts: DownloadAndResizeOpts) {
const controller = new AbortController()
const to = setTimeout(() => controller.abort(), opts.timeout || 5e3)
const res = await fetch(opts.uri)
const resBody = await res.blob()
clearTimeout(to)
const dataUri = await blobToDataUri(resBody)
return await doResize(dataUri, opts)
}
export async function shareImageModal(_opts: {uri: string}) {
// TODO
throw new Error('TODO')
}
export async function saveImageToAlbum(_opts: {uri: string; album: string}) {
// TODO
throw new Error('TODO')
}
export async function getImageDim(path: string): Promise<Dimensions> {
var img = document.createElement('img')
const promise = new Promise((resolve, reject) => {
img.onload = resolve
img.onerror = reject
})
img.src = path
await promise
return {width: img.width, height: img.height}
}
// internal methods
// =
interface DoResizeOpts {
width: number
height: number
mode: 'contain' | 'cover' | 'stretch'
maxSize: number
}
async function doResize(dataUri: string, opts: DoResizeOpts): Promise<RNImage> {
let newDataUri
for (let i = 0; i <= 10; i++) {
newDataUri = await createResizedImage(dataUri, {
width: opts.width,
height: opts.height,
quality: 1 - i * 0.1,
mode: opts.mode,
})
if (getDataUriSize(newDataUri) < opts.maxSize) {
break
}
}
if (!newDataUri) {
throw new Error('Failed to compress image')
}
return {
path: newDataUri,
mime: 'image/jpeg',
size: getDataUriSize(newDataUri),
width: opts.width,
height: opts.height,
}
}
function createResizedImage(
dataUri: string,
{
width,
height,
quality,
mode,
}: {
width: number
height: number
quality: number
mode: 'contain' | 'cover' | 'stretch'
},
): Promise<string> {
return new Promise((resolve, reject) => {
const img = document.createElement('img')
img.addEventListener('load', () => {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
if (!ctx) {
return reject(new Error('Failed to resize image'))
}
canvas.width = width
canvas.height = height
let scale = 1
if (mode === 'cover') {
scale = img.width < img.height ? width / img.width : height / img.height
} else if (mode === 'contain') {
scale = img.width > img.height ? width / img.width : height / img.height
}
let w = img.width * scale
let h = img.height * scale
let x = (width - w) / 2
let y = (height - h) / 2
ctx.drawImage(img, x, y, w, h)
resolve(canvas.toDataURL('image/jpeg', quality))
})
img.src = dataUri
})
}