Remove image resizer (#5464)
This commit is contained in:
@@ -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
@@ -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', () => ({
|
||||||
|
|||||||
@@ -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
@@ -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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user