deer social fork for personal usage. but you might see a use idk. github mirror
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

better upload image quality

ayla f3fa63f3 07119057

+72 -44
+7 -2
src/lib/api/index.ts
··· 39 39 type PostDraft, 40 40 type ThreadDraft, 41 41 } from '#/view/com/composer/state/composer' 42 + import {type BetterImage} from '#/types/bsky/post' 42 43 import {createGIFDescription} from '../gif-alt-text' 43 44 import {uploadBlob} from './upload-blob' 44 45 ··· 300 301 count: imagesDraft.length, 301 302 }) 302 303 onStateChange?.(t`Uploading images...`) 303 - const images: AppBskyEmbedImages.Image[] = await Promise.all( 304 + const images: BetterImage[] = await Promise.all( 304 305 imagesDraft.map(async (image, i) => { 305 306 logger.debug(`Compressing image #${i}`) 306 - const {path, width, height, mime} = await compressImage(image) 307 + const {path, width, height, mime, size, quality} = 308 + await compressImage(image) 307 309 logger.debug(`Uploading image #${i}`) 308 310 const res = await uploadBlob(agent, path, mime) 309 311 return { 310 312 image: res.data.blob, 311 313 alt: image.alt, 312 314 aspectRatio: {width, height}, 315 + mime: mime, 316 + quality, 317 + size, 313 318 } 314 319 }), 315 320 )
+2 -2
src/lib/constants.ts
··· 83 83 ] 84 84 85 85 export const POST_IMG_MAX = { 86 - width: 2000, 87 - height: 2000, 86 + width: 5000, 87 + height: 5000, 88 88 size: 1000000, 89 89 } 90 90
+13 -11
src/lib/media/manip.ts
··· 21 21 import {isAndroid, isIOS} from '#/platform/detection' 22 22 import {type PickerImage} from './picker.shared' 23 23 import {type Dimensions} from './types' 24 + import {getDataUriSize} from './util' 24 25 import {mimeToExt} from './video/util' 25 26 26 27 export async function compressIfNeeded( ··· 38 39 }) 39 40 const finalImageMovedPath = await moveToPermanentPath( 40 41 resizedImage.path, 41 - '.jpg', 42 + '.webp', 42 43 ) 43 44 const finalImg = { 44 45 ...resizedImage, ··· 63 64 const ext = urip.pathname.split('.').pop() 64 65 if (ext === 'png') { 65 66 appendExt = 'png' 67 + } else if (ext === 'webp') { 68 + appendExt = 'webp' 66 69 } 67 70 } catch (e: any) { 68 71 console.error('Invalid URI', opts.uri, e) ··· 195 198 width: imageRes.width, 196 199 height: imageRes.height, 197 200 }) 201 + const originalSize = getDataUriSize(localUri) + 1024 198 202 199 - let minQualityPercentage = 0 200 - let maxQualityPercentage = 101 // exclusive 203 + let maxQualityPercentage = 110 // exclusive 201 204 let newDataUri 202 205 const intermediateUris = [] 203 206 204 - while (maxQualityPercentage - minQualityPercentage > 1) { 205 - const qualityPercentage = Math.round( 206 - (maxQualityPercentage + minQualityPercentage) / 2, 207 - ) 207 + while (maxQualityPercentage > 1) { 208 + const qualityPercentage = Math.round(maxQualityPercentage - 10) 208 209 const resizeRes = await manipulateAsync( 209 210 localUri, 210 211 [{resize: newDimensions}], 211 212 { 212 - format: SaveFormat.JPEG, 213 + format: SaveFormat.WEBP, 213 214 compress: qualityPercentage / 100, 214 215 }, 215 216 ) ··· 223 224 ) 224 225 } 225 226 226 - if (fileInfo.size < opts.maxSize) { 227 - minQualityPercentage = qualityPercentage 227 + if (fileInfo.size < opts.maxSize && fileInfo.size < originalSize) { 228 228 newDataUri = { 229 229 path: normalizePath(resizeRes.uri), 230 - mime: 'image/jpeg', 230 + mime: 'image/webp', 231 231 size: fileInfo.size, 232 232 width: resizeRes.width, 233 233 height: resizeRes.height, 234 + quality: qualityPercentage, 234 235 } 236 + break 235 237 } else { 236 238 maxQualityPercentage = qualityPercentage 237 239 }
+13 -10
src/lib/media/manip.web.ts
··· 91 91 dataUri: string, 92 92 opts: DoResizeOpts, 93 93 ): Promise<PickerImage> { 94 + const originalSize = getDataUriSize(dataUri) + 1024 95 + 94 96 let newDataUri 95 97 96 - let minQualityPercentage = 0 97 - let maxQualityPercentage = 101 //exclusive 98 + let maxQualityPercentage = 110 //exclusive 99 + let finalCompressionPercentage: number = 100 98 100 99 - while (maxQualityPercentage - minQualityPercentage > 1) { 100 - const qualityPercentage = Math.round( 101 - (maxQualityPercentage + minQualityPercentage) / 2, 102 - ) 101 + while (maxQualityPercentage > 1) { 102 + const qualityPercentage = Math.round(maxQualityPercentage - 10) 103 103 const tempDataUri = await createResizedImage(dataUri, { 104 104 width: opts.width, 105 105 height: opts.height, ··· 107 107 mode: opts.mode, 108 108 }) 109 109 110 - if (getDataUriSize(tempDataUri) < opts.maxSize) { 111 - minQualityPercentage = qualityPercentage 110 + const size = getDataUriSize(tempDataUri) 111 + if (size < opts.maxSize && size < originalSize) { 112 112 newDataUri = tempDataUri 113 + finalCompressionPercentage = qualityPercentage 114 + break 113 115 } else { 114 116 maxQualityPercentage = qualityPercentage 115 117 } ··· 120 122 } 121 123 return { 122 124 path: newDataUri, 123 - mime: 'image/jpeg', 125 + mime: 'image/webp', 124 126 size: getDataUriSize(newDataUri), 125 127 width: opts.width, 126 128 height: opts.height, 129 + quality: finalCompressionPercentage, 127 130 } 128 131 } 129 132 ··· 163 166 canvas.height = h 164 167 165 168 ctx.drawImage(img, 0, 0, w, h) 166 - resolve(canvas.toDataURL('image/jpeg', quality)) 169 + resolve(canvas.toDataURL('image/webp', quality)) 167 170 }) 168 171 img.addEventListener('error', ev => { 169 172 reject(ev.error)
+3 -2
src/lib/media/picker.e2e.tsx
··· 28 28 29 29 return await compressIfNeeded({ 30 30 path: file, 31 - mime: 'image/jpeg', 31 + mime: 'image/webp', 32 32 size: fileInfo.size, 33 33 width: 4288, 34 34 height: 2848, 35 + quality: 100, 35 36 }) 36 37 } 37 38 ··· 62 63 export async function openCropper(opts: OpenCropperOptions) { 63 64 const item = await ExpoImageCropTool.openCropperAsync({ 64 65 ...opts, 65 - format: 'jpeg', 66 + format: 'png', 66 67 }) 67 68 68 69 return {
+2 -1
src/lib/media/picker.shared.ts
··· 13 13 14 14 export type PickerImage = ImageMeta & { 15 15 size: number 16 + quality: number 16 17 } 17 18 18 19 export async function openPicker(opts?: ImagePickerOptions) { ··· 34 35 return false 35 36 }) 36 37 .map(image => ({ 37 - mime: image.mimeType || 'image/jpeg', 38 + mime: image.mimeType || 'image/webp', 38 39 height: image.height, 39 40 width: image.width, 40 41 path: image.uri,
+4 -2
src/lib/media/picker.tsx
··· 22 22 23 23 return { 24 24 path: asset.uri, 25 - mime: asset.mimeType ?? 'image/jpeg', 25 + mime: asset.mimeType ?? 'image/png', 26 26 size: asset.fileSize ?? 0, 27 27 width: asset.width, 28 28 height: asset.height, 29 + quality: 100, 29 30 } 30 31 } 31 32 32 33 export async function openCropper(opts: OpenCropperOptions) { 33 34 const item = await ExpoImageCropTool.openCropperAsync({ 34 35 ...opts, 35 - format: 'jpeg', 36 + format: 'png', 36 37 }) 37 38 38 39 return { ··· 41 42 size: item.size, 42 43 width: item.width, 43 44 height: item.height, 45 + quality: 100, 44 46 } 45 47 }
+12 -12
src/state/gallery.ts
··· 94 94 path: uri, 95 95 width: width, 96 96 height: height, 97 - mime: 'image/jpeg', 97 + mime: 'image/png', 98 98 }, 99 99 } 100 100 }) ··· 113 113 path: uri, 114 114 width: width, 115 115 height: height, 116 - mime: match ? match[1] : 'image/jpeg', 116 + mime: match ? match[1] : 'image/png', 117 117 }, 118 118 } 119 119 } ··· 197 197 198 198 export async function compressImage(img: ComposerImage): Promise<PickerImage> { 199 199 const source = img.transformed || img.source 200 + const originalSize = getDataUriSize(img.source.path) + 1024 200 201 201 202 const [w, h] = containImageRes(source.width, source.height, POST_IMG_MAX) 202 203 203 - let minQualityPercentage = 0 204 - let maxQualityPercentage = 101 // exclusive 204 + let maxQualityPercentage = 205 + 110 - (originalSize >= POST_IMG_MAX.size * 2 ? 10 : 0) // exclusive 205 206 let newDataUri 206 207 207 - while (maxQualityPercentage - minQualityPercentage > 1) { 208 - const qualityPercentage = Math.round( 209 - (maxQualityPercentage + minQualityPercentage) / 2, 210 - ) 208 + while (maxQualityPercentage > 1) { 209 + const qualityPercentage = Math.round(maxQualityPercentage - 10) 211 210 212 211 const res = await manipulateAsync( 213 212 source.path, 214 213 [{resize: {width: w, height: h}}], 215 214 { 216 215 compress: qualityPercentage / 100, 217 - format: SaveFormat.JPEG, 216 + format: SaveFormat.WEBP, 218 217 base64: true, 219 218 }, 220 219 ) 221 220 222 221 const base64 = res.base64 223 222 const size = base64 ? getDataUriSize(base64) : 0 224 - if (base64 && size <= POST_IMG_MAX.size) { 225 - minQualityPercentage = qualityPercentage 223 + if (base64 && size <= POST_IMG_MAX.size && size <= originalSize) { 226 224 newDataUri = { 227 225 path: await moveIfNecessary(res.uri), 228 226 width: res.width, 229 227 height: res.height, 230 - mime: 'image/jpeg', 228 + mime: 'image/webp', 231 229 size, 230 + quality: qualityPercentage, 232 231 } 232 + break 233 233 } else { 234 234 maxQualityPercentage = qualityPercentage 235 235 }
+13
src/types/bsky/post.ts
··· 1 1 import { 2 2 type $Typed, 3 + type AppBskyEmbedDefs, 3 4 AppBskyEmbedExternal, 4 5 AppBskyEmbedImages, 5 6 AppBskyEmbedRecord, ··· 8 9 AppBskyFeedDefs, 9 10 AppBskyGraphDefs, 10 11 AppBskyLabelerDefs, 12 + type BlobRef, 11 13 } from '@atproto/api' 12 14 13 15 export type Embed = ··· 147 149 } 148 150 } 149 151 } 152 + 153 + export interface BetterImage { 154 + $type?: 'app.bsky.embed.images#image' 155 + image: BlobRef 156 + /** Alt text description of the image, for accessibility. */ 157 + alt: string 158 + aspectRatio?: AppBskyEmbedDefs.AspectRatio 159 + mime?: string 160 + quality?: number 161 + size?: number 162 + }
+3 -2
src/view/com/modals/CropImage.web.tsx
··· 61 61 ], 62 62 { 63 63 base64: true, 64 - format: SaveFormat.JPEG, 64 + format: SaveFormat.WEBP, 65 65 }, 66 66 ) 67 67 68 68 onSelect({ 69 69 path: result.uri, 70 - mime: 'image/jpeg', 70 + mime: 'image/png', 71 71 size: result.base64 !== undefined ? getDataUriSize(result.base64) : 0, 72 72 width: result.width, 73 73 height: result.height, 74 + quality: 100, 74 75 }) 75 76 76 77 closeModal()