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:
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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[]> {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user