react-native-expo-image-cropper
Version:
Crop, rotate and filp image in your expo project!
439 lines (397 loc) • 17.8 kB
JavaScript
import React, { Component } from 'react'
import {
Dimensions,
Image,
ScrollView,
Modal,
View,
Text,
SafeAreaView,
TouchableOpacity,
YellowBox
} from 'react-native'
import * as ImageManipulator from 'expo-image-manipulator'
import * as FileSystem from 'expo-file-system'
import PropTypes from 'prop-types'
import AutoHeightImage from 'react-native-auto-height-image'
import Icon from 'react-native-vector-icons/MaterialCommunityIcons'
import { ifIphoneX, isIphoneX, getStatusBarHeight, getBottomSpace } from 'react-native-iphone-x-helper'
import ImageCropOverlay from '../manipulator/ImageCropOverlay'
const { width, height } = Dimensions.get('window')
YellowBox.ignoreWarnings(['componentWillReceiveProps', 'componentWillUpdate', 'componentWillMount']);
YellowBox.ignoreWarnings([
'Warning: componentWillMount is deprecated',
'Warning: componentWillReceiveProps is deprecated',
'Module RCTImageLoader requires',
]);
class ExpoImageManipulator extends Component {
constructor(props) {
super(props)
const { squareAspect } = this.props
this.state = {
cropMode: false,
processing: false,
zoomScale: 1,
squareAspect,
}
this.scrollOffset = 0
this.currentPos = {
left: 0,
top: 0,
}
this.currentSize = {
width: 0,
height: 0,
}
this.maxSizes = {
width: 0,
height: 0,
}
this.actualSize = {
width: 0,
height: 0
}
}
async componentDidMount() {
await this.onConvertImageToEditableSize()
}
async onConvertImageToEditableSize() {
const { photo: { uri: rawUri } } = this.props
const { uri, width, height } = await ImageManipulator.manipulateAsync(rawUri,
[
{
resize: {
width: 1080,
},
},
])
this.setState({
uri,
})
this.actualSize.width = width
this.actualSize.height = height
}
get isRemote() {
const { uri } = this.state
return /^(http|https|ftp)?(?:[:/]*)([a-z0-9.-]*)(?::([0-9]+))?(\/[^?#]*)?(?:\?([^#]*))?(?:#(.*))?$/.test(uri)
}
onToggleModal = () => {
const { onToggleModal } = this.props
onToggleModal()
this.setState({ cropMode: false })
}
onCropImage = () => {
this.setState({ processing: true })
const { uri } = this.state
Image.getSize(uri, async (actualWidth, actualHeight) => {
let cropObj = this.getCropBounds(actualWidth, actualHeight);
if (cropObj.height > 0 && cropObj.width > 0) {
let uriToCrop = uri
if (this.isRemote) {
const response = await FileSystem.downloadAsync(
uri,
FileSystem.documentDirectory + 'image',
)
uriToCrop = response.uri
}
const { uri: uriCroped, base64, width: croppedWidth, height: croppedHeight } = await this.crop(cropObj, uriToCrop)
this.actualSize.width = croppedWidth
this.actualSize.height = croppedHeight
this.setState({
uri: uriCroped, base64, cropMode: false, processing: false,
})
} else {
this.setState({cropMode: false, processing: false})
}
})
}
onRotateImage = async () => {
const { uri } = this.state
let uriToCrop = uri
if (this.isRemote) {
const response = await FileSystem.downloadAsync(
uri,
FileSystem.documentDirectory + 'image',
)
uriToCrop = response.uri
}
Image.getSize(uri, async (width2, height2) => {
const { uri: rotUri, base64 } = await this.rotate(uriToCrop, width2, height2)
this.setState({ uri: rotUri, base64 })
})
}
onFlipImage = async (orientation) => {
const { uri } = this.state
let uriToCrop = uri
if (this.isRemote) {
const response = await FileSystem.downloadAsync(
uri,
FileSystem.documentDirectory + 'image',
)
uriToCrop = response.uri
}
Image.getSize(uri, async (width2, height2) => {
const { uri: rotUri, base64 } = await this.filp(uriToCrop, orientation)
this.setState({ uri: rotUri, base64 })
})
}
onHandleScroll = (event) => {
this.scrollOffset = event.nativeEvent.contentOffset.y
}
getCropBounds = (actualWidth, actualHeight) => {
let imageRatio = actualHeight / actualWidth
var originalHeight = Dimensions.get('window').height - 64
if (isIphoneX()) {
originalHeight = Dimensions.get('window').height - 122
}
let renderedImageWidth = imageRatio < (originalHeight / width) ? width : originalHeight / imageRatio
let renderedImageHeight = imageRatio < (originalHeight / width) ? width * imageRatio : originalHeight
let renderedImageY = (originalHeight - renderedImageHeight) / 2.0
let renderedImageX = (width - renderedImageWidth) / 2.0
const renderImageObj = {
left: renderedImageX,
top: renderedImageY,
width: renderedImageWidth,
height: renderedImageHeight,
}
const cropOverlayObj = {
left: this.currentPos.left,
top: this.currentPos.top,
width: this.currentSize.width,
height: this.currentSize.height,
}
var intersectAreaObj = {}
let x = Math.max(renderImageObj.left, cropOverlayObj.left);
let num1 = Math.min(renderImageObj.left + renderImageObj.width, cropOverlayObj.left + cropOverlayObj.width);
let y = Math.max(renderImageObj.top, cropOverlayObj.top);
let num2 = Math.min(renderImageObj.top + renderImageObj.height, cropOverlayObj.top + cropOverlayObj.height);
if (num1 >= x && num2 >= y)
intersectAreaObj = {
originX: (x - renderedImageX) * (actualWidth / renderedImageWidth) ,
originY: (y - renderedImageY) * (actualWidth / renderedImageWidth),
width: (num1 - x) * (actualWidth / renderedImageWidth),
height: (num2 - y) * (actualWidth / renderedImageWidth)
}
else {
intersectAreaObj = {
originX: x - renderedImageX,
originY: y - renderedImageY,
width: 0,
height: 0
}
}
return intersectAreaObj
}
filp = async (uri, orientation) => {
const { saveOptions } = this.props
const manipResult = await ImageManipulator.manipulateAsync(uri, [{
flip: orientation == 'vertical' ? ImageManipulator.FlipType.Vertical : ImageManipulator.FlipType.Horizontal
}],
saveOptions
);
return manipResult;
};
rotate = async (uri, width2, height2) => {
const { saveOptions } = this.props
const manipResult = await ImageManipulator.manipulateAsync(uri, [{
rotate: -90,
}, {
resize: {
width: this.trueWidth || width2,
// height: this.trueHeight || height2,
},
}], saveOptions)
return manipResult
}
crop = async (cropObj, uri) => {
const { saveOptions } = this.props
if (cropObj.height > 0 && cropObj.width > 0) {
const manipResult = await ImageManipulator.manipulateAsync(
uri,
[{
crop: cropObj,
}],
saveOptions,
)
return manipResult
}
return {
uri: null,
base64: null,
}
};
calculateMaxSizes = (event) => {
let w1 = event.nativeEvent.layout.width || 100
let h1 = event.nativeEvent.layout.height || 100
if (this.state.squareAspect) {
if (w1 < h1) h1 = w1
else w1 = h1
}
this.maxSizes.width = w1
this.maxSizes.height = h1
};
// eslint-disable-next-line camelcase
async UNSAFE_componentWillReceiveProps() {
await this.onConvertImageToEditableSize()
}
zoomImage() {
// this.refs.imageScrollView.zoomScale = 5
// this.setState({width: width})
// this.setState({zoomScale: 5})
// this.setState(curHeight)
}
render() {
const {
isVisible,
onPictureChoosed,
borderColor = '#a4a4a4',
allowRotate = true,
pinchGestureEnabled,
btnTexts,
} = this.props
const {
uri,
base64,
cropMode,
processing,
zoomScale
} = this.state
let imageRatio = this.actualSize.height / this.actualSize.width
var originalHeight = Dimensions.get('window').height - 64
if (isIphoneX()) {
originalHeight = Dimensions.get('window').height - 122
}
let cropRatio = originalHeight / width
let cropWidth = imageRatio < cropRatio ? width : originalHeight / imageRatio
let cropHeight = imageRatio < cropRatio ? width * imageRatio : originalHeight
let cropInitialTop = (originalHeight - cropHeight) / 2.0
let cropInitialLeft = (width - cropWidth) / 2.0
if (this.currentSize.width == 0 && cropMode) {
this.currentSize.width = cropWidth;
this.currentSize.height = cropHeight;
this.currentPos.top = cropInitialTop;
this.currentPos.left = cropInitialLeft;
}
if (uri == undefined) {
return (
<View></View>
)
} else {
return (
<Modal
animationType="slide"
transparent={true}
visible={isVisible}
hardwareAccelerated
onRequestClose={() => {
this.onToggleModal()
}}>
<SafeAreaView
style={{width, flexDirection: 'row', backgroundColor: 'black', justifyContent: 'space-between'}}
>
<ScrollView scrollEnabled={false} horizontal contentContainerStyle={{width: '100%', paddingHorizontal: 15, height: 44, alignItems: 'center'}}>
{!cropMode ?
<View style={{flexDirection: 'row'}}>
<TouchableOpacity onPress={() => this.onToggleModal()} style={{width: 32, height: 32, alignItems: 'center', justifyContent: 'center'}}>
<Icon size={24} name={'arrow-left'} color="white" />
</TouchableOpacity>
<View style={{flex: 1, flexDirection: 'row', justifyContent: 'flex-end'}}>
<TouchableOpacity onPress={() => this.setState({cropMode: true})} style={{marginLeft: 10, width: 32, height: 32, alignItems: 'center', justifyContent: 'center'}}>
<Image source={require('../assets/crop-free.png')} style={{width: 24, height: 24}}></Image>
</TouchableOpacity>
<TouchableOpacity onPress={() => this.onRotateImage()} style={{marginLeft: 10, width: 32, height: 32, alignItems: 'center', justifyContent: 'center'}}>
<Image source={require('../assets/rotate-left.png')} style={{width: 24, height: 24}}></Image>
</TouchableOpacity>
<TouchableOpacity onPress={() => this.onFlipImage('vertical')} style={{marginLeft: 10, width: 32, height: 32, alignItems: 'center', justifyContent: 'center'}}>
<Image source={require('../assets/flip-vertical.png')} style={{width: 24, height: 24}}></Image>
</TouchableOpacity>
<TouchableOpacity onPress={() => this.onFlipImage('horizontal')} style={{marginLeft: 10, width: 32, height: 32, alignItems: 'center', justifyContent: 'center'}}>
<Image source={require('../assets/flip-horizontal.png')} style={{width: 24, height: 24}}></Image>
</TouchableOpacity>
<TouchableOpacity onPress={() => {onPictureChoosed({ uri, base64 }); this.onToggleModal()}} style={{marginLeft: 10, width: 60, height: 32, alignItems: 'center', justifyContent: 'center'}}>
<Text style={{fontWeight: '500', color: 'white', fontSize: 18}}>{'DONE'}</Text>
</TouchableOpacity>
</View>
</View> :
<View style={{flexDirection: 'row'}}>
<TouchableOpacity onPress={() => this.setState({cropMode: false})} style={{width: 32, height: 32, alignItems: 'center', justifyContent: 'center'}}>
<Icon size={24} name={'arrow-left'} color="white" />
</TouchableOpacity>
<View style={{flex: 1, flexDirection: 'row', justifyContent: 'flex-end'}}>
<TouchableOpacity onPress={() => this.onCropImage()} style={{marginRight: 10, width: 60, height: 32, alignItems: 'center', justifyContent: 'center'}}>
<Text style={{fontWeight: '500', color: 'white', fontSize: 18}}>{processing ? 'Processing' : 'CROP'}</Text>
</TouchableOpacity>
</View>
</View>
}
</ScrollView>
</SafeAreaView>
<View style={{ flex: 1, backgroundColor: 'black' , width: Dimensions.get('window').width }}>
<ScrollView
ref={'imageScrollView'}
style={{ position: 'relative', flex: 1}}
contentContainerStyle={{backgroundColor: 'black'}}
maximumZoomScale={5}
minimumZoomScale={0.5}
onScroll={this.onHandleScroll}
bounces={false}
showsHorizontalScrollIndicator={false}
showsVerticalScrollIndicator={false}
ref={(c) => { this.scrollView = c }}
scrollEventThrottle={16}
scrollEnabled={false}
pinchGestureEnabled={false}
// scrollEnabled={cropMode ? false : true}
// pinchGestureEnabled={cropMode ? false : pinchGestureEnabled}
>
<AutoHeightImage
style={{ backgroundColor: 'black' }}
source={{ uri }}
resizeMode={imageRatio >= 1 ? "contain" : 'contain'}
width={width}
height={originalHeight}
onLayout={this.calculateMaxSizes}
/>
{!!cropMode && (
<ImageCropOverlay onLayoutChanged={(top, left, width, height) => {
this.currentSize.width = width;
this.currentSize.height = height;
this.currentPos.top = top
this.currentPos.left = left
}} initialWidth={cropWidth} initialHeight={cropHeight} initialTop={cropInitialTop} initialLeft={cropInitialLeft} minHeight={100} minWidth={100} />
)
}
</ScrollView>
</View>
</Modal>
)
}
}
}
export default ExpoImageManipulator
ExpoImageManipulator.defaultProps = {
onPictureChoosed: ({ uri, base64 }) => console.log('URI:', uri, base64),
btnTexts: {
crop: 'Crop',
rotate: 'Rotate',
done: 'Done',
processing: 'Processing',
},
dragVelocity: 100,
resizeVelocity: 50,
saveOptions: {
compress: 1,
format: ImageManipulator.SaveFormat.PNG,
base64: false,
},
}
ExpoImageManipulator.propTypes = {
isVisible: PropTypes.bool.isRequired,
onPictureChoosed: PropTypes.func,
btnTexts: PropTypes.object,
saveOptions: PropTypes.object,
photo: PropTypes.object.isRequired,
onToggleModal: PropTypes.func.isRequired,
dragVelocity: PropTypes.number,
resizeVelocity: PropTypes.number,
}