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