about summary refs log tree commit diff
path: root/src/state/models/media/image.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/state/models/media/image.ts')
-rw-r--r--src/state/models/media/image.ts177
1 files changed, 170 insertions, 7 deletions
diff --git a/src/state/models/media/image.ts b/src/state/models/media/image.ts
index dcd47665c..ff464a5a9 100644
--- a/src/state/models/media/image.ts
+++ b/src/state/models/media/image.ts
@@ -1,13 +1,26 @@
 import {Image as RNImage} from 'react-native-image-crop-picker'
 import {RootStoreModel} from 'state/index'
-import {compressAndResizeImageForPost} from 'lib/media/manip'
 import {makeAutoObservable, runInAction} from 'mobx'
-import {openCropper} from 'lib/media/picker'
 import {POST_IMG_MAX} from 'lib/constants'
-import {scaleDownDimensions} from 'lib/media/util'
+import * as ImageManipulator from 'expo-image-manipulator'
+import {getDataUriSize, scaleDownDimensions} 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
+
+export interface ImageManipulationAttributes {
+  rotate?: number
+  scale?: number
+  position?: Position
+  flipHorizontal?: boolean
+  flipVertical?: boolean
+  aspectRatio?: '4:3' | '1:1' | '3:4' | 'None'
+}
+
 export class ImageModel implements RNImage {
   path: string
   mime = 'image/jpeg'
@@ -20,6 +33,17 @@ export class ImageModel implements RNImage {
   scaledWidth: number = POST_IMG_MAX.width
   scaledHeight: number = POST_IMG_MAX.height
 
+  // Web manipulation
+  aspectRatio?: ImageManipulationAttributes['aspectRatio']
+  position?: Position = undefined
+  prev?: RNImage = undefined
+  rotation?: number = 0
+  scale?: number = 1
+  flipHorizontal?: boolean = false
+  flipVertical?: boolean = false
+
+  prevAttributes: ImageManipulationAttributes = {}
+
   constructor(public rootStore: RootStoreModel, image: RNImage) {
     makeAutoObservable(this, {
       rootStore: false,
@@ -32,12 +56,55 @@ export class ImageModel implements RNImage {
     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
+  // }
+
+  get ratioMultipliers() {
+    return {
+      '4:3': 4 / 3,
+      '1:1': 1,
+      '3:4': 3 / 4,
+      None: this.width / this.height,
+    }
+  }
+
+  getDisplayDimensions(
+    as: ImageManipulationAttributes['aspectRatio'] = '1:1',
+    maxSide: number,
+  ) {
+    const ratioMultiplier = this.ratioMultipliers[as]
+
+    if (ratioMultiplier === 1) {
+      return {
+        height: maxSide,
+        width: maxSide,
+      }
+    }
+
+    if (ratioMultiplier < 1) {
+      return {
+        width: maxSide * ratioMultiplier,
+        height: maxSide,
+      }
+    }
+
+    return {
+      width: maxSide,
+      height: maxSide / ratioMultiplier,
+    }
+  }
+
   calcScaledDimensions() {
     const {width, height} = scaleDownDimensions(
       {width: this.width, height: this.height},
       POST_IMG_MAX,
     )
-
     this.scaledWidth = width
     this.scaledHeight = height
   }
@@ -46,6 +113,7 @@ export class ImageModel implements RNImage {
     this.altText = altText
   }
 
+  // Only for mobile
   async crop() {
     try {
       const cropped = await openCropper(this.rootStore, {
@@ -55,15 +123,13 @@ export class ImageModel implements RNImage {
         width: this.scaledWidth,
         height: this.scaledHeight,
       })
-
       runInAction(() => {
         this.cropped = cropped
+        this.compress()
       })
     } catch (err) {
       this.rootStore.log.error('Failed to crop photo', err)
     }
-
-    this.compress()
   }
 
   async compress() {
@@ -74,6 +140,8 @@ export class ImageModel implements RNImage {
           : {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,
@@ -87,4 +155,99 @@ export class ImageModel implements RNImage {
       this.rootStore.log.error('Failed to compress photo', err)
     }
   }
+
+  // Web manipulation
+  async manipulate(
+    attributes: {
+      crop?: ActionCrop['crop']
+    } & ImageManipulationAttributes,
+  ) {
+    const {aspectRatio, crop, flipHorizontal, flipVertical, rotate, scale} =
+      attributes
+    const modifiers = []
+
+    if (flipHorizontal !== undefined) {
+      this.flipHorizontal = flipHorizontal
+    }
+
+    if (flipVertical !== undefined) {
+      this.flipVertical = flipVertical
+    }
+
+    if (this.flipHorizontal) {
+      modifiers.push({flip: FlipType.Horizontal})
+    }
+
+    if (this.flipVertical) {
+      modifiers.push({flip: FlipType.Vertical})
+    }
+
+    // TODO: Fix rotation -- currently not functional
+    if (rotate !== undefined) {
+      this.rotation = rotate
+    }
+
+    if (this.rotation !== undefined) {
+      modifiers.push({rotate: this.rotation})
+    }
+
+    if (crop !== undefined) {
+      modifiers.push({
+        crop: {
+          originX: crop.originX * this.width,
+          originY: crop.originY * this.height,
+          height: crop.height * this.height,
+          width: crop.width * this.width,
+        },
+      })
+    }
+
+    if (scale !== undefined) {
+      this.scale = scale
+    }
+
+    if (aspectRatio !== undefined) {
+      this.aspectRatio = aspectRatio
+    }
+
+    const ratioMultiplier = this.ratioMultipliers[this.aspectRatio ?? '1:1']
+
+    // TODO: Ollie - should support up to 2000 but smaller images that scale
+    // up need an updated compression factor calculation. Use 1000 for now.
+    const MAX_SIDE = 1000
+
+    const result = await ImageManipulator.manipulateAsync(
+      this.path,
+      [
+        ...modifiers,
+        {resize: ratioMultiplier > 1 ? {width: MAX_SIDE} : {height: MAX_SIDE}},
+      ],
+      {
+        compress: 0.7, // TODO: revisit compression calculation
+        format: SaveFormat.JPEG,
+      },
+    )
+
+    runInAction(() => {
+      this.compressed = {
+        mime: 'image/jpeg',
+        path: result.uri,
+        size: getDataUriSize(result.uri),
+        ...result,
+      }
+    })
+  }
+
+  previous() {
+    this.compressed = this.prev
+
+    const {flipHorizontal, flipVertical, rotate, position, scale} =
+      this.prevAttributes
+
+    this.scale = scale
+    this.rotation = rotate
+    this.flipHorizontal = flipHorizontal
+    this.flipVertical = flipVertical
+    this.position = position
+  }
 }