Bluesky app fork with some witchin' additions 馃挮
at main 197 lines 4.8 kB view raw
1/// <reference lib="dom" /> 2 3import {type PickerImage} from './picker.shared' 4import {type Dimensions} from './types' 5import {blobToDataUri, getDataUriSize} from './util' 6import {mimeToExt} from './video/util' 7 8export async function compressIfNeeded( 9 img: PickerImage, 10 maxSize: number, 11): Promise<PickerImage> { 12 if (img.size < maxSize) { 13 return img 14 } 15 return await doResize(img.path, { 16 width: img.width, 17 height: img.height, 18 mode: 'stretch', 19 maxSize, 20 }) 21} 22 23export interface DownloadAndResizeOpts { 24 uri: string 25 width: number 26 height: number 27 mode: 'contain' | 'cover' | 'stretch' 28 maxSize: number 29 timeout: number 30} 31 32export async function downloadAndResize(opts: DownloadAndResizeOpts) { 33 const controller = new AbortController() 34 const to = setTimeout(() => controller.abort(), opts.timeout || 5e3) 35 const res = await fetch(opts.uri) 36 const resBody = await res.blob() 37 clearTimeout(to) 38 39 const dataUri = await blobToDataUri(resBody) 40 return await doResize(dataUri, opts) 41} 42 43export async function shareImageModal(_opts: {uri: string}) { 44 // TODO 45 throw new Error('TODO') 46} 47 48export async function saveImageToMediaLibrary(_opts: {uri: string}) { 49 // TODO 50 throw new Error('TODO') 51} 52 53export async function downloadVideoWeb({uri}: {uri: string}) { 54 // download the file to cache 55 const downloadResponse = await fetch(uri) 56 .then(res => res.blob()) 57 .catch(() => null) 58 if (downloadResponse == null) return false 59 const extension = mimeToExt(downloadResponse.type) 60 61 const blobUrl = URL.createObjectURL(downloadResponse) 62 const link = document.createElement('a') 63 link.setAttribute('download', uri.slice(-10) + '.' + extension) 64 link.setAttribute('href', blobUrl) 65 link.click() 66 return true 67} 68 69export async function getImageDim(path: string): Promise<Dimensions> { 70 var img = document.createElement('img') 71 const promise = new Promise((resolve, reject) => { 72 img.onload = resolve 73 img.onerror = reject 74 }) 75 img.src = path 76 await promise 77 return {width: img.width, height: img.height} 78} 79 80// internal methods 81// = 82 83interface DoResizeOpts { 84 width: number 85 height: number 86 mode: 'contain' | 'cover' | 'stretch' 87 maxSize: number 88} 89 90async function doResize( 91 dataUri: string, 92 opts: DoResizeOpts, 93): Promise<PickerImage> { 94 let newDataUri 95 96 let minQualityPercentage = 0 97 let maxQualityPercentage = 101 //exclusive 98 99 while (maxQualityPercentage - minQualityPercentage > 1) { 100 const qualityPercentage = Math.round( 101 (maxQualityPercentage + minQualityPercentage) / 2, 102 ) 103 const tempDataUri = await createResizedImage(dataUri, { 104 width: opts.width, 105 height: opts.height, 106 quality: qualityPercentage / 100, 107 mode: opts.mode, 108 }) 109 110 if (getDataUriSize(tempDataUri) < opts.maxSize) { 111 minQualityPercentage = qualityPercentage 112 newDataUri = tempDataUri 113 } else { 114 maxQualityPercentage = qualityPercentage 115 } 116 } 117 118 if (!newDataUri) { 119 throw new Error('Failed to compress image') 120 } 121 return { 122 path: newDataUri, 123 mime: 'image/jpeg', 124 size: getDataUriSize(newDataUri), 125 width: opts.width, 126 height: opts.height, 127 } 128} 129 130function createResizedImage( 131 dataUri: string, 132 { 133 width, 134 height, 135 quality, 136 mode, 137 }: { 138 width: number 139 height: number 140 quality: number 141 mode: 'contain' | 'cover' | 'stretch' 142 }, 143): Promise<string> { 144 return new Promise((resolve, reject) => { 145 const img = document.createElement('img') 146 img.addEventListener('load', () => { 147 const canvas = document.createElement('canvas') 148 const ctx = canvas.getContext('2d') 149 if (!ctx) { 150 return reject(new Error('Failed to resize image')) 151 } 152 153 let scale = 1 154 if (mode === 'cover') { 155 scale = img.width < img.height ? width / img.width : height / img.height 156 } else if (mode === 'contain') { 157 scale = img.width > img.height ? width / img.width : height / img.height 158 } 159 let w = img.width * scale 160 let h = img.height * scale 161 162 canvas.width = w 163 canvas.height = h 164 165 ctx.drawImage(img, 0, 0, w, h) 166 resolve(canvas.toDataURL('image/jpeg', quality)) 167 }) 168 img.addEventListener('error', ev => { 169 reject(ev.error) 170 }) 171 img.src = dataUri 172 }) 173} 174 175export async function saveBytesToDisk( 176 filename: string, 177 bytes: Uint8Array<ArrayBuffer>, 178 type: string, 179) { 180 const blob = new Blob([bytes], {type}) 181 const url = URL.createObjectURL(blob) 182 await downloadUrl(url, filename) 183 // Firefox requires a small delay 184 setTimeout(() => URL.revokeObjectURL(url), 100) 185 return true 186} 187 188async function downloadUrl(href: string, filename: string) { 189 const a = document.createElement('a') 190 a.href = href 191 a.download = filename 192 a.click() 193} 194 195export async function safeDeleteAsync() { 196 // no-op 197}