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