Remove image resizer (#5464)

This commit is contained in:
Hailey
2024-09-24 10:15:33 -07:00
committed by GitHub
parent d2fae81b33
commit ea43d20c61
6 changed files with 147 additions and 99 deletions
+66 -62
View File
@@ -1,26 +1,30 @@
import ImageResizer from '@bam.tech/react-native-image-resizer' import {deleteAsync} from 'expo-file-system'
import {manipulateAsync, SaveFormat} from 'expo-image-manipulator'
import RNFetchBlob from 'rn-fetch-blob' import RNFetchBlob from 'rn-fetch-blob'
import { import {
downloadAndResize, downloadAndResize,
DownloadAndResizeOpts, DownloadAndResizeOpts,
getResizedDimensions,
} from '../../src/lib/media/manip' } from '../../src/lib/media/manip'
const mockResizedImage = {
path: 'file://resized-image.jpg',
size: 100,
width: 100,
height: 100,
mime: 'image/jpeg',
}
describe('downloadAndResize', () => { describe('downloadAndResize', () => {
const errorSpy = jest.spyOn(global.console, 'error') const errorSpy = jest.spyOn(global.console, 'error')
const mockResizedImage = {
path: jest.fn().mockReturnValue('file://resized-image.jpg'),
size: 100,
width: 50,
height: 50,
mime: 'image/jpeg',
}
beforeEach(() => { beforeEach(() => {
const mockedCreateResizedImage = const mockedCreateResizedImage = manipulateAsync as jest.Mock
ImageResizer.createResizedImage as jest.Mock mockedCreateResizedImage.mockResolvedValue({
mockedCreateResizedImage.mockResolvedValue(mockResizedImage) uri: 'file://resized-image.jpg',
...mockResizedImage,
})
}) })
afterEach(() => { afterEach(() => {
@@ -54,17 +58,17 @@ describe('downloadAndResize', () => {
'GET', 'GET',
'https://example.com/image.jpg', 'https://example.com/image.jpg',
) )
expect(ImageResizer.createResizedImage).toHaveBeenCalledWith(
'file://downloaded-image.jpg', // First time it gets called is to get dimensions
100, expect(manipulateAsync).toHaveBeenCalledWith(expect.any(String), [], {})
100, expect(manipulateAsync).toHaveBeenCalledWith(
'JPEG', expect.any(String),
100, [{resize: {height: opts.height, width: opts.width}}],
undefined, {format: SaveFormat.JPEG, compress: 1.0},
undefined,
undefined,
{mode: 'cover'},
) )
expect(deleteAsync).toHaveBeenCalledWith(expect.any(String), {
idempotent: true,
})
}) })
it('should return undefined for invalid URI', async () => { it('should return undefined for invalid URI', async () => {
@@ -82,46 +86,6 @@ describe('downloadAndResize', () => {
expect(result).toBeUndefined() expect(result).toBeUndefined()
}) })
it('should return undefined for unsupported file type', async () => {
const mockedFetch = RNFetchBlob.fetch as jest.Mock
mockedFetch.mockResolvedValueOnce({
path: jest.fn().mockReturnValue('file://downloaded-image'),
info: jest.fn().mockReturnValue({status: 200}),
flush: jest.fn(),
})
const opts: DownloadAndResizeOpts = {
uri: 'https://example.com/image',
width: 100,
height: 100,
maxSize: 500000,
mode: 'cover',
timeout: 10000,
}
const result = await downloadAndResize(opts)
expect(result).toEqual(mockResizedImage)
expect(RNFetchBlob.config).toHaveBeenCalledWith({
fileCache: true,
appendExt: 'jpeg',
})
expect(RNFetchBlob.fetch).toHaveBeenCalledWith(
'GET',
'https://example.com/image',
)
expect(ImageResizer.createResizedImage).toHaveBeenCalledWith(
'file://downloaded-image',
100,
100,
'JPEG',
100,
undefined,
undefined,
undefined,
{mode: 'cover'},
)
})
it('should return undefined for non-200 response', async () => { it('should return undefined for non-200 response', async () => {
const mockedFetch = RNFetchBlob.fetch as jest.Mock const mockedFetch = RNFetchBlob.fetch as jest.Mock
mockedFetch.mockResolvedValueOnce({ mockedFetch.mockResolvedValueOnce({
@@ -143,4 +107,44 @@ describe('downloadAndResize', () => {
expect(errorSpy).not.toHaveBeenCalled() expect(errorSpy).not.toHaveBeenCalled()
expect(result).toBeUndefined() expect(result).toBeUndefined()
}) })
it('should not downsize whenever dimensions are below the max dimensions', () => {
const initialDimensionsOne = {
width: 1200,
height: 1000,
}
const resizedDimensionsOne = getResizedDimensions(initialDimensionsOne)
const initialDimensionsTwo = {
width: 1000,
height: 1200,
}
const resizedDimensionsTwo = getResizedDimensions(initialDimensionsTwo)
expect(resizedDimensionsOne).toEqual(initialDimensionsOne)
expect(resizedDimensionsTwo).toEqual(initialDimensionsTwo)
})
it('should resize dimensions and maintain aspect ratio if they are above the max dimensons', () => {
const initialDimensionsOne = {
width: 3000,
height: 1500,
}
const resizedDimensionsOne = getResizedDimensions(initialDimensionsOne)
const initialDimensionsTwo = {
width: 2000,
height: 4000,
}
const resizedDimensionsTwo = getResizedDimensions(initialDimensionsTwo)
expect(resizedDimensionsOne).toEqual({
width: 2000,
height: 1000,
})
expect(resizedDimensionsTwo).toEqual({
width: 1000,
height: 2000,
})
})
}) })
+10 -2
View File
@@ -42,8 +42,16 @@ jest.mock('rn-fetch-blob', () => ({
fetch: jest.fn(), fetch: jest.fn(),
})) }))
jest.mock('@bam.tech/react-native-image-resizer', () => ({ jest.mock('expo-file-system', () => ({
createResizedImage: jest.fn(), getInfoAsync: jest.fn().mockResolvedValue({exists: true, size: 100}),
deleteAsync: jest.fn(),
}))
jest.mock('expo-image-manipulator', () => ({
manipulateAsync: jest.fn().mockResolvedValue({
uri: 'file://resized-image',
}),
SaveFormat: jest.requireActual('expo-image-manipulator').SaveFormat,
})) }))
jest.mock('@segment/analytics-react-native', () => ({ jest.mock('@segment/analytics-react-native', () => ({
-1
View File
@@ -54,7 +54,6 @@
}, },
"dependencies": { "dependencies": {
"@atproto/api": "^0.13.7", "@atproto/api": "^0.13.7",
"@bam.tech/react-native-image-resizer": "^3.0.4",
"@braintree/sanitize-url": "^6.0.2", "@braintree/sanitize-url": "^6.0.2",
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet", "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
"@emoji-mart/react": "^1.1.1", "@emoji-mart/react": "^1.1.1",
+58 -16
View File
@@ -6,18 +6,20 @@ import {
copyAsync, copyAsync,
deleteAsync, deleteAsync,
EncodingType, EncodingType,
getInfoAsync,
makeDirectoryAsync, makeDirectoryAsync,
StorageAccessFramework, StorageAccessFramework,
writeAsStringAsync, writeAsStringAsync,
} from 'expo-file-system' } from 'expo-file-system'
import {manipulateAsync, SaveFormat} from 'expo-image-manipulator'
import * as MediaLibrary from 'expo-media-library' import * as MediaLibrary from 'expo-media-library'
import * as Sharing from 'expo-sharing' import * as Sharing from 'expo-sharing'
import ImageResizer from '@bam.tech/react-native-image-resizer'
import {Buffer} from 'buffer' import {Buffer} from 'buffer'
import RNFetchBlob from 'rn-fetch-blob' import RNFetchBlob from 'rn-fetch-blob'
import {POST_IMG_MAX} from '#/lib/constants'
import {logger} from '#/logger' import {logger} from '#/logger'
import {isAndroid, isIOS} from 'platform/detection' import {isAndroid, isIOS} from '#/platform/detection'
import {Dimensions} from './types' import {Dimensions} from './types'
export async function compressIfNeeded( export async function compressIfNeeded(
@@ -165,29 +167,47 @@ interface DoResizeOpts {
} }
async function doResize(localUri: string, opts: DoResizeOpts): Promise<Image> { async function doResize(localUri: string, opts: DoResizeOpts): Promise<Image> {
// We need to get the dimensions of the image before we resize it. Previously, the library we used allowed us to enter
// a "max size", and it would do the "best possible size" calculation for us.
// Now instead, we have to supply the final dimensions to the manipulation function instead.
// 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,
})
for (let i = 0; i < 9; i++) { for (let i = 0; i < 9; i++) {
const quality = 100 - i * 10 // nearest 10th
const resizeRes = await ImageResizer.createResizedImage( const quality = Math.round((1 - 0.1 * i) * 10) / 10
const resizeRes = await manipulateAsync(
localUri, localUri,
opts.width, [{resize: newDimensions}],
opts.height, {
'JPEG', format: SaveFormat.JPEG,
quality, compress: quality,
undefined, },
undefined,
undefined,
{mode: opts.mode},
) )
if (resizeRes.size < opts.maxSize) {
const fileInfo = await getInfoAsync(resizeRes.uri)
if (!fileInfo.exists) {
throw new Error(
'The image manipulation library failed to create a new image.',
)
}
if (fileInfo.size < opts.maxSize) {
safeDeleteAsync(imageRes.uri)
return { return {
path: normalizePath(resizeRes.path), path: normalizePath(resizeRes.uri),
mime: 'image/jpeg', mime: 'image/jpeg',
size: resizeRes.size, size: fileInfo.size,
width: resizeRes.width, width: resizeRes.width,
height: resizeRes.height, height: resizeRes.height,
} }
} else { } else {
safeDeleteAsync(resizeRes.path) safeDeleteAsync(resizeRes.uri)
} }
} }
throw new Error( throw new Error(
@@ -311,3 +331,25 @@ async function withTempFile<T>(
safeDeleteAsync(tmpDirUri) safeDeleteAsync(tmpDirUri)
} }
} }
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),
}
}
+13 -13
View File
@@ -2,23 +2,18 @@ import {useEffect, useState} from 'react'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {logger} from '#/logger' import * as apilib from '#/lib/api/index'
import {createComposerImage} from '#/state/gallery' import {POST_IMG_MAX} from '#/lib/constants'
import {useFetchDid} from '#/state/queries/handle'
import {useGetPost} from '#/state/queries/post'
import {useAgent} from '#/state/session'
import * as apilib from 'lib/api/index'
import {POST_IMG_MAX} from 'lib/constants'
import { import {
EmbeddingDisabledError, EmbeddingDisabledError,
getFeedAsEmbed, getFeedAsEmbed,
getListAsEmbed, getListAsEmbed,
getPostAsQuote, getPostAsQuote,
getStarterPackAsEmbed, getStarterPackAsEmbed,
} from 'lib/link-meta/bsky' } from '#/lib/link-meta/bsky'
import {getLinkMeta} from 'lib/link-meta/link-meta' import {getLinkMeta} from '#/lib/link-meta/link-meta'
import {resolveShortLink} from 'lib/link-meta/resolve-short-link' import {resolveShortLink} from '#/lib/link-meta/resolve-short-link'
import {downloadAndResize} from 'lib/media/manip' import {downloadAndResize} from '#/lib/media/manip'
import { import {
isBskyCustomFeedUrl, isBskyCustomFeedUrl,
isBskyListUrl, isBskyListUrl,
@@ -26,8 +21,13 @@ import {
isBskyStarterPackUrl, isBskyStarterPackUrl,
isBskyStartUrl, isBskyStartUrl,
isShortLink, isShortLink,
} from 'lib/strings/url-helpers' } from '#/lib/strings/url-helpers'
import {ComposerOpts} from 'state/shell/composer' import {logger} from '#/logger'
import {createComposerImage} from '#/state/gallery'
import {useFetchDid} from '#/state/queries/handle'
import {useGetPost} from '#/state/queries/post'
import {useAgent} from '#/state/session'
import {ComposerOpts} from '#/state/shell/composer'
export function useExternalLinkFetch({ export function useExternalLinkFetch({
setQuote, setQuote,
-5
View File
@@ -2983,11 +2983,6 @@
"@babel/helper-validator-identifier" "^7.24.6" "@babel/helper-validator-identifier" "^7.24.6"
to-fast-properties "^2.0.0" to-fast-properties "^2.0.0"
"@bam.tech/react-native-image-resizer@^3.0.4":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@bam.tech/react-native-image-resizer/-/react-native-image-resizer-3.0.5.tgz#6661ba020de156268f73bdc92fbb93ef86f88a13"
integrity sha512-u5QGUQGGVZiVCJ786k9/kd7pPRZ6eYfJCYO18myVCH8FbVI7J8b5GT2Svjj2x808DlWeqfaZOOzxPqo27XYvrQ==
"@bcoe/v8-coverage@^0.2.3": "@bcoe/v8-coverage@^0.2.3":
version "0.2.3" version "0.2.3"
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"