|
| 1 | +import React, { useState, useCallback, useRef, useEffect } from 'react'; |
| 2 | +import { useTranslation } from 'react-i18next'; |
| 3 | +import { Button, TextField } from '@mui/material'; |
| 4 | +import { get } from 'lodash'; |
| 5 | +import ReactCrop from 'react-image-crop'; |
| 6 | +import "react-image-crop/dist/ReactCrop.css"; |
| 7 | + |
| 8 | +// Increase pixel density for crop preview quality on retina screens. |
| 9 | + |
| 10 | +const pixelRatio = window.devicePixelRatio || 1; |
| 11 | + |
| 12 | +// We resize the canvas down when saving on retina devices otherwise the image |
| 13 | +// will be double or triple the preview size. |
| 14 | +/* const getResizedCanvas = (canvas, newWidth, newHeight) => { |
| 15 | + * const tmpCanvas = document.createElement("canvas"); |
| 16 | + * tmpCanvas.width = newWidth; |
| 17 | + * tmpCanvas.height = newHeight; |
| 18 | + * |
| 19 | + * const ctx = tmpCanvas.getContext("2d"); |
| 20 | + * ctx.drawImage( |
| 21 | + * canvas, |
| 22 | + * 0, |
| 23 | + * 0, |
| 24 | + * canvas.width, |
| 25 | + * canvas.height, |
| 26 | + * 0, |
| 27 | + * 0, |
| 28 | + * newWidth, |
| 29 | + * newHeight |
| 30 | + * ); |
| 31 | + * |
| 32 | + * return tmpCanvas; |
| 33 | + * } |
| 34 | + * */ |
| 35 | +/* const generateDownload = (previewCanvas, crop) => { |
| 36 | + * if (!crop || !previewCanvas) { |
| 37 | + * return; |
| 38 | + * } |
| 39 | + * |
| 40 | + * const canvas = getResizedCanvas(previewCanvas, crop.width, crop.height); |
| 41 | + * |
| 42 | + * canvas.toBlob( |
| 43 | + * (blob) => { |
| 44 | + * const previewUrl = window.URL.createObjectURL(blob); |
| 45 | + * |
| 46 | + * const anchor = document.createElement("a"); |
| 47 | + * anchor.download = "cropPreview.png"; |
| 48 | + * anchor.href = URL.createObjectURL(blob); |
| 49 | + * anchor.click(); |
| 50 | + * |
| 51 | + * window.URL.revokeObjectURL(previewUrl); |
| 52 | + * }, |
| 53 | + * "image/png", |
| 54 | + * 1 |
| 55 | + * ); |
| 56 | + * } |
| 57 | + * */ |
| 58 | + |
| 59 | +const ImageUploader = props => { |
| 60 | + const { t } = useTranslation() |
| 61 | + const [fileName, setFileName] = useState(props.defaultName); |
| 62 | + const [upImg, setUpImg] = useState(props.defaultImg); |
| 63 | + const imgRef = useRef(null); |
| 64 | + const previewCanvasRef = useRef(null); |
| 65 | + const [crop, setCrop] = useState({ unit: "%", width: 30, height: props.isCircle ? undefined : 30, aspect: props.isCircle ? 1 : undefined }); |
| 66 | + const [completedCrop, setCompletedCrop] = useState(null); |
| 67 | + const [base64, setBase64] = useState(null); |
| 68 | + |
| 69 | + const onSelectFile = (e) => { |
| 70 | + if (e.target.files && e.target.files.length > 0) { |
| 71 | + const reader = new FileReader(); |
| 72 | + reader.addEventListener("load", () => setUpImg(reader.result)); |
| 73 | + const file = e.target.files[0]; |
| 74 | + setFileName(file.name) |
| 75 | + reader.readAsDataURL(e.target.files[0]); |
| 76 | + } |
| 77 | + }; |
| 78 | + |
| 79 | + const onLoad = useCallback((img) => { |
| 80 | + imgRef.current = img; |
| 81 | + }, []); |
| 82 | + |
| 83 | + const uploadImage = () => { |
| 84 | + props.onUpload(base64, fileName) |
| 85 | + } |
| 86 | + |
| 87 | + useEffect(() => { |
| 88 | + if (!completedCrop || !previewCanvasRef.current || !imgRef.current) { |
| 89 | + return; |
| 90 | + } |
| 91 | + |
| 92 | + const image = imgRef.current; |
| 93 | + const canvas = previewCanvasRef.current; |
| 94 | + const crop = completedCrop; |
| 95 | + |
| 96 | + const scaleX = image.naturalWidth / image.width; |
| 97 | + const scaleY = image.naturalHeight / image.height; |
| 98 | + const ctx = canvas.getContext("2d"); |
| 99 | + |
| 100 | + canvas.width = crop.width * pixelRatio; |
| 101 | + canvas.height = crop.height * pixelRatio; |
| 102 | + |
| 103 | + ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0); |
| 104 | + ctx.imageSmoothingQuality = "high"; |
| 105 | + |
| 106 | + ctx.drawImage( |
| 107 | + image, |
| 108 | + crop.x * scaleX, |
| 109 | + crop.y * scaleY, |
| 110 | + crop.width * scaleX, |
| 111 | + crop.height * scaleY, |
| 112 | + 0, |
| 113 | + 0, |
| 114 | + crop.width, |
| 115 | + crop.height |
| 116 | + ); |
| 117 | + setBase64(canvas.toDataURL()); |
| 118 | + }, [completedCrop]); |
| 119 | + |
| 120 | + return ( |
| 121 | + <div className="App"> |
| 122 | + <div className='flex-vertical-center'> |
| 123 | + <TextField |
| 124 | + variant="outlined" |
| 125 | + inputProps={{ |
| 126 | + type: "file", |
| 127 | + accept: "image/*" |
| 128 | + }} |
| 129 | + onChange={onSelectFile} |
| 130 | + /> |
| 131 | + <Button |
| 132 | + style={{marginLeft: '10px'}} |
| 133 | + variant='outlined' |
| 134 | + color='primary' |
| 135 | + disabled={!get(completedCrop, 'width') || !get(completedCrop, 'height')} |
| 136 | + onClick={uploadImage} |
| 137 | + > |
| 138 | + {t('common.upload')} |
| 139 | + </Button> |
| 140 | + </div> |
| 141 | + <div style={{marginTop: '10px'}}> |
| 142 | + <ReactCrop |
| 143 | + src={upImg} |
| 144 | + onImageLoaded={onLoad} |
| 145 | + crop={crop} |
| 146 | + onChange={(c) => setCrop(c)} |
| 147 | + onComplete={(c) => setCompletedCrop(c)} |
| 148 | + crossorigin='Anonymous' |
| 149 | + circularCrop={props.isCircle} |
| 150 | + /> |
| 151 | + </div> |
| 152 | + <div style={{display: 'none'}}> |
| 153 | + <canvas |
| 154 | + ref={previewCanvasRef} |
| 155 | + style={{ |
| 156 | + width: Math.round(get(completedCrop, 'width', 0)), |
| 157 | + height: Math.round(get(completedCrop, 'height', 0)) |
| 158 | + }} |
| 159 | + /> |
| 160 | + </div> |
| 161 | + </div> |
| 162 | + ); |
| 163 | +} |
| 164 | + |
| 165 | +export default ImageUploader; |
0 commit comments