forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
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}