about summary refs log tree commit diff
diff options
context:
space:
mode:
authorHailey <me@haileyok.com>2024-09-24 10:15:33 -0700
committerGitHub <noreply@github.com>2024-09-24 10:15:33 -0700
commitea43d20c61547523e34ae864ca4ddffdedd8dfb1 (patch)
tree876b0f207f19df040bd15281477eb90584ab5610
parentd2fae81b33ae0a73d0b9f87700365d60bc51f094 (diff)
downloadvoidsky-ea43d20c61547523e34ae864ca4ddffdedd8dfb1.tar.zst
Remove image resizer (#5464)
-rw-r--r--__tests__/lib/images.test.ts126
-rw-r--r--jest/jestSetup.js12
-rw-r--r--package.json1
-rw-r--r--src/lib/media/manip.ts74
-rw-r--r--src/view/com/composer/useExternalLinkFetch.ts26
-rw-r--r--yarn.lock5
6 files changed, 146 insertions, 98 deletions
diff --git a/__tests__/lib/images.test.ts b/__tests__/lib/images.test.ts
index 595f566c4..a5acad25f 100644
--- a/__tests__/lib/images.test.ts
+++ b/__tests__/lib/images.test.ts
@@ -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 {
   downloadAndResize,
   DownloadAndResizeOpts,
+  getResizedDimensions,
 } from '../../src/lib/media/manip'
 
+const mockResizedImage = {
+  path: 'file://resized-image.jpg',
+  size: 100,
+  width: 100,
+  height: 100,
+  mime: 'image/jpeg',
+}
+
 describe('downloadAndResize', () => {
   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(() => {
-    const mockedCreateResizedImage =
-      ImageResizer.createResizedImage as jest.Mock
-    mockedCreateResizedImage.mockResolvedValue(mockResizedImage)
+    const mockedCreateResizedImage = manipulateAsync as jest.Mock
+    mockedCreateResizedImage.mockResolvedValue({
+      uri: 'file://resized-image.jpg',
+      ...mockResizedImage,
+    })
   })
 
   afterEach(() => {
@@ -54,17 +58,17 @@ describe('downloadAndResize', () => {
       'GET',
       'https://example.com/image.jpg',
     )
-    expect(ImageResizer.createResizedImage).toHaveBeenCalledWith(
-      'file://downloaded-image.jpg',
-      100,
-      100,
-      'JPEG',
-      100,
-      undefined,
-      undefined,
-      undefined,
-      {mode: 'cover'},
+
+    // First time it gets called is to get dimensions
+    expect(manipulateAsync).toHaveBeenCalledWith(expect.any(String), [], {})
+    expect(manipulateAsync).toHaveBeenCalledWith(
+      expect.any(String),
+      [{resize: {height: opts.height, width: opts.width}}],
+      {format: SaveFormat.JPEG, compress: 1.0},
     )
+    expect(deleteAsync).toHaveBeenCalledWith(expect.any(String), {
+      idempotent: true,
+    })
   })
 
   it('should return undefined for invalid URI', async () => {
@@ -82,11 +86,11 @@ describe('downloadAndResize', () => {
     expect(result).toBeUndefined()
   })
 
-  it('should return undefined for unsupported file type', async () => {
+  it('should return undefined for non-200 response', async () => {
     const mockedFetch = RNFetchBlob.fetch as jest.Mock
     mockedFetch.mockResolvedValueOnce({
       path: jest.fn().mockReturnValue('file://downloaded-image'),
-      info: jest.fn().mockReturnValue({status: 200}),
+      info: jest.fn().mockReturnValue({status: 400}),
       flush: jest.fn(),
     })
 
@@ -100,47 +104,47 @@ describe('downloadAndResize', () => {
     }
 
     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'},
-    )
+    expect(errorSpy).not.toHaveBeenCalled()
+    expect(result).toBeUndefined()
   })
 
-  it('should return undefined for non-200 response', async () => {
-    const mockedFetch = RNFetchBlob.fetch as jest.Mock
-    mockedFetch.mockResolvedValueOnce({
-      path: jest.fn().mockReturnValue('file://downloaded-image'),
-      info: jest.fn().mockReturnValue({status: 400}),
-      flush: jest.fn(),
-    })
+  it('should not downsize whenever dimensions are below the max dimensions', () => {
+    const initialDimensionsOne = {
+      width: 1200,
+      height: 1000,
+    }
+    const resizedDimensionsOne = getResizedDimensions(initialDimensionsOne)
 
-    const opts: DownloadAndResizeOpts = {
-      uri: 'https://example.com/image',
-      width: 100,
-      height: 100,
-      maxSize: 500000,
-      mode: 'cover',
-      timeout: 10000,
+    const initialDimensionsTwo = {
+      width: 1000,
+      height: 1200,
     }
+    const resizedDimensionsTwo = getResizedDimensions(initialDimensionsTwo)
 
-    const result = await downloadAndResize(opts)
-    expect(errorSpy).not.toHaveBeenCalled()
-    expect(result).toBeUndefined()
+    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,
+    })
   })
 })
diff --git a/jest/jestSetup.js b/jest/jestSetup.js
index a68c1dc4b..50a33589e 100644
--- a/jest/jestSetup.js
+++ b/jest/jestSetup.js
@@ -42,8 +42,16 @@ jest.mock('rn-fetch-blob', () => ({
   fetch: jest.fn(),
 }))
 
-jest.mock('@bam.tech/react-native-image-resizer', () => ({
-  createResizedImage: jest.fn(),
+jest.mock('expo-file-system', () => ({
+  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', () => ({
diff --git a/package.json b/package.json
index 5b2369d2a..4b3486545 100644
--- a/package.json
+++ b/package.json
@@ -54,7 +54,6 @@
   },
   "dependencies": {
     "@atproto/api": "^0.13.7",
-    "@bam.tech/react-native-image-resizer": "^3.0.4",
     "@braintree/sanitize-url": "^6.0.2",
     "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
     "@emoji-mart/react": "^1.1.1",
diff --git a/src/lib/media/manip.ts b/src/lib/media/manip.ts
index 3f01e98c5..e75f13755 100644
--- a/src/lib/media/manip.ts
+++ b/src/lib/media/manip.ts
@@ -6,18 +6,20 @@ import {
   copyAsync,
   deleteAsync,
   EncodingType,
+  getInfoAsync,
   makeDirectoryAsync,
   StorageAccessFramework,
   writeAsStringAsync,
 } from 'expo-file-system'
+import {manipulateAsync, SaveFormat} from 'expo-image-manipulator'
 import * as MediaLibrary from 'expo-media-library'
 import * as Sharing from 'expo-sharing'
-import ImageResizer from '@bam.tech/react-native-image-resizer'
 import {Buffer} from 'buffer'
 import RNFetchBlob from 'rn-fetch-blob'
 
+import {POST_IMG_MAX} from '#/lib/constants'
 import {logger} from '#/logger'
-import {isAndroid, isIOS} from 'platform/detection'
+import {isAndroid, isIOS} from '#/platform/detection'
 import {Dimensions} from './types'
 
 export async function compressIfNeeded(
@@ -165,29 +167,47 @@ interface DoResizeOpts {
 }
 
 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++) {
-    const quality = 100 - i * 10
-    const resizeRes = await ImageResizer.createResizedImage(
+    // nearest 10th
+    const quality = Math.round((1 - 0.1 * i) * 10) / 10
+    const resizeRes = await manipulateAsync(
       localUri,
-      opts.width,
-      opts.height,
-      'JPEG',
-      quality,
-      undefined,
-      undefined,
-      undefined,
-      {mode: opts.mode},
+      [{resize: newDimensions}],
+      {
+        format: SaveFormat.JPEG,
+        compress: quality,
+      },
     )
-    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 {
-        path: normalizePath(resizeRes.path),
+        path: normalizePath(resizeRes.uri),
         mime: 'image/jpeg',
-        size: resizeRes.size,
+        size: fileInfo.size,
         width: resizeRes.width,
         height: resizeRes.height,
       }
     } else {
-      safeDeleteAsync(resizeRes.path)
+      safeDeleteAsync(resizeRes.uri)
     }
   }
   throw new Error(
@@ -311,3 +331,25 @@ async function withTempFile<T>(
     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),
+  }
+}
diff --git a/src/view/com/composer/useExternalLinkFetch.ts b/src/view/com/composer/useExternalLinkFetch.ts
index 1a36b5034..60afadefe 100644
--- a/src/view/com/composer/useExternalLinkFetch.ts
+++ b/src/view/com/composer/useExternalLinkFetch.ts
@@ -2,23 +2,18 @@ import {useEffect, useState} from 'react'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
-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 * as apilib from 'lib/api/index'
-import {POST_IMG_MAX} from 'lib/constants'
+import * as apilib from '#/lib/api/index'
+import {POST_IMG_MAX} from '#/lib/constants'
 import {
   EmbeddingDisabledError,
   getFeedAsEmbed,
   getListAsEmbed,
   getPostAsQuote,
   getStarterPackAsEmbed,
-} from 'lib/link-meta/bsky'
-import {getLinkMeta} from 'lib/link-meta/link-meta'
-import {resolveShortLink} from 'lib/link-meta/resolve-short-link'
-import {downloadAndResize} from 'lib/media/manip'
+} from '#/lib/link-meta/bsky'
+import {getLinkMeta} from '#/lib/link-meta/link-meta'
+import {resolveShortLink} from '#/lib/link-meta/resolve-short-link'
+import {downloadAndResize} from '#/lib/media/manip'
 import {
   isBskyCustomFeedUrl,
   isBskyListUrl,
@@ -26,8 +21,13 @@ import {
   isBskyStarterPackUrl,
   isBskyStartUrl,
   isShortLink,
-} from 'lib/strings/url-helpers'
-import {ComposerOpts} from 'state/shell/composer'
+} from '#/lib/strings/url-helpers'
+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({
   setQuote,
diff --git a/yarn.lock b/yarn.lock
index 225f109f7..17fe86237 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2983,11 +2983,6 @@
     "@babel/helper-validator-identifier" "^7.24.6"
     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":
   version "0.2.3"
   resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"