import type { Body, Meta, UppyFile } from '@uppy/core' import type { I18n } from '@uppy/utils' import Cropper from 'cropperjs' import { Component } from 'preact' import type ImageEditor from './ImageEditor.js' import getCanvasDataThatFitsPerfectlyIntoContainer from './utils/getCanvasDataThatFitsPerfectlyIntoContainer.js' import getScaleFactorThatRemovesDarkCorners from './utils/getScaleFactorThatRemovesDarkCorners.js' import limitCropboxMovementOnMove from './utils/limitCropboxMovementOnMove.js' import limitCropboxMovementOnResize from './utils/limitCropboxMovementOnResize.js' type Props = { currentImage: UppyFile storeCropperInstance: (cropper: Cropper) => void opts: ImageEditor['opts'] i18n: I18n save: () => void } type State = { angle90Deg: number angleGranular: number prevCropboxData: Cropper.CropBoxData | null } export default class Editor extends Component< Props, State > { imgElement!: HTMLImageElement cropper!: Cropper constructor(props: Props) { super(props) this.state = { angle90Deg: 0, angleGranular: 0, prevCropboxData: null, } this.storePrevCropboxData = this.storePrevCropboxData.bind(this) this.limitCropboxMovement = this.limitCropboxMovement.bind(this) } componentDidMount(): void { const { opts, storeCropperInstance } = this.props this.cropper = new Cropper(this.imgElement, opts.cropperOptions) this.imgElement.addEventListener('cropstart', this.storePrevCropboxData) // @ts-expect-error custom cropper event but DOM API does not understand this.imgElement.addEventListener('cropend', this.limitCropboxMovement) storeCropperInstance(this.cropper) } componentWillUnmount(): void { this.cropper.destroy() this.imgElement.removeEventListener('cropstart', this.storePrevCropboxData) // @ts-expect-error custom cropper event but DOM API does not understand this.imgElement.removeEventListener('cropend', this.limitCropboxMovement) } storePrevCropboxData(): void { this.setState({ prevCropboxData: this.cropper.getCropBoxData() }) } limitCropboxMovement(event: { detail: { action: string } }): void { const canvasData = this.cropper.getCanvasData() const cropboxData = this.cropper.getCropBoxData() const { prevCropboxData } = this.state // 1. When we grab the cropbox in the middle and move it if (event.detail.action === 'all') { const newCropboxData = limitCropboxMovementOnMove( canvasData, cropboxData, prevCropboxData!, ) if (newCropboxData) this.cropper.setCropBoxData(newCropboxData) // 2. When we stretch the cropbox by one of its sides } else { const newCropboxData = limitCropboxMovementOnResize( canvasData, cropboxData, prevCropboxData!, ) if (newCropboxData) this.cropper.setCropBoxData(newCropboxData) } } onRotate90Deg = (): void => { // 1. Set state const { angle90Deg } = this.state const newAngle = angle90Deg - 90 this.setState({ angle90Deg: newAngle, angleGranular: 0, }) // 2. Rotate the image // Important to reset scale here, or cropper will get confused on further rotations this.cropper.scale(1) this.cropper.rotateTo(newAngle) // 3. Fit the rotated image into the view const canvasData = this.cropper.getCanvasData() const containerData = this.cropper.getContainerData() const newCanvasData = getCanvasDataThatFitsPerfectlyIntoContainer( containerData, canvasData, ) this.cropper.setCanvasData(newCanvasData) // 4. Make cropbox fully wrap the image this.cropper.setCropBoxData(newCanvasData) } onRotateGranular = (ev: Event): void => { // 1. Set state const newGranularAngle = Number((ev.target as HTMLInputElement).value) this.setState({ angleGranular: newGranularAngle }) // 2. Rotate the image const { angle90Deg } = this.state const newAngle = angle90Deg + newGranularAngle this.cropper.rotateTo(newAngle) // 3. Scale the image so that it fits into the cropbox const image = this.cropper.getImageData() const scaleFactor = getScaleFactorThatRemovesDarkCorners( image.naturalWidth, image.naturalHeight, newGranularAngle, ) // Preserve flip const scaleFactorX = this.cropper.getImageData().scaleX < 0 ? -scaleFactor : scaleFactor this.cropper.scale(scaleFactorX, scaleFactor) } renderGranularRotate() { const { i18n } = this.props const { angleGranular } = this.state return ( ) } renderRevert() { const { i18n, opts } = this.props return ( ) } renderRotate() { const { i18n } = this.props return ( ) } renderFlip() { const { i18n } = this.props return ( ) } renderZoomIn() { const { i18n } = this.props return ( ) } renderZoomOut() { const { i18n } = this.props return ( ) } renderCropSquare() { const { i18n } = this.props return ( ) } renderCropWidescreen() { const { i18n } = this.props return ( ) } renderCropWidescreenVertical() { const { i18n } = this.props return ( ) } render() { const { currentImage, opts } = this.props const { actions } = opts const imageURL = URL.createObjectURL(currentImage.data) return (
{currentImage.name} { this.imgElement = ref as HTMLImageElement }} />
{actions.revert && this.renderRevert()} {actions.rotate && this.renderRotate()} {actions.granularRotate && this.renderGranularRotate()} {actions.flip && this.renderFlip()} {actions.zoomIn && this.renderZoomIn()} {actions.zoomOut && this.renderZoomOut()} {actions.cropSquare && this.renderCropSquare()} {actions.cropWidescreen && this.renderCropWidescreen()} {actions.cropWidescreenVertical && this.renderCropWidescreenVertical()}
) } }