/PerspectiveCropper.tsx Secret
Created
May 18, 2021 04:02
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { Component } from "react" | |
import { Image, ImageLoadEventData, LayoutChangeEvent, NativeSyntheticEvent, View, ViewStyle } from "react-native" | |
import Cropper from "./Cropper"; | |
import { OpenCVContext, OpenCVContextProps } from "../../../Contexts/OpenCVContext"; | |
interface PerspectiveCropperProps { | |
source: {uri: string}; | |
style?: ViewStyle; | |
} | |
// Cropper에서 좌표 가중치를 계산하기 위해 image~와 imageLayout~을 상태로 만들어 줄 필요가 있다. | |
interface State { | |
didLayout: boolean; | |
imageLoaded: boolean; | |
viewWidth: number; | |
viewHeight: number; | |
imageWidth: number; | |
imageHeight: number; | |
resizedImageWidth: number; | |
resizedImageHeight: number; | |
uri: string; | |
} | |
export interface Point { | |
x: number; | |
y: number | |
} | |
interface Points { | |
p1: Point, | |
p2: Point, | |
p3: Point, | |
p4: Point | |
} | |
export default class PersPectiveCropper extends Component<PerspectiveCropperProps, State> { | |
static contextType = OpenCVContext; | |
opencvContext: OpenCVContextProps | undefined = undefined; | |
private async loadImageToOpenCV(uri: string, imageWidth: number, imageHeight: number) { | |
if(!this.opencvContext) return; | |
const {imageLoad} = this.opencvContext; | |
imageLoad(uri, imageWidth, imageHeight); | |
} | |
points: Points = { | |
p1: {x: 0, y: 0}, | |
p2: {x: 0, y: 0}, | |
p3: {x: 0, y: 0}, | |
p4: {x: 0, y: 0}, | |
} | |
constructor(props: PerspectiveCropperProps, context: OpenCVContextProps) { | |
super(props); | |
this.opencvContext = context; | |
const state: State = { | |
didLayout: false, | |
imageLoaded: false, | |
viewWidth: 0, | |
viewHeight: 0, | |
imageWidth: 0, | |
imageHeight: 0, | |
resizedImageWidth: 0, | |
resizedImageHeight: 0, | |
uri: this.props.source.uri | |
} | |
this.state = state; | |
this.onImageLoad = this.onImageLoad.bind(this); | |
this.onLayout = this.onLayout.bind(this); | |
this.setP1 = this.setP1.bind(this); | |
this.setP2 = this.setP2.bind(this); | |
this.setP3 = this.setP3.bind(this); | |
this.setP4 = this.setP4.bind(this); | |
this.setPoints = [this.setP1, this.setP2, this.setP3, this.setP4]; | |
} | |
// onLayout보다 먼저 발생했다 ... 언제나? | |
onImageLoad(event: NativeSyntheticEvent<ImageLoadEventData>) { | |
const {width: imageWidth, height: imageHeight} = event.nativeEvent.source; // 이미지의 실제 너비, 높이 | |
this.loadImageToOpenCV(this.state.uri, imageWidth, imageHeight); | |
const {didLayout, viewWidth, viewHeight} = this.state; | |
// onLayout보다 먼저 발생한 경우, (일반적) | |
if(!didLayout) { | |
this.setState({ | |
...this.state, | |
imageLoaded: true, | |
imageWidth, | |
imageHeight | |
}); | |
return; | |
} | |
// onImageLoad가 더 늦게 발생한 경우, (외부에서 이미지를 변경한 경우) | |
const viewRatio = viewWidth / viewHeight; // FastImage의 비율 | |
const imageRatio = imageWidth / imageHeight; // 이미지의 비율 | |
const isWidthFull = imageRatio > viewRatio; | |
// FastImage 안의 이미지 높이, 너비 결정 | |
const resizedImageWidth = isWidthFull ? viewWidth : (viewHeight * imageRatio); | |
const resizedImageHeight = isWidthFull ? (viewWidth / imageRatio) : viewHeight; | |
// console.log("[onImageLoad] 이미지 가로 풀? ", isWidthFull); | |
this.setState({ | |
...this.state, | |
imageLoaded: true, | |
imageWidth, | |
imageHeight, | |
resizedImageWidth, | |
resizedImageHeight | |
}); | |
} | |
onLayout(event: LayoutChangeEvent) { | |
// FastImage의 높이, 너비 | |
const {width: viewWidth, height: viewHeight} = event.nativeEvent.layout; | |
const {imageWidth, imageHeight, imageLoaded} = this.state; | |
if(!imageLoaded) { | |
this.setState({ | |
...this.state, | |
didLayout: true, | |
viewWidth, | |
viewHeight | |
}) | |
return | |
} | |
// 이미지 로드 후 레이아웃 이벤트 발생 (일반적인 경우) | |
const viewRatio = viewWidth / viewHeight; // FastImage의 비율 | |
const imageRatio = imageWidth / imageHeight; // 실제 이미지의 비율 | |
const isWidthFull = imageRatio > viewRatio; | |
const resizedImageWidth = isWidthFull ? viewWidth : viewHeight * imageRatio; | |
const resizedImageHeight = isWidthFull ? viewWidth / imageRatio : viewHeight; | |
// console.log("[onLayout] 이미지 가로 풀? ", isWidthFull); | |
// 이미지는 이미 로드됐기 때문에, imageWidth와 imageHeight는 최신 상태이다. | |
this.setState({ | |
...this.state, | |
didLayout: true, | |
viewWidth, | |
viewHeight, | |
resizedImageWidth, | |
resizedImageHeight | |
}); | |
} | |
setP1(point: Point) { this.points.p1 = point; } | |
setP2(point: Point) { this.points.p2 = point; } | |
setP3(point: Point) { this.points.p3 = point; } | |
setP4(point: Point) { this.points.p4 = point; } | |
setPoints: ((point: Point) => void)[] = []; | |
// 크롭이 완료되고, 이미지가 리랜더링되기 전에 다시 크롭하면 오류가 발생한다. 외부에서 차단해야 한다. | |
crop() { | |
if(!this.opencvContext) return; | |
const {crop} = this.opencvContext; | |
crop(this.points); | |
} | |
componentDidUpdate(_: PerspectiveCropperProps, prevState: State){ | |
const newUri = this.props.source.uri; | |
if(newUri !== prevState.uri) { | |
// uri 바뀌면 이미지 다시 로드되면서 onImageLoad 실행 => loadImageToOpenCV | |
this.setState({ | |
...this.state, | |
uri: newUri | |
}); | |
} | |
} | |
componentWillUnmount() { | |
if(!this.opencvContext) return; | |
const {imageUnload} = this.opencvContext; | |
imageUnload(); | |
} | |
// 외부에서 입력되는 style이 hook인 경우, onLayout 재실행 | |
render() { | |
return ( | |
<View onLayout={this.onLayout} style={[this.props.style, {justifyContent: 'center', alignItems: 'center'}]}> | |
{this.state.didLayout && <Image width={this.state.viewWidth} height={this.state.viewHeight} resizeMode="contain" style={{position: 'absolute', width: '100%', height: '100%'}} onLoad={this.onImageLoad} source={{uri: this.state.uri}}/>} | |
{this.state.imageLoaded && <Cropper resizedImageWidth={this.state.resizedImageWidth} resizedImageHeight={this.state.resizedImageHeight} imageWidth={this.state.imageWidth} imageHeight={this.state.imageHeight} setPoints={this.setPoints}/>} | |
</View> | |
) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment