about summary refs log tree commit diff
path: root/src/lib
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib')
-rw-r--r--src/lib/api/index.ts2
-rw-r--r--src/lib/constants.ts8
-rw-r--r--src/lib/media/manip.ts198
-rw-r--r--src/lib/media/manip.web.ts192
-rw-r--r--src/lib/media/picker.e2e.tsx106
-rw-r--r--src/lib/media/picker.tsx87
-rw-r--r--src/lib/media/picker.web.tsx69
-rw-r--r--src/lib/media/types.ts28
-rw-r--r--src/lib/media/util.ts40
9 files changed, 322 insertions, 408 deletions
diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts
index 457921d69..1b12f29c5 100644
--- a/src/lib/api/index.ts
+++ b/src/lib/api/index.ts
@@ -10,8 +10,8 @@ import {
 import {AtUri} from '@atproto/api'
 import {RootStoreModel} from 'state/models/root-store'
 import {isNetworkError} from 'lib/strings/errors'
+import {Image} from 'lib/media/types'
 import {LinkMeta} from '../link-meta/link-meta'
-import {Image} from '../media/manip'
 import {isWeb} from 'platform/detection'
 
 export interface ExternalEmbedDraft {
diff --git a/src/lib/constants.ts b/src/lib/constants.ts
index 0cde9b014..d49d8c75c 100644
--- a/src/lib/constants.ts
+++ b/src/lib/constants.ts
@@ -161,6 +161,8 @@ export function SUGGESTED_FOLLOWS(serviceUrl: string) {
   }
 }
 
-export const POST_IMG_MAX_WIDTH = 2000
-export const POST_IMG_MAX_HEIGHT = 2000
-export const POST_IMG_MAX_SIZE = 1000000
+export const POST_IMG_MAX = {
+  width: 2000,
+  height: 2000,
+  size: 1000000,
+}
diff --git a/src/lib/media/manip.ts b/src/lib/media/manip.ts
index 6ff8b691c..f77b861e2 100644
--- a/src/lib/media/manip.ts
+++ b/src/lib/media/manip.ts
@@ -1,13 +1,77 @@
 import RNFetchBlob from 'rn-fetch-blob'
 import ImageResizer from '@bam.tech/react-native-image-resizer'
 import {Image as RNImage, Share} from 'react-native'
+import {Image} from 'react-native-image-crop-picker'
 import RNFS from 'react-native-fs'
 import uuid from 'react-native-uuid'
 import * as Toast from 'view/com/util/Toast'
+import {Dimensions} from './types'
+import {POST_IMG_MAX} from 'lib/constants'
+import {isAndroid} from 'platform/detection'
 
-export interface Dim {
-  width: number
-  height: number
+export async function compressAndResizeImageForPost(
+  image: Image,
+): Promise<Image> {
+  const uri = `file://${image.path}`
+  let resized: Omit<Image, 'mime'>
+
+  for (let i = 0; i < 9; i++) {
+    const quality = 100 - i * 10
+
+    try {
+      resized = await ImageResizer.createResizedImage(
+        uri,
+        POST_IMG_MAX.width,
+        POST_IMG_MAX.height,
+        'JPEG',
+        quality,
+        undefined,
+        undefined,
+        undefined,
+        {mode: 'cover'},
+      )
+    } catch (err) {
+      throw new Error(`Failed to resize: ${err}`)
+    }
+
+    if (resized.size < POST_IMG_MAX.size) {
+      const path = await moveToPermanentPath(resized.path)
+
+      return {
+        path,
+        mime: 'image/jpeg',
+        size: resized.size,
+        height: resized.height,
+        width: resized.width,
+      }
+    }
+  }
+
+  throw new Error(
+    `This image is too big! We couldn't compress it down to ${POST_IMG_MAX.size} bytes`,
+  )
+}
+
+export async function compressIfNeeded(
+  img: Image,
+  maxSize: number = 1000000,
+): Promise<Image> {
+  const origUri = `file://${img.path}`
+  if (img.size < maxSize) {
+    return img
+  }
+  const resizedImage = await doResize(origUri, {
+    width: img.width,
+    height: img.height,
+    mode: 'stretch',
+    maxSize,
+  })
+  const finalImageMovedPath = await moveToPermanentPath(resizedImage.path)
+  const finalImg = {
+    ...resizedImage,
+    path: finalImageMovedPath,
+  }
+  return finalImg
 }
 
 export interface DownloadAndResizeOpts {
@@ -19,14 +83,6 @@ export interface DownloadAndResizeOpts {
   timeout: number
 }
 
-export interface Image {
-  path: string
-  mime: string
-  size: number
-  width: number
-  height: number
-}
-
 export async function downloadAndResize(opts: DownloadAndResizeOpts) {
   let appendExt = 'jpeg'
   try {
@@ -55,7 +111,7 @@ export async function downloadAndResize(opts: DownloadAndResizeOpts) {
       localUri = `file://${localUri}`
     }
 
-    return await resize(localUri, opts)
+    return await doResize(localUri, opts)
   } finally {
     if (downloadRes) {
       downloadRes.flush()
@@ -63,17 +119,47 @@ export async function downloadAndResize(opts: DownloadAndResizeOpts) {
   }
 }
 
-export interface ResizeOpts {
+export async function saveImageModal({uri}: {uri: string}) {
+  const downloadResponse = await RNFetchBlob.config({
+    fileCache: true,
+  }).fetch('GET', uri)
+
+  const imagePath = downloadResponse.path()
+  const base64Data = await downloadResponse.readFile('base64')
+  const result = await Share.share({
+    url: 'data:image/png;base64,' + base64Data,
+  })
+  if (result.action === Share.sharedAction) {
+    Toast.show('Image saved to gallery')
+  } else if (result.action === Share.dismissedAction) {
+    // dismissed
+  }
+  RNFS.unlink(imagePath)
+}
+
+export function getImageDim(path: string): Promise<Dimensions> {
+  return new Promise((resolve, reject) => {
+    RNImage.getSize(
+      path,
+      (width, height) => {
+        resolve({width, height})
+      },
+      reject,
+    )
+  })
+}
+
+// internal methods
+// =
+
+interface DoResizeOpts {
   width: number
   height: number
   mode: 'contain' | 'cover' | 'stretch'
   maxSize: number
 }
 
-export async function resize(
-  localUri: string,
-  opts: ResizeOpts,
-): Promise<Image> {
+async function doResize(localUri: string, opts: DoResizeOpts): Promise<Image> {
   for (let i = 0; i < 9; i++) {
     const quality = 100 - i * 10
     const resizeRes = await ImageResizer.createResizedImage(
@@ -89,7 +175,7 @@ export async function resize(
     )
     if (resizeRes.size < opts.maxSize) {
       return {
-        path: resizeRes.path,
+        path: normalizePath(resizeRes.path),
         mime: 'image/jpeg',
         size: resizeRes.size,
         width: resizeRes.width,
@@ -102,78 +188,24 @@ export async function resize(
   )
 }
 
-export async function compressIfNeeded(
-  img: Image,
-  maxSize: number,
-): Promise<Image> {
-  const origUri = `file://${img.path}`
-  if (img.size < maxSize) {
-    return img
-  }
-  const resizedImage = await resize(origUri, {
-    width: img.width,
-    height: img.height,
-    mode: 'stretch',
-    maxSize,
-  })
-  const finalImageMovedPath = await moveToPremanantPath(resizedImage.path)
-  const finalImg = {
-    ...resizedImage,
-    path: finalImageMovedPath,
-  }
-  return finalImg
-}
-
-export function scaleDownDimensions(dim: Dim, max: Dim): Dim {
-  if (dim.width < max.width && dim.height < max.height) {
-    return dim
-  }
-  let wScale = dim.width > max.width ? max.width / dim.width : 1
-  let hScale = dim.height > max.height ? max.height / dim.height : 1
-  if (wScale < hScale) {
-    return {width: dim.width * wScale, height: dim.height * wScale}
-  }
-  return {width: dim.width * hScale, height: dim.height * hScale}
-}
-
-export async function saveImageModal({uri}: {uri: string}) {
-  const downloadResponse = await RNFetchBlob.config({
-    fileCache: true,
-  }).fetch('GET', uri)
-
-  const imagePath = downloadResponse.path()
-  const base64Data = await downloadResponse.readFile('base64')
-  const result = await Share.share({
-    url: 'data:image/png;base64,' + base64Data,
-  })
-  if (result.action === Share.sharedAction) {
-    Toast.show('Image saved to gallery')
-  } else if (result.action === Share.dismissedAction) {
-    // dismissed
-  }
-  RNFS.unlink(imagePath)
-}
-
-export async function moveToPremanantPath(path: string) {
+async function moveToPermanentPath(path: string): Promise<string> {
   /*
   Since this package stores images in a temp directory, we need to move the file to a permanent location.
   Relevant: IOS bug when trying to open a second time:
   https://github.com/ivpusic/react-native-image-crop-picker/issues/1199
   */
   const filename = uuid.v4()
+
   const destinationPath = `${RNFS.TemporaryDirectoryPath}/${filename}`
-  RNFS.moveFile(path, destinationPath)
-  return destinationPath
+  await RNFS.moveFile(path, destinationPath)
+  return normalizePath(destinationPath)
 }
 
-export function getImageDim(path: string): Promise<Dim> {
-  return new Promise((resolve, reject) => {
-    RNImage.getSize(
-      path,
-      (width, height) => {
-        resolve({width, height})
-      },
-      reject,
-    )
-  })
+function normalizePath(str: string): string {
+  if (isAndroid) {
+    if (!str.startsWith('file://')) {
+      return `file://${str}`
+    }
+  }
+  return str
 }
diff --git a/src/lib/media/manip.web.ts b/src/lib/media/manip.web.ts
index cd0bb3bc9..85f6b6138 100644
--- a/src/lib/media/manip.web.ts
+++ b/src/lib/media/manip.web.ts
@@ -1,6 +1,40 @@
-// import {Share} from 'react-native'
-// import * as Toast from 'view/com/util/Toast'
-import {extractDataUriMime, getDataUriSize} from './util'
+import {Dimensions} from './types'
+import {Image as RNImage} from 'react-native-image-crop-picker'
+import {getDataUriSize, blobToDataUri} from './util'
+import {POST_IMG_MAX} from 'lib/constants'
+
+export async function compressAndResizeImageForPost({
+  path,
+  width,
+  height,
+}: {
+  path: string
+  width: number
+  height: number
+}): Promise<RNImage> {
+  // Compression is handled in `doResize` via `quality`
+  return await doResize(path, {
+    width,
+    height,
+    maxSize: POST_IMG_MAX.size,
+    mode: 'stretch',
+  })
+}
+
+export async function compressIfNeeded(
+  img: RNImage,
+  maxSize: number,
+): Promise<RNImage> {
+  if (img.size < maxSize) {
+    return img
+  }
+  return await doResize(img.path, {
+    width: img.width,
+    height: img.height,
+    mode: 'stretch',
+    maxSize,
+  })
+}
 
 export interface DownloadAndResizeOpts {
   uri: string
@@ -11,14 +45,6 @@ export interface DownloadAndResizeOpts {
   timeout: number
 }
 
-export interface Image {
-  path: string
-  mime: string
-  size: number
-  width: number
-  height: number
-}
-
 export async function downloadAndResize(opts: DownloadAndResizeOpts) {
   const controller = new AbortController()
   const to = setTimeout(() => controller.abort(), opts.timeout || 5e3)
@@ -27,58 +53,7 @@ export async function downloadAndResize(opts: DownloadAndResizeOpts) {
   clearTimeout(to)
 
   const dataUri = await blobToDataUri(resBody)
-  return await resize(dataUri, opts)
-}
-
-export interface ResizeOpts {
-  width: number
-  height: number
-  mode: 'contain' | 'cover' | 'stretch'
-  maxSize: number
-}
-
-export async function resize(
-  dataUri: string,
-  _opts: ResizeOpts,
-): Promise<Image> {
-  const dim = await getImageDim(dataUri)
-  // TODO -- need to resize
-  return {
-    path: dataUri,
-    mime: extractDataUriMime(dataUri),
-    size: getDataUriSize(dataUri),
-    width: dim.width,
-    height: dim.height,
-  }
-}
-
-export async function compressIfNeeded(
-  img: Image,
-  maxSize: number,
-): Promise<Image> {
-  if (img.size > maxSize) {
-    // TODO
-    throw new Error(
-      "This image is too large and we haven't implemented compression yet -- sorry!",
-    )
-  }
-  return img
-}
-
-export interface Dim {
-  width: number
-  height: number
-}
-export function scaleDownDimensions(dim: Dim, max: Dim): Dim {
-  if (dim.width < max.width && dim.height < max.height) {
-    return dim
-  }
-  let wScale = dim.width > max.width ? max.width / dim.width : 1
-  let hScale = dim.height > max.height ? max.height / dim.height : 1
-  if (wScale < hScale) {
-    return {width: dim.width * wScale, height: dim.height * wScale}
-  }
-  return {width: dim.width * hScale, height: dim.height * hScale}
+  return await doResize(dataUri, opts)
 }
 
 export async function saveImageModal(_opts: {uri: string}) {
@@ -86,11 +61,7 @@ export async function saveImageModal(_opts: {uri: string}) {
   throw new Error('TODO')
 }
 
-export async function moveToPremanantPath(path: string) {
-  return path
-}
-
-export async function getImageDim(path: string): Promise<Dim> {
+export async function getImageDim(path: string): Promise<Dimensions> {
   var img = document.createElement('img')
   const promise = new Promise((resolve, reject) => {
     img.onload = resolve
@@ -101,17 +72,82 @@ export async function getImageDim(path: string): Promise<Dim> {
   return {width: img.width, height: img.height}
 }
 
-function blobToDataUri(blob: Blob): Promise<string> {
+// internal methods
+// =
+
+interface DoResizeOpts {
+  width: number
+  height: number
+  mode: 'contain' | 'cover' | 'stretch'
+  maxSize: number
+}
+
+async function doResize(dataUri: string, opts: DoResizeOpts): Promise<RNImage> {
+  let newDataUri
+
+  for (let i = 0; i <= 10; i++) {
+    newDataUri = await createResizedImage(dataUri, {
+      width: opts.width,
+      height: opts.height,
+      quality: 1 - i * 0.1,
+      mode: opts.mode,
+    })
+    if (getDataUriSize(newDataUri) < opts.maxSize) {
+      break
+    }
+  }
+  if (!newDataUri) {
+    throw new Error('Failed to compress image')
+  }
+  return {
+    path: newDataUri,
+    mime: 'image/jpeg',
+    size: getDataUriSize(newDataUri),
+    width: opts.width,
+    height: opts.height,
+  }
+}
+
+function createResizedImage(
+  dataUri: string,
+  {
+    width,
+    height,
+    quality,
+    mode,
+  }: {
+    width: number
+    height: number
+    quality: number
+    mode: 'contain' | 'cover' | 'stretch'
+  },
+): Promise<string> {
   return new Promise((resolve, reject) => {
-    const reader = new FileReader()
-    reader.onloadend = () => {
-      if (typeof reader.result === 'string') {
-        resolve(reader.result)
-      } else {
-        reject(new Error('Failed to read blob'))
+    const img = document.createElement('img')
+    img.addEventListener('load', () => {
+      const canvas = document.createElement('canvas')
+      const ctx = canvas.getContext('2d')
+      if (!ctx) {
+        return reject(new Error('Failed to resize image'))
       }
-    }
-    reader.onerror = reject
-    reader.readAsDataURL(blob)
+
+      canvas.width = width
+      canvas.height = height
+
+      let scale = 1
+      if (mode === 'cover') {
+        scale = img.width < img.height ? width / img.width : height / img.height
+      } else if (mode === 'contain') {
+        scale = img.width > img.height ? width / img.width : height / img.height
+      }
+      let w = img.width * scale
+      let h = img.height * scale
+      let x = (width - w) / 2
+      let y = (height - h) / 2
+
+      ctx.drawImage(img, x, y, w, h)
+      resolve(canvas.toDataURL('image/jpeg', quality))
+    })
+    img.src = dataUri
   })
 }
diff --git a/src/lib/media/picker.e2e.tsx b/src/lib/media/picker.e2e.tsx
index 9f4765ac2..e53dc42be 100644
--- a/src/lib/media/picker.e2e.tsx
+++ b/src/lib/media/picker.e2e.tsx
@@ -1,13 +1,8 @@
 import {RootStoreModel} from 'state/index'
-import {PickerOpts, CameraOpts, CropperOpts, PickedMedia} from './types'
-import {
-  scaleDownDimensions,
-  Dim,
-  compressIfNeeded,
-  moveToPremanantPath,
-} from 'lib/media/manip'
-export type {PickedMedia} from './types'
+import {Image as RNImage} from 'react-native-image-crop-picker'
 import RNFS from 'react-native-fs'
+import {CropperOptions} from './types'
+import {compressAndResizeImageForPost} from './manip'
 
 let _imageCounter = 0
 async function getFile() {
@@ -17,100 +12,33 @@ async function getFile() {
       .concat(['Media', 'DCIM', '100APPLE'])
       .join('/'),
   )
-  return files[_imageCounter++ % files.length]
-}
-
-export async function openPicker(
-  _store: RootStoreModel,
-  opts: PickerOpts,
-): Promise<PickedMedia[]> {
-  const mediaType = opts.mediaType || 'photo'
-  const items = await getFile()
-  const toMedia = (item: RNFS.ReadDirItem) => ({
-    mediaType,
-    path: item.path,
+  const file = files[_imageCounter++ % files.length]
+  return await compressAndResizeImageForPost({
+    path: file.path,
     mime: 'image/jpeg',
-    size: item.size,
+    size: file.size,
     width: 4288,
     height: 2848,
   })
-  if (Array.isArray(items)) {
-    return items.map(toMedia)
-  }
-  return [toMedia(items)]
 }
 
-export async function openCamera(
-  _store: RootStoreModel,
-  opts: CameraOpts,
-): Promise<PickedMedia> {
-  const mediaType = opts.mediaType || 'photo'
-  const item = await getFile()
-  return {
-    mediaType,
-    path: item.path,
-    mime: 'image/jpeg',
-    size: item.size,
-    width: 4288,
-    height: 2848,
-  }
+export async function openPicker(_store: RootStoreModel): Promise<RNImage[]> {
+  return [await getFile()]
+}
+
+export async function openCamera(_store: RootStoreModel): Promise<RNImage> {
+  return await getFile()
 }
 
 export async function openCropper(
   _store: RootStoreModel,
-  opts: CropperOpts,
-): Promise<PickedMedia> {
-  const mediaType = opts.mediaType || 'photo'
-  const item = await getFile()
+  opts: CropperOptions,
+): Promise<RNImage> {
   return {
-    mediaType,
-    path: item.path,
+    path: opts.path,
     mime: 'image/jpeg',
-    size: item.size,
+    size: 123,
     width: 4288,
     height: 2848,
   }
 }
-
-export async function pickImagesFlow(
-  store: RootStoreModel,
-  maxFiles: number,
-  maxDim: Dim,
-  maxSize: number,
-) {
-  const items = await openPicker(store, {
-    multiple: true,
-    maxFiles,
-    mediaType: 'photo',
-  })
-  const result = []
-  for (const image of items) {
-    result.push(
-      await cropAndCompressFlow(store, image.path, image, maxDim, maxSize),
-    )
-  }
-  return result
-}
-
-export async function cropAndCompressFlow(
-  store: RootStoreModel,
-  path: string,
-  imgDim: Dim,
-  maxDim: Dim,
-  maxSize: number,
-) {
-  // choose target dimensions based on the original
-  // this causes the photo cropper to start with the full image "selected"
-  const {width, height} = scaleDownDimensions(imgDim, maxDim)
-  const cropperRes = await openCropper(store, {
-    mediaType: 'photo',
-    path,
-    freeStyleCropEnabled: true,
-    width,
-    height,
-  })
-
-  const img = await compressIfNeeded(cropperRes, maxSize)
-  const permanentPath = await moveToPremanantPath(img.path)
-  return permanentPath
-}
diff --git a/src/lib/media/picker.tsx b/src/lib/media/picker.tsx
index 70a5d9068..af4a3e4d3 100644
--- a/src/lib/media/picker.tsx
+++ b/src/lib/media/picker.tsx
@@ -5,14 +5,8 @@ import {
   ImageOrVideo,
 } from 'react-native-image-crop-picker'
 import {RootStoreModel} from 'state/index'
-import {PickerOpts, CameraOpts, CropperOpts, PickedMedia} from './types'
-import {
-  scaleDownDimensions,
-  Dim,
-  compressIfNeeded,
-  moveToPremanantPath,
-} from 'lib/media/manip'
-export type {PickedMedia} from './types'
+import {PickerOpts, CameraOpts, CropperOptions} from './types'
+import {Image as RNImage} from 'react-native-image-crop-picker'
 
 /**
  * NOTE
@@ -25,18 +19,17 @@ export type {PickedMedia} from './types'
 
 export async function openPicker(
   _store: RootStoreModel,
-  opts: PickerOpts,
-): Promise<PickedMedia[]> {
-  const mediaType = opts.mediaType || 'photo'
+  opts?: PickerOpts,
+): Promise<RNImage[]> {
   const items = await openPickerFn({
-    mediaType,
-    multiple: opts.multiple,
-    maxFiles: opts.maxFiles,
+    mediaType: 'photo', // TODO: eventually add other media types
+    multiple: opts?.multiple,
+    maxFiles: opts?.maxFiles,
     forceJpg: true, // ios only
     compressImageQuality: 0.8,
   })
+
   const toMedia = (item: ImageOrVideo) => ({
-    mediaType,
     path: item.path,
     mime: item.mime,
     size: item.size,
@@ -52,20 +45,17 @@ export async function openPicker(
 export async function openCamera(
   _store: RootStoreModel,
   opts: CameraOpts,
-): Promise<PickedMedia> {
-  const mediaType = opts.mediaType || 'photo'
+): Promise<RNImage> {
   const item = await openCameraFn({
-    mediaType,
     width: opts.width,
     height: opts.height,
     freeStyleCropEnabled: opts.freeStyleCropEnabled,
     cropperCircleOverlay: opts.cropperCircleOverlay,
-    cropping: true,
+    cropping: false,
     forceJpg: true, // ios only
     compressImageQuality: 0.8,
   })
   return {
-    mediaType,
     path: item.path,
     mime: item.mime,
     size: item.size,
@@ -76,21 +66,15 @@ export async function openCamera(
 
 export async function openCropper(
   _store: RootStoreModel,
-  opts: CropperOpts,
-): Promise<PickedMedia> {
-  const mediaType = opts.mediaType || 'photo'
+  opts: CropperOptions,
+): Promise<RNImage> {
   const item = await openCropperFn({
-    path: opts.path,
-    mediaType: opts.mediaType || 'photo',
-    width: opts.width,
-    height: opts.height,
-    freeStyleCropEnabled: opts.freeStyleCropEnabled,
-    cropperCircleOverlay: opts.cropperCircleOverlay,
+    ...opts,
     forceJpg: true, // ios only
     compressImageQuality: 0.8,
   })
+
   return {
-    mediaType,
     path: item.path,
     mime: item.mime,
     size: item.size,
@@ -98,46 +82,3 @@ export async function openCropper(
     height: item.height,
   }
 }
-
-export async function pickImagesFlow(
-  store: RootStoreModel,
-  maxFiles: number,
-  maxDim: Dim,
-  maxSize: number,
-) {
-  const items = await openPicker(store, {
-    multiple: true,
-    maxFiles,
-    mediaType: 'photo',
-  })
-  const result = []
-  for (const image of items) {
-    result.push(
-      await cropAndCompressFlow(store, image.path, image, maxDim, maxSize),
-    )
-  }
-  return result
-}
-
-export async function cropAndCompressFlow(
-  store: RootStoreModel,
-  path: string,
-  imgDim: Dim,
-  maxDim: Dim,
-  maxSize: number,
-) {
-  // choose target dimensions based on the original
-  // this causes the photo cropper to start with the full image "selected"
-  const {width, height} = scaleDownDimensions(imgDim, maxDim)
-  const cropperRes = await openCropper(store, {
-    mediaType: 'photo',
-    path,
-    freeStyleCropEnabled: true,
-    width,
-    height,
-  })
-
-  const img = await compressIfNeeded(cropperRes, maxSize)
-  const permanentPath = await moveToPremanantPath(img.path)
-  return permanentPath
-}
diff --git a/src/lib/media/picker.web.tsx b/src/lib/media/picker.web.tsx
index 158c37971..3a9869985 100644
--- a/src/lib/media/picker.web.tsx
+++ b/src/lib/media/picker.web.tsx
@@ -1,16 +1,10 @@
 /// <reference lib="dom" />
 
-import {PickerOpts, CameraOpts, CropperOpts, PickedMedia} from './types'
-export type {PickedMedia} from './types'
+import {PickerOpts, CameraOpts, CropperOptions} from './types'
 import {RootStoreModel} from 'state/index'
-import {
-  scaleDownDimensions,
-  getImageDim,
-  Dim,
-  compressIfNeeded,
-  moveToPremanantPath,
-} from 'lib/media/manip'
+import {getImageDim} from 'lib/media/manip'
 import {extractDataUriMime} from './util'
+import {Image as RNImage} from 'react-native-image-crop-picker'
 
 interface PickedFile {
   uri: string
@@ -21,13 +15,12 @@ interface PickedFile {
 export async function openPicker(
   _store: RootStoreModel,
   opts: PickerOpts,
-): Promise<PickedMedia[]> {
+): Promise<RNImage[]> {
   const res = await selectFile(opts)
   const dim = await getImageDim(res.uri)
   const mime = extractDataUriMime(res.uri)
   return [
     {
-      mediaType: 'photo',
       path: res.uri,
       mime,
       size: res.size,
@@ -40,21 +33,21 @@ export async function openPicker(
 export async function openCamera(
   _store: RootStoreModel,
   _opts: CameraOpts,
-): Promise<PickedMedia> {
+): Promise<RNImage> {
   // const mediaType = opts.mediaType || 'photo' TODO
   throw new Error('TODO')
 }
 
 export async function openCropper(
   store: RootStoreModel,
-  opts: CropperOpts,
-): Promise<PickedMedia> {
+  opts: CropperOptions,
+): Promise<RNImage> {
   // TODO handle more opts
   return new Promise((resolve, reject) => {
     store.shell.openModal({
       name: 'crop-image',
       uri: opts.path,
-      onSelect: (img?: PickedMedia) => {
+      onSelect: (img?: RNImage) => {
         if (img) {
           resolve(img)
         } else {
@@ -65,52 +58,6 @@ export async function openCropper(
   })
 }
 
-export async function pickImagesFlow(
-  store: RootStoreModel,
-  maxFiles: number,
-  maxDim: Dim,
-  maxSize: number,
-) {
-  const items = await openPicker(store, {
-    multiple: true,
-    maxFiles,
-    mediaType: 'photo',
-  })
-  const result = []
-  for (const image of items) {
-    result.push(
-      await cropAndCompressFlow(store, image.path, image, maxDim, maxSize),
-    )
-  }
-  return result
-}
-
-export async function cropAndCompressFlow(
-  store: RootStoreModel,
-  path: string,
-  imgDim: Dim,
-  maxDim: Dim,
-  maxSize: number,
-) {
-  // choose target dimensions based on the original
-  // this causes the photo cropper to start with the full image "selected"
-  const {width, height} = scaleDownDimensions(imgDim, maxDim)
-  const cropperRes = await openCropper(store, {
-    mediaType: 'photo',
-    path,
-    freeStyleCropEnabled: true,
-    width,
-    height,
-  })
-
-  const img = await compressIfNeeded(cropperRes, maxSize)
-  const permanentPath = await moveToPremanantPath(img.path)
-  return permanentPath
-}
-
-// helpers
-// =
-
 /**
  * Opens the select file dialog in the browser.
  * NOTE:
diff --git a/src/lib/media/types.ts b/src/lib/media/types.ts
index 3197b4d3e..e6f442759 100644
--- a/src/lib/media/types.ts
+++ b/src/lib/media/types.ts
@@ -1,31 +1,21 @@
+import {openCropper} from 'react-native-image-crop-picker'
+
+export interface Dimensions {
+  width: number
+  height: number
+}
+
 export interface PickerOpts {
-  mediaType?: 'photo'
+  mediaType?: string
   multiple?: boolean
   maxFiles?: number
 }
 
 export interface CameraOpts {
-  mediaType?: 'photo'
-  width: number
-  height: number
-  freeStyleCropEnabled?: boolean
-  cropperCircleOverlay?: boolean
-}
-
-export interface CropperOpts {
-  path: string
-  mediaType?: 'photo'
   width: number
   height: number
   freeStyleCropEnabled?: boolean
   cropperCircleOverlay?: boolean
 }
 
-export interface PickedMedia {
-  mediaType: 'photo'
-  path: string
-  mime: string
-  size: number
-  width: number
-  height: number
-}
+export type CropperOptions = Parameters<typeof openCropper>[0]
diff --git a/src/lib/media/util.ts b/src/lib/media/util.ts
index a27c71d82..75915de6b 100644
--- a/src/lib/media/util.ts
+++ b/src/lib/media/util.ts
@@ -1,7 +1,45 @@
+import {Dimensions} from './types'
+
 export function extractDataUriMime(uri: string): string {
   return uri.substring(uri.indexOf(':') + 1, uri.indexOf(';'))
 }
 
+// Fairly accurate estimate that is more performant
+// than decoding and checking length of URI
 export function getDataUriSize(uri: string): number {
-  return Math.round((uri.length * 3) / 4) // very rough estimate
+  return Math.round((uri.length * 3) / 4)
+}
+
+export function scaleDownDimensions(
+  dim: Dimensions,
+  max: Dimensions,
+): Dimensions {
+  if (dim.width < max.width && dim.height < max.height) {
+    return dim
+  }
+  const wScale = dim.width > max.width ? max.width / dim.width : 1
+  const hScale = dim.height > max.height ? max.height / dim.height : 1
+  if (wScale < hScale) {
+    return {width: dim.width * wScale, height: dim.height * wScale}
+  }
+  return {width: dim.width * hScale, height: dim.height * hScale}
+}
+
+export function isUriImage(uri: string) {
+  return /\.(jpg|jpeg|png).*$/.test(uri)
+}
+
+export function blobToDataUri(blob: Blob): Promise<string> {
+  return new Promise((resolve, reject) => {
+    const reader = new FileReader()
+    reader.onloadend = () => {
+      if (typeof reader.result === 'string') {
+        resolve(reader.result)
+      } else {
+        reject(new Error('Failed to read blob'))
+      }
+    }
+    reader.onerror = reject
+    reader.readAsDataURL(blob)
+  })
 }