Files
solid-data/src/KTPScanner.js
zadit biasa aja aa734e1fe2 ok
2025-06-29 13:04:45 +00:00

492 lines
14 KiB
JavaScript

import React, { useEffect, useRef, useState } from "react";
import Modal from "./Modal";
import FormComponent from "./FormComponent";
const STORAGE_KEY = "camera_canvas_gallery";
const CameraCanvas = () => {
const videoRef = useRef(null);
const canvasRef = useRef(null);
const hiddenCanvasRef = useRef(null);
const [capturedImage, setCapturedImage] = useState(null);
const [galleryImages, setGalleryImages] = useState([]);
const [fileTemp, setFileTemp] = useState(null);
const [isFreeze, setIsFreeze] = useState(false);
const freezeFrameRef = useRef(null);
const [modalOpen, setModalOpen] = useState(false);
const [loading, setLoading] = useState(false);
const fileInputRef = useRef(null);
const triggerFileSelect = () => {
fileInputRef.current?.click();
};
const rectRef = useRef({
x: 0,
y: 0,
width: 0,
height: 0,
radius: 20,
});
const drawRoundedRect = (ctx, x, y, width, height, radius) => {
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.lineTo(x + width - radius, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
ctx.lineTo(x + width, y + height - radius);
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
ctx.lineTo(x + radius, y + height);
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
ctx.lineTo(x, y + radius);
ctx.quadraticCurveTo(x, y, x + radius, y);
ctx.closePath();
ctx.strokeStyle = "red";
ctx.lineWidth = 3;
ctx.stroke();
};
const fillOutsideRect = (ctx, rect, canvasWidth, canvasHeight) => {
ctx.save();
const { x, y, width, height, radius } = rect;
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.lineTo(x + width - radius, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
ctx.lineTo(x + width, y + height - radius);
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
ctx.lineTo(x + radius, y + height);
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
ctx.lineTo(x, y + radius);
ctx.quadraticCurveTo(x, y, x + radius, y);
ctx.closePath();
ctx.rect(0, 0, canvasWidth, canvasHeight);
ctx.fillStyle = "rgba(173, 173, 173, 1)";
ctx.fill("evenodd");
ctx.restore();
};
useEffect(() => {
const savedGallery = localStorage.getItem(STORAGE_KEY);
if (savedGallery) setGalleryImages(JSON.parse(savedGallery));
const getCameraStream = async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: { ideal: "environment" } },
audio: false,
});
if (videoRef.current) {
videoRef.current.srcObject = stream;
videoRef.current.onloadedmetadata = () => {
videoRef.current.play();
const video = videoRef.current;
const canvas = canvasRef.current;
const hiddenCanvas = hiddenCanvasRef.current;
const ctx = canvas.getContext("2d");
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
canvas.style.maxWidth = "100%";
canvas.style.height = "auto";
hiddenCanvas.width = video.videoWidth;
hiddenCanvas.height = video.videoHeight;
const rectWidth = canvas.width * 0.9;
const rectHeight = (53.98 / 85.6) * rectWidth;
const rectX = (canvas.width - rectWidth) / 2;
const rectY = (canvas.height - rectHeight) / 2;
rectRef.current = {
x: rectX,
y: rectY,
width: rectWidth,
height: rectHeight,
radius: 20,
};
const drawToCanvas = () => {
if (video.readyState === 4) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (isFreeze && freezeFrameRef.current) {
ctx.putImageData(freezeFrameRef.current, 0, 0);
} else {
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
}
drawRoundedRect(
ctx,
rectRef.current.x,
rectRef.current.y,
rectRef.current.width,
rectRef.current.height,
rectRef.current.radius
);
if (isFreeze) {
fillOutsideRect(
ctx,
rectRef.current,
canvas.width,
canvas.height
);
}
}
requestAnimationFrame(drawToCanvas);
};
drawToCanvas();
};
}
} catch (err) {
console.error("Gagal mendapatkan kamera:", err);
}
};
getCameraStream();
}, [isFreeze]);
const shootImage = () => {
const video = videoRef.current;
const { x, y, width, height } = rectRef.current;
const hiddenCanvas = hiddenCanvasRef.current;
const hiddenCtx = hiddenCanvas.getContext("2d");
const visibleCtx = canvasRef.current.getContext("2d");
freezeFrameRef.current = visibleCtx.getImageData(
0,
0,
canvasRef.current.width,
canvasRef.current.height
);
setIsFreeze(true);
hiddenCtx.drawImage(video, 0, 0, hiddenCanvas.width, hiddenCanvas.height);
const cropCanvas = document.createElement("canvas");
cropCanvas.width = Math.floor(width);
cropCanvas.height = Math.floor(height);
const cropCtx = cropCanvas.getContext("2d");
cropCtx.drawImage(
hiddenCanvas,
Math.floor(x),
Math.floor(y),
Math.floor(width),
Math.floor(height),
0,
0,
Math.floor(width),
Math.floor(height)
);
const imageDataUrl = cropCanvas.toDataURL("image/png", 1.0);
setCapturedImage(imageDataUrl);
};
const ReadImage = async (capturedImage) => {
try {
setLoading(true);
setModalOpen(true);
let res = await fetch(
"https://bot.kediritechnopark.com/webhook/mastersnapper/read",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ image: capturedImage }),
}
);
setLoading(false);
const data = await res.json();
console.log(data);
setFileTemp(data);
} catch (error) {
console.error("Failed to read image:", error);
}
};
const handleSaveTemp = async (correctedData) => {
try {
await fetch(
"https://bot.kediritechnopark.com/webhook/mastersnapper/save",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ correctedData }),
}
);
const updatedGallery = [fileTemp, ...galleryImages];
setGalleryImages(updatedGallery);
localStorage.setItem(STORAGE_KEY, JSON.stringify(updatedGallery));
setFileTemp(null);
} catch (err) {
console.error("Gagal menyimpan ke server:", err);
}
};
const handleDeleteTemp = async () => {
try {
await fetch(
"https://bot.kediritechnopark.com/webhook/mastersnapper/delete",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ fileTemp }),
}
);
setFileTemp(null);
} catch (err) {
console.error("Gagal menghapus dari server:", err);
}
};
const removeImage = (index) => {
const newGallery = [...galleryImages];
newGallery.splice(index, 1);
setGalleryImages(newGallery);
localStorage.setItem(STORAGE_KEY, JSON.stringify(newGallery));
};
const aspectRatio = 53.98 / 85.6;
const handleManualUpload = async (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onloadend = () => {
const imageDataUrl = reader.result;
setCapturedImage(imageDataUrl);
setIsFreeze(true);
// Create an image object from the uploaded file
const image = new Image();
image.onload = () => {
// Get the width of the rounded rectangle from rectRef
const rectWidth = rectRef.current.width;
const rectHeight = rectRef.current.height;
// Create a canvas to draw the uploaded image
const canvas = canvasRef.current;
const ctx = canvas.getContext("2d");
// Set the scale factor based on the rectangle width
const scaleFactor = rectWidth / image.width;
// Calculate the new height based on the aspect ratio
const newHeight = image.height * scaleFactor;
// Clear the canvas and draw the video or freeze frame
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (isFreeze && freezeFrameRef.current) {
ctx.putImageData(freezeFrameRef.current, 0, 0);
} else {
const video = videoRef.current;
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
}
// Draw the rounded rectangle
drawRoundedRect(
ctx,
rectRef.current.x,
rectRef.current.y,
rectRef.current.width,
rectRef.current.height,
rectRef.current.radius
);
// Draw the image inside the rounded rectangle
ctx.save();
ctx.beginPath();
ctx.moveTo(
rectRef.current.x + rectRef.current.radius,
rectRef.current.y
);
ctx.lineTo(
rectRef.current.x + rectRef.current.width - rectRef.current.radius,
rectRef.current.y
);
ctx.quadraticCurveTo(
rectRef.current.x + rectRef.current.width,
rectRef.current.y,
rectRef.current.x + rectRef.current.width,
rectRef.current.y + rectRef.current.radius
);
ctx.lineTo(
rectRef.current.x + rectRef.current.width,
rectRef.current.y + rectRef.current.height - rectRef.current.radius
);
ctx.quadraticCurveTo(
rectRef.current.x + rectRef.current.width,
rectRef.current.y + rectRef.current.height,
rectRef.current.x + rectRef.current.width - rectRef.current.radius,
rectRef.current.y + rectRef.current.height
);
ctx.lineTo(
rectRef.current.x + rectRef.current.radius,
rectRef.current.y + rectRef.current.height
);
ctx.quadraticCurveTo(
rectRef.current.x,
rectRef.current.y + rectRef.current.height,
rectRef.current.x,
rectRef.current.y + rectRef.current.height - rectRef.current.radius
);
ctx.lineTo(
rectRef.current.x,
rectRef.current.y + rectRef.current.radius
);
ctx.quadraticCurveTo(
rectRef.current.x,
rectRef.current.y,
rectRef.current.x + rectRef.current.radius,
rectRef.current.y
);
ctx.closePath();
ctx.clip(); // Clip the image within the rounded rectangle
// Draw the uploaded image inside the clipped region
ctx.drawImage(
image,
rectRef.current.x,
rectRef.current.y,
rectWidth,
newHeight // Height is scaled based on the image's aspect ratio
);
ctx.restore();
// Save the image data into the freeze frame reference
freezeFrameRef.current = ctx.getImageData(
0,
0,
canvas.width,
canvas.height
);
};
image.src = imageDataUrl;
};
reader.readAsDataURL(file);
};
return (
<div>
<div
style={{
display: "flex",
alignItems: "center",
padding: "12px 16px",
backgroundColor: "#f5f5f5",
fontFamily: "sans-serif",
fontSize: "16px",
fontWeight: "bold",
}}
>
<button
style={{
marginRight: "12px",
fontSize: "18px",
background: "none",
border: "none",
cursor: "pointer",
}}
>
&lt;
</button>
<div>Scan KTP atau unggah</div>
</div>
<video
ref={videoRef}
autoPlay
playsInline
muted
style={{ display: "none" }}
/>
<canvas ref={canvasRef} style={{ maxWidth: "100%", height: "auto" }} />
<canvas ref={hiddenCanvasRef} style={{ display: "none" }} />
<div
style={{
backgroundColor: "white",
borderRadius: 16,
textAlign: "center",
top: "-17px",
position: "relative",
padding: "20px",
}}
>
<h2 style={{ marginTop: 0 }}>Data Verification</h2>
{!isFreeze ? (
<>
<div
style={{
padding: 10,
backgroundColor: "#ff6d6d",
borderRadius: 15,
color: "white",
fontWeight: "bold",
}}
onClick={shootImage}
>
Ambil Gambar
</div>
<div>atau</div>
<div
style={{
padding: 10,
backgroundColor: "#ff6d6d",
borderRadius: 15,
color: "white",
fontWeight: "bold",
}}
onClick={triggerFileSelect}
>
Upload Gambar
</div>
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={(e) => handleManualUpload(e)}
style={{ marginRight: 10, display: "none" }}
/>
</>
) : (
<div
style={{
padding: 10,
backgroundColor: "#ff6d6d",
borderRadius: 15,
color: "white",
fontWeight: "bold",
}}
onClick={() => ReadImage(capturedImage)}
>
Scan KTP
</div>
)}
</div>
<Modal
isOpen={modalOpen}
onClose={() => setModalOpen(false)}
loading={loading}
fileTemp={fileTemp}
onSave={handleSaveTemp}
onDelete={handleDeleteTemp}
/>
</div>
);
};
export default CameraCanvas;