Spaces:
Sleeping
Sleeping
File size: 4,956 Bytes
8608e55 9107ac2 8608e55 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 |
/**
* Page.jsx
*
* This component displays a single page image and highlights a specific circle on it.
* - Reads query parameters `image` (page image URL) and `circle` (circle text to highlight)
* - Loads the image and calculates scale info for proper overlays
* - Sends the image to the backend detection API to get all circles
* - Finds the circle that matches the target text and highlights it
* - Supports zooming via buttons and mouse wheel
*/
import { useState, useEffect, useRef } from "react";
import { useSearchParams } from "react-router-dom";
import ZoomControls from "./ZoomControls";
import useZoom from "../hooks/useZoom";
import "../styles/Zoom.css";
function Page() {
const [searchParams] = useSearchParams();
const targetCircleText = searchParams.get("circle");
const pageImage = searchParams.get("image");
const [highlightCircle, setHighlightCircle] = useState(null);
const [imageInfo, setImageInfo] = useState(null);
const imgRef = useRef(null);
const wrapperRef = useRef(null);
const { zoom, zoomIn, zoomOut, handleWheel } = useZoom({ min: 1, max: 3, step: 0.25 });
const handleImageLoad = () => {
if (!imgRef.current) return;
const info = {
naturalWidth: imgRef.current.naturalWidth,
naturalHeight: imgRef.current.naturalHeight,
clientWidth: imgRef.current.clientWidth,
clientHeight: imgRef.current.clientHeight,
scaleX: imgRef.current.clientWidth / imgRef.current.naturalWidth,
scaleY: imgRef.current.clientHeight / imgRef.current.naturalHeight,
};
setImageInfo(info);
};
useEffect(() => {
if (!pageImage || !targetCircleText) {
setHighlightCircle(null);
return;
}
const detect = async () => {
try {
const blob = await fetch(pageImage).then((res) => res.blob());
const formData = new FormData();
formData.append("file", blob, "page.png");
const resp = await fetch("/detect/", {
method: "POST",
body: formData,
});
if (!resp.ok) throw new Error(`Detection failed: ${await resp.text()}`);
const data = await resp.json();
const circles = data.circles || [];
const targetCircle = circles.find(
(c) =>
c.circle_text &&
c.circle_text.trim().toLowerCase() === targetCircleText.trim().toLowerCase()
);
setHighlightCircle(targetCircle || null);
} catch (err) {
console.error("Detection error:", err);
setHighlightCircle(null);
}
};
detect();
}, [pageImage, targetCircleText]);
const getScaledCircle = () => {
if (!highlightCircle || !imageInfo) return null;
return {
cx: highlightCircle.x * imageInfo.scaleX,
cy: highlightCircle.y * imageInfo.scaleY,
r: highlightCircle.r * Math.min(imageInfo.scaleX, imageInfo.scaleY),
};
};
const scaledCircle = getScaledCircle();
return (
<div style={{ position: "relative", width: "100%", height: "100%" }}>
<ZoomControls zoom={zoom} zoomIn={zoomIn} zoomOut={zoomOut} />
{pageImage && (
<div
className="image-container"
style={{ position: "relative", display: "inline-block", overflow: "auto" }}
onWheel={handleWheel}
>
<div
ref={wrapperRef}
className="zoom-wrapper"
style={{
position: "relative",
width: imageInfo ? imageInfo.clientWidth : "auto",
height: imageInfo ? imageInfo.clientHeight : "auto",
transform: `scale(${zoom})`,
transformOrigin: "0 0",
transition: "transform 120ms ease-out",
}}
>
<img
ref={imgRef}
src={pageImage}
alt="Page"
onLoad={handleImageLoad}
style={{
width: imageInfo ? imageInfo.clientWidth : "100%",
height: imageInfo ? imageInfo.clientHeight : "auto",
display: "block",
userSelect: "none",
}}
onError={(e) => {
console.error("Image failed to load:", pageImage);
e.target.alt = "Failed to load image";
}}
/>
{/* Circle overlay */}
{scaledCircle && (
<svg
width={imageInfo?.clientWidth || 0}
height={imageInfo?.clientHeight || 0}
style={{ position: "absolute", top: 0, left: 0, pointerEvents: "none" }}
>
<circle
cx={scaledCircle.cx}
cy={scaledCircle.cy}
r={scaledCircle.r}
stroke="blue"
strokeWidth="3"
fill="none"
/>
</svg>
)}
</div>
</div>
)}
</div>
);
}
export default Page; |