diff options
Diffstat (limited to 'src/lib')
-rw-r--r-- | src/lib/api/index.ts | 2 | ||||
-rw-r--r-- | src/lib/constants.ts | 8 | ||||
-rw-r--r-- | src/lib/media/manip.ts | 198 | ||||
-rw-r--r-- | src/lib/media/manip.web.ts | 192 | ||||
-rw-r--r-- | src/lib/media/picker.e2e.tsx | 106 | ||||
-rw-r--r-- | src/lib/media/picker.tsx | 87 | ||||
-rw-r--r-- | src/lib/media/picker.web.tsx | 69 | ||||
-rw-r--r-- | src/lib/media/types.ts | 28 | ||||
-rw-r--r-- | src/lib/media/util.ts | 40 |
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) + }) } |