Rework scaled dimensions and compression (#737)
* Rework scaled dimensions and compression * Unbreak image / banner uploads --------- Co-authored-by: Paul Frazee <pfrazee@gmail.com>
This commit is contained in:
parent
deebe18aaa
commit
072682dd9f
12 changed files with 175 additions and 238 deletions
1
src/state/models/cache/image-sizes.ts
vendored
1
src/state/models/cache/image-sizes.ts
vendored
|
@ -16,6 +16,7 @@ export class ImageSizesCache {
|
|||
if (Dimensions) {
|
||||
return Dimensions
|
||||
}
|
||||
|
||||
const prom =
|
||||
this.activeRequests.get(uri) ||
|
||||
new Promise<Dimensions>(resolve => {
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue