From d87c232660f215608c26280f4cfbd5af3948f9d1 Mon Sep 17 00:00:00 2001
From: Paul Frazee <pfrazee@gmail.com>
Date: Wed, 27 Sep 2023 09:08:21 -0700
Subject: [PATCH] Improve image cropping on android and introduce aspect ratio
 field (#1525)

* Fix image cropping on android

* Store and use aspect ratio field in post images (close #1392)
---
 src/lib/api/index.ts                        |  2 ++
 src/state/models/media/image.ts             | 13 +++++++++----
 src/view/com/util/images/AutoSizedImage.tsx |  4 +++-
 src/view/com/util/post-embeds/index.tsx     |  9 +++++++--
 4 files changed, 21 insertions(+), 7 deletions(-)

diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts
index 4ecd3204..8a9389a1 100644
--- a/src/lib/api/index.ts
+++ b/src/lib/api/index.ts
@@ -133,10 +133,12 @@ export async function post(store: RootStoreModel, opts: PostOpts) {
       opts.onStateChange?.(`Uploading image #${images.length + 1}...`)
       await image.compress()
       const path = image.compressed?.path ?? image.path
+      const {width, height} = image.compressed || image
       const res = await uploadBlob(store, path, 'image/jpeg')
       images.push({
         image: res.data.blob,
         alt: image.altText ?? '',
+        aspectRatio: {width, height},
       })
     }
 
diff --git a/src/state/models/media/image.ts b/src/state/models/media/image.ts
index 844ecb77..10aef0ff 100644
--- a/src/state/models/media/image.ts
+++ b/src/state/models/media/image.ts
@@ -8,6 +8,7 @@ import {openCropper} from 'lib/media/picker'
 import {ActionCrop, FlipType, SaveFormat} from 'expo-image-manipulator'
 import {Position} from 'react-avatar-editor'
 import {Dimensions} from 'lib/media/types'
+import {isIOS} from 'platform/detection'
 
 export interface ImageManipulationAttributes {
   aspectRatio?: '4:3' | '1:1' | '3:4' | 'None'
@@ -164,8 +165,13 @@ export class ImageModel implements Omit<RNImage, 'size'> {
   // Mobile
   async crop() {
     try {
-      // openCropper requires an output width and height hence
-      // getting upload dimensions before cropping is necessary.
+      // NOTE
+      // on ios, react-native-image-cropper gives really bad quality
+      // without specifying width and height. on android, however, the
+      // crop stretches incorrectly if you do specify it. these are
+      // both separate bugs in the library. we deal with that by
+      // providing width & height for ios only
+      // -prf
       const {width, height} = this.getUploadDimensions({
         width: this.width,
         height: this.height,
@@ -175,8 +181,7 @@ export class ImageModel implements Omit<RNImage, 'size'> {
         mediaType: 'photo',
         path: this.path,
         freeStyleCropEnabled: true,
-        width,
-        height,
+        ...(isIOS ? {width, height} : {}),
       })
 
       runInAction(() => {
diff --git a/src/view/com/util/images/AutoSizedImage.tsx b/src/view/com/util/images/AutoSizedImage.tsx
index da2f7ab4..035e29c2 100644
--- a/src/view/com/util/images/AutoSizedImage.tsx
+++ b/src/view/com/util/images/AutoSizedImage.tsx
@@ -11,6 +11,7 @@ const MAX_ASPECT_RATIO = 5 // 5/1
 interface Props {
   alt?: string
   uri: string
+  dimensionsHint?: Dimensions
   onPress?: () => void
   onLongPress?: () => void
   onPressIn?: () => void
@@ -21,6 +22,7 @@ interface Props {
 export function AutoSizedImage({
   alt,
   uri,
+  dimensionsHint,
   onPress,
   onLongPress,
   onPressIn,
@@ -29,7 +31,7 @@ export function AutoSizedImage({
 }: Props) {
   const store = useStores()
   const [dim, setDim] = React.useState<Dimensions | undefined>(
-    store.imageSizes.get(uri),
+    dimensionsHint || store.imageSizes.get(uri),
   )
   const [aspectRatio, setAspectRatio] = React.useState<number>(
     dim ? calc(dim) : 1,
diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx
index ce6da4a1..2d79eed8 100644
--- a/src/view/com/util/post-embeds/index.tsx
+++ b/src/view/com/util/post-embeds/index.tsx
@@ -93,7 +93,11 @@ export function PostEmbeds({
     const {images} = embed
 
     if (images.length > 0) {
-      const items = embed.images.map(img => ({uri: img.fullsize, alt: img.alt}))
+      const items = embed.images.map(img => ({
+        uri: img.fullsize,
+        alt: img.alt,
+        aspectRatio: img.aspectRatio,
+      }))
       const openLightbox = (index: number) => {
         store.shell.openLightbox(new ImagesLightbox(items, index))
       }
@@ -104,12 +108,13 @@ export function PostEmbeds({
       }
 
       if (images.length === 1) {
-        const {alt, thumb} = images[0]
+        const {alt, thumb, aspectRatio} = images[0]
         return (
           <View style={[styles.imagesContainer, style]}>
             <AutoSizedImage
               alt={alt}
               uri={thumb}
+              dimensionsHint={aspectRatio}
               onPress={() => openLightbox(0)}
               onPressIn={() => onPressIn(0)}
               style={[