Apply per-use-case image size configs (#10690)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Eric Bailey
2026-06-04 11:23:04 -05:00
committed by GitHub
parent 66db349ef2
commit 795d493dea
14 changed files with 141 additions and 101 deletions
+5 -1
View File
@@ -21,6 +21,7 @@ import {sha256} from 'js-sha256'
import {CID} from 'multiformats/cid'
import * as Hasher from 'multiformats/hashes/hasher'
import {IMAGE_SIZE_CONFIG_POSTS} from '#/lib/constants'
import {isNetworkError} from '#/lib/strings/errors'
import {shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip'
import {logger} from '#/logger'
@@ -324,7 +325,10 @@ async function resolveMedia(
const images: AppBskyEmbedImages.Image[] = await Promise.all(
imagesDraft.map(async (image, i) => {
logger.debug(`Compressing image #${i}`)
const {path, width, height, mime} = await compressImage(image)
const {path, width, height, mime} = await compressImage(
image,
IMAGE_SIZE_CONFIG_POSTS,
)
logger.debug(`Uploading image #${i}`)
const res = await uploadBlob(agent, path, mime)
return {
+2 -5
View File
@@ -7,7 +7,7 @@ import {
} from '@atproto/api'
import {AtUri} from '@atproto/api'
import {DM_SERVICE_HEADERS, POST_IMG_MAX} from '#/lib/constants'
import {DM_SERVICE_HEADERS, IMAGE_SIZE_CONFIG_2K_1MB} from '#/lib/constants'
import {getLinkMeta, type LinkMeta} from '#/lib/link-meta/link-meta'
import {resolveShortLink} from '#/lib/link-meta/resolve-short-link'
import {downloadAndResize} from '#/lib/media/manip'
@@ -284,10 +284,7 @@ export async function imageToThumb(
try {
const img = await downloadAndResize({
uri: imageUri,
width: POST_IMG_MAX.width,
height: POST_IMG_MAX.height,
mode: 'contain',
maxSize: POST_IMG_MAX.size,
...IMAGE_SIZE_CONFIG_2K_1MB,
timeout: 15e3,
})
if (img) {
+8 -4
View File
@@ -97,10 +97,14 @@ export const STAGING_FEEDS = [
`feedgen|${STAGING_DEFAULT_FEED('thevids')}`,
]
export const POST_IMG_MAX = {
width: 2000,
height: 2000,
size: 1000000,
export const IMAGE_SIZE_CONFIG_POSTS = {
maxDimension: 4000,
maxSize: 2000000,
}
export const IMAGE_SIZE_CONFIG_2K_1MB = {
maxDimension: 2000,
maxSize: 1000000,
}
export const STAGING_LINK_META_PROXY =
+16 -39
View File
@@ -16,24 +16,21 @@ import {manipulateAsync, SaveFormat} from 'expo-image-manipulator'
import * as MediaLibrary from 'expo-media-library'
import * as Sharing from 'expo-sharing'
import {POST_IMG_MAX} from '#/lib/constants'
import {logger} from '#/logger'
import {IS_ANDROID, IS_IOS} from '#/env'
import {type PickerImage} from './picker.shared'
import {type Dimensions} from './types'
import {convertCdnPreset} from './util'
import {convertCdnPreset, getResizedDimensions} from './util'
export async function compressIfNeeded(
img: PickerImage,
maxSize: number = POST_IMG_MAX.size,
{maxDimension, maxSize}: {maxDimension: number; maxSize: number},
): Promise<PickerImage> {
if (img.size < maxSize) {
return img
}
const resizedImage = await doResize(normalizePath(img.path), {
width: img.width,
height: img.height,
mode: 'stretch',
maxDimension,
maxSize,
})
const finalImageMovedPath = await moveToPermanentPath(
@@ -49,9 +46,7 @@ export async function compressIfNeeded(
export interface DownloadAndResizeOpts {
uri: string
width: number
height: number
mode: 'contain' | 'cover' | 'stretch'
maxDimension: number
maxSize: number
timeout: number
}
@@ -67,7 +62,10 @@ export async function downloadAndResize(opts: DownloadAndResizeOpts) {
const path = await downloadImage(opts.uri, String(uuid.v4()), opts.timeout)
try {
return await doResize(path, opts)
return await doResize(path, {
maxDimension: opts.maxDimension,
maxSize: opts.maxSize,
})
} finally {
void safeDeleteAsync(path)
}
@@ -188,9 +186,7 @@ export function getImageDim(path: string): Promise<Dimensions> {
// =
interface DoResizeOpts {
width: number
height: number
mode: 'contain' | 'cover' | 'stretch'
maxDimension: number
maxSize: number
}
@@ -204,10 +200,13 @@ async function doResize(
// Performing an "empty" manipulation lets us get the dimensions of the original image. React Native's Image.getSize()
// does not work for local files...
const imageRes = await manipulateAsync(localUri, [], {})
const newDimensions = getResizedDimensions({
width: imageRes.width,
height: imageRes.height,
})
const newDimensions = getResizedDimensions(
{
width: imageRes.width,
height: imageRes.height,
},
opts.maxDimension,
)
let minQualityPercentage = 0
let maxQualityPercentage = 101 // exclusive
@@ -388,28 +387,6 @@ async function withTempFile<T>(
}
}
export function getResizedDimensions(originalDims: {
width: number
height: number
}) {
if (
originalDims.width <= POST_IMG_MAX.width &&
originalDims.height <= POST_IMG_MAX.height
) {
return originalDims
}
const ratio = Math.min(
POST_IMG_MAX.width / originalDims.width,
POST_IMG_MAX.height / originalDims.height,
)
return {
width: Math.round(originalDims.width * ratio),
height: Math.round(originalDims.height * ratio),
}
}
async function downloadImage(uri: string, destName: string, timeout: number) {
// Download to a temp path first, then rename with the correct extension
// based on the response's mimeType.
+22 -17
View File
@@ -1,27 +1,28 @@
import {type PickerImage} from './picker.shared'
import {type Dimensions} from './types'
import {blobToDataUri, convertCdnPreset, getDataUriSize} from './util'
import {
blobToDataUri,
convertCdnPreset,
getDataUriSize,
getResizedDimensions,
} from './util'
export async function compressIfNeeded(
img: PickerImage,
maxSize: number,
{maxDimension, maxSize}: {maxDimension: number; maxSize: number},
): Promise<PickerImage> {
if (img.size < maxSize) {
return img
}
return await doResize(img.path, {
width: img.width,
height: img.height,
mode: 'stretch',
maxDimension,
maxSize,
})
}
export interface DownloadAndResizeOpts {
uri: string
width: number
height: number
mode: 'contain' | 'cover' | 'stretch'
maxDimension: number
maxSize: number
timeout: number
}
@@ -34,7 +35,10 @@ export async function downloadAndResize(opts: DownloadAndResizeOpts) {
clearTimeout(to)
const dataUri = await blobToDataUri(resBody)
return await doResize(dataUri, opts)
return await doResize(dataUri, {
maxDimension: opts.maxDimension,
maxSize: opts.maxSize,
})
}
export async function shareImageModal(_opts: {uri: string}) {
@@ -70,9 +74,7 @@ export async function getImageDim(path: string): Promise<Dimensions> {
// =
interface DoResizeOpts {
width: number
height: number
mode: 'contain' | 'cover' | 'stretch'
maxDimension: number
maxSize: number
}
@@ -80,6 +82,9 @@ async function doResize(
dataUri: string,
opts: DoResizeOpts,
): Promise<PickerImage> {
const sourceDims = await getImageDim(dataUri)
const newDimensions = getResizedDimensions(sourceDims, opts.maxDimension)
let newDataUri
let minQualityPercentage = 0
@@ -90,10 +95,10 @@ async function doResize(
(maxQualityPercentage + minQualityPercentage) / 2,
)
const tempDataUri = await createResizedImage(dataUri, {
width: opts.width,
height: opts.height,
width: newDimensions.width,
height: newDimensions.height,
quality: qualityPercentage / 100,
mode: opts.mode,
mode: 'contain',
})
if (getDataUriSize(tempDataUri) < opts.maxSize) {
@@ -111,8 +116,8 @@ async function doResize(
path: newDataUri,
mime: 'image/jpeg',
size: getDataUriSize(newDataUri),
width: opts.width,
height: opts.height,
width: newDimensions.width,
height: newDimensions.height,
}
}
+11 -7
View File
@@ -8,6 +8,7 @@ import ExpoImageCropTool, {
type OpenCropperOptions,
} from '@bsky.app/expo-image-crop-tool'
import {IMAGE_SIZE_CONFIG_2K_1MB} from '#/lib/constants'
import {compressIfNeeded} from './manip'
import {type PickerImage} from './picker.shared'
@@ -28,13 +29,16 @@ async function getFile() {
throw new Error('Failed to get file info')
}
return await compressIfNeeded({
path: file,
mime: 'image/jpeg',
size: fileInfo.size,
width: 4288,
height: 2848,
})
return await compressIfNeeded(
{
path: file,
mime: 'image/jpeg',
size: fileInfo.size,
width: 4288,
height: 2848,
},
IMAGE_SIZE_CONFIG_2K_1MB,
)
}
export async function openPicker(): Promise<PickerImage[]> {
+25
View File
@@ -2,6 +2,31 @@ export function extractDataUriMime(uri: string): string {
return uri.substring(uri.indexOf(':') + 1, uri.indexOf(';'))
}
export function getResizedDimensions(
originalDims: {
width: number
height: number
},
maxDimension: number,
) {
if (
originalDims.width <= maxDimension &&
originalDims.height <= maxDimension
) {
return originalDims
}
const ratio = Math.min(
maxDimension / originalDims.width,
maxDimension / originalDims.height,
)
return {
width: Math.round(originalDims.width * ratio),
height: Math.round(originalDims.height * ratio),
}
}
// Fairly accurate estimate that is more performant
// than decoding and checking length of URI
export function getDataUriSize(uri: string): number {