about summary refs log tree commit diff
path: root/src/state/models
diff options
context:
space:
mode:
Diffstat (limited to 'src/state/models')
-rw-r--r--src/state/models/cache/image-sizes.ts1
-rw-r--r--src/state/models/media/gallery.ts24
-rw-r--r--src/state/models/media/image.ts177
3 files changed, 114 insertions, 88 deletions
diff --git a/src/state/models/cache/image-sizes.ts b/src/state/models/cache/image-sizes.ts
index bbfb9612b..c30a68f4d 100644
--- a/src/state/models/cache/image-sizes.ts
+++ b/src/state/models/cache/image-sizes.ts
@@ -16,6 +16,7 @@ export class ImageSizesCache {
     if (Dimensions) {
       return Dimensions
     }
+
     const prom =
       this.activeRequests.get(uri) ||
       new Promise<Dimensions>(resolve => {
diff --git a/src/state/models/media/gallery.ts b/src/state/models/media/gallery.ts
index 67f8d2ea1..52ef8f375 100644
--- a/src/state/models/media/gallery.ts
+++ b/src/state/models/media/gallery.ts
@@ -4,7 +4,6 @@ import {ImageModel} from './image'
 import {Image as RNImage} from 'react-native-image-crop-picker'
 import {openPicker} from 'lib/media/picker'
 import {getImageDim} from 'lib/media/manip'
-import {getDataUriSize} from 'lib/media/util'
 import {isNative} from 'platform/detection'
 
 export class GalleryModel {
@@ -24,13 +23,7 @@ export class GalleryModel {
     return this.images.length
   }
 
-  get paths() {
-    return this.images.map(image =>
-      image.compressed === undefined ? image.path : image.compressed.path,
-    )
-  }
-
-  async add(image_: RNImage) {
+  async add(image_: Omit<RNImage, 'size'>) {
     if (this.size >= 4) {
       return
     }
@@ -39,15 +32,9 @@ export class GalleryModel {
     if (!this.images.some(i => i.path === image_.path)) {
       const image = new ImageModel(this.rootStore, image_)
 
-      if (!isNative) {
-        await image.manipulate({})
-      } else {
-        await image.compress()
-      }
-
-      runInAction(() => {
-        this.images.push(image)
-      })
+      // Initial resize
+      image.manipulate({})
+      this.images.push(image)
     }
   }
 
@@ -70,11 +57,10 @@ export class GalleryModel {
 
     const {width, height} = await getImageDim(uri)
 
-    const image: RNImage = {
+    const image = {
       path: uri,
       height,
       width,
-      size: getDataUriSize(uri),
       mime: 'image/jpeg',
     }
 
diff --git a/src/state/models/media/image.ts b/src/state/models/media/image.ts
index 6edf88d9d..e524c49de 100644
--- a/src/state/models/media/image.ts
+++ b/src/state/models/media/image.ts
@@ -3,14 +3,11 @@ import {RootStoreModel} from 'state/index'
 import {makeAutoObservable, runInAction} from 'mobx'
 import {POST_IMG_MAX} from 'lib/constants'
 import * as ImageManipulator from 'expo-image-manipulator'
-import {getDataUriSize, scaleDownDimensions} from 'lib/media/util'
+import {getDataUriSize} from 'lib/media/util'
 import {openCropper} from 'lib/media/picker'
 import {ActionCrop, FlipType, SaveFormat} from 'expo-image-manipulator'
 import {Position} from 'react-avatar-editor'
-import {compressAndResizeImageForPost} from 'lib/media/manip'
-
-// TODO: EXIF embed
-// Cases to consider: ExternalEmbed
+import {Dimensions} from 'lib/media/types'
 
 export interface ImageManipulationAttributes {
   aspectRatio?: '4:3' | '1:1' | '3:4' | 'None'
@@ -21,17 +18,16 @@ export interface ImageManipulationAttributes {
   flipVertical?: boolean
 }
 
-export class ImageModel implements RNImage {
+const MAX_IMAGE_SIZE_IN_BYTES = 976560
+
+export class ImageModel implements Omit<RNImage, 'size'> {
   path: string
   mime = 'image/jpeg'
   width: number
   height: number
-  size: number
   altText = ''
   cropped?: RNImage = undefined
   compressed?: RNImage = undefined
-  scaledWidth: number = POST_IMG_MAX.width
-  scaledHeight: number = POST_IMG_MAX.height
 
   // Web manipulation
   prev?: RNImage
@@ -44,7 +40,7 @@ export class ImageModel implements RNImage {
   }
   prevAttributes: ImageManipulationAttributes = {}
 
-  constructor(public rootStore: RootStoreModel, image: RNImage) {
+  constructor(public rootStore: RootStoreModel, image: Omit<RNImage, 'size'>) {
     makeAutoObservable(this, {
       rootStore: false,
     })
@@ -52,19 +48,8 @@ export class ImageModel implements RNImage {
     this.path = image.path
     this.width = image.width
     this.height = image.height
-    this.size = image.size
-    this.calcScaledDimensions()
   }
 
-  // TODO: Revisit compression factor due to updated sizing with zoom
-  // get compressionFactor() {
-  //   const MAX_IMAGE_SIZE_IN_BYTES = 976560
-
-  //   return this.size < MAX_IMAGE_SIZE_IN_BYTES
-  //     ? 1
-  //     : MAX_IMAGE_SIZE_IN_BYTES / this.size
-  // }
-
   setRatio(aspectRatio: ImageManipulationAttributes['aspectRatio']) {
     this.attributes.aspectRatio = aspectRatio
   }
@@ -93,8 +78,24 @@ export class ImageModel implements RNImage {
     }
   }
 
-  getDisplayDimensions(
-    as: ImageManipulationAttributes['aspectRatio'] = '1:1',
+  getUploadDimensions(
+    dimensions: Dimensions,
+    maxDimensions: Dimensions = POST_IMG_MAX,
+    as: ImageManipulationAttributes['aspectRatio'] = 'None',
+  ) {
+    const {width, height} = dimensions
+    const {width: maxWidth, height: maxHeight} = maxDimensions
+
+    return width < maxWidth && height < maxHeight
+      ? {
+          width,
+          height,
+        }
+      : this.getResizedDimensions(as, POST_IMG_MAX.width)
+  }
+
+  getResizedDimensions(
+    as: ImageManipulationAttributes['aspectRatio'] = 'None',
     maxSide: number,
   ) {
     const ratioMultiplier = this.ratioMultipliers[as]
@@ -119,59 +120,70 @@ export class ImageModel implements RNImage {
     }
   }
 
-  calcScaledDimensions() {
-    const {width, height} = scaleDownDimensions(
-      {width: this.width, height: this.height},
-      POST_IMG_MAX,
-    )
-    this.scaledWidth = width
-    this.scaledHeight = height
-  }
-
   async setAltText(altText: string) {
     this.altText = altText
   }
 
-  // Only for mobile
+  // Only compress prior to upload
+  async compress() {
+    for (let i = 10; i > 0; i--) {
+      // Float precision
+      const factor = Math.round(i) / 10
+      const compressed = await ImageManipulator.manipulateAsync(
+        this.cropped?.path ?? this.path,
+        undefined,
+        {
+          compress: factor,
+          base64: true,
+          format: SaveFormat.JPEG,
+        },
+      )
+
+      if (compressed.base64 !== undefined) {
+        const size = getDataUriSize(compressed.base64)
+
+        if (size < MAX_IMAGE_SIZE_IN_BYTES) {
+          runInAction(() => {
+            this.compressed = {
+              mime: 'image/jpeg',
+              path: compressed.uri,
+              size,
+              ...compressed,
+            }
+          })
+          return
+        }
+      }
+    }
+
+    // Compression fails when removing redundant information is not possible.
+    // This can be tested with images that have high variance in noise.
+    throw new Error('Failed to compress image')
+  }
+
+  // Mobile
   async crop() {
     try {
+      // openCropper requires an output width and height hence
+      // getting upload dimensions before cropping is necessary.
+      const {width, height} = this.getUploadDimensions({
+        width: this.width,
+        height: this.height,
+      })
+
       const cropped = await openCropper(this.rootStore, {
         mediaType: 'photo',
         path: this.path,
         freeStyleCropEnabled: true,
-        width: this.scaledWidth,
-        height: this.scaledHeight,
-      })
-      runInAction(() => {
-        this.cropped = cropped
-        this.compress()
-      })
-    } catch (err) {
-      this.rootStore.log.error('Failed to crop photo', err)
-    }
-  }
-
-  async compress() {
-    try {
-      const {width, height} = scaleDownDimensions(
-        this.cropped
-          ? {width: this.cropped.width, height: this.cropped.height}
-          : {width: this.width, height: this.height},
-        POST_IMG_MAX,
-      )
-
-      // TODO: Revisit this - currently iOS uses this as well
-      const compressed = await compressAndResizeImageForPost({
-        ...(this.cropped === undefined ? this : this.cropped),
         width,
         height,
       })
 
       runInAction(() => {
-        this.compressed = compressed
+        this.cropped = cropped
       })
     } catch (err) {
-      this.rootStore.log.error('Failed to compress photo', err)
+      this.rootStore.log.error('Failed to crop photo', err)
     }
   }
 
@@ -181,6 +193,9 @@ export class ImageModel implements RNImage {
       crop?: ActionCrop['crop']
     } & ImageManipulationAttributes,
   ) {
+    let uploadWidth: number | undefined
+    let uploadHeight: number | undefined
+
     const {aspectRatio, crop, position, scale} = attributes
     const modifiers = []
 
@@ -197,14 +212,34 @@ export class ImageModel implements RNImage {
     }
 
     if (crop !== undefined) {
+      const croppedHeight = crop.height * this.height
+      const croppedWidth = crop.width * this.width
       modifiers.push({
         crop: {
           originX: crop.originX * this.width,
           originY: crop.originY * this.height,
-          height: crop.height * this.height,
-          width: crop.width * this.width,
+          height: croppedHeight,
+          width: croppedWidth,
         },
       })
+
+      const uploadDimensions = this.getUploadDimensions(
+        {width: croppedWidth, height: croppedHeight},
+        POST_IMG_MAX,
+        aspectRatio,
+      )
+
+      uploadWidth = uploadDimensions.width
+      uploadHeight = uploadDimensions.height
+    } else {
+      const uploadDimensions = this.getUploadDimensions(
+        {width: this.width, height: this.height},
+        POST_IMG_MAX,
+        aspectRatio,
+      )
+
+      uploadWidth = uploadDimensions.width
+      uploadHeight = uploadDimensions.height
     }
 
     if (scale !== undefined) {
@@ -222,36 +257,40 @@ export class ImageModel implements RNImage {
     const ratioMultiplier =
       this.ratioMultipliers[this.attributes.aspectRatio ?? '1:1']
 
-    const MAX_SIDE = 2000
-
     const result = await ImageManipulator.manipulateAsync(
       this.path,
       [
         ...modifiers,
-        {resize: ratioMultiplier > 1 ? {width: MAX_SIDE} : {height: MAX_SIDE}},
+        {
+          resize:
+            ratioMultiplier > 1 ? {width: uploadWidth} : {height: uploadHeight},
+        },
       ],
       {
-        compress: 0.9,
+        base64: true,
         format: SaveFormat.JPEG,
       },
     )
 
     runInAction(() => {
-      this.compressed = {
+      this.cropped = {
         mime: 'image/jpeg',
         path: result.uri,
-        size: getDataUriSize(result.uri),
+        size:
+          result.base64 !== undefined
+            ? getDataUriSize(result.base64)
+            : MAX_IMAGE_SIZE_IN_BYTES + 999, // shouldn't hit this unless manipulation fails
         ...result,
       }
     })
   }
 
-  resetCompressed() {
+  resetCropped() {
     this.manipulate({})
   }
 
   previous() {
-    this.compressed = this.prev
+    this.cropped = this.prev
     this.attributes = this.prevAttributes
   }
 }