77 lines
2.5 KiB
TypeScript
77 lines
2.5 KiB
TypeScript
/**
|
||
* Visualizes a bbox by cropping the source page PNG via CSS background.
|
||
* Uses /api/static/processing/png/<doc>/p-NNN.png as the image source.
|
||
*
|
||
* Falls back to a simple coordinates-only badge if PNG missing.
|
||
*/
|
||
import type { BBox } from "@/lib/fm-types";
|
||
|
||
export function FmBboxThumb({
|
||
bbox,
|
||
docId,
|
||
pageNum,
|
||
width = 96,
|
||
height = 96,
|
||
label,
|
||
}: {
|
||
bbox?: BBox;
|
||
docId?: string;
|
||
pageNum?: number;
|
||
width?: number;
|
||
height?: number;
|
||
label?: string;
|
||
}) {
|
||
if (!bbox) return null;
|
||
const { x, y, w, h } = bbox;
|
||
if ([x, y, w, h].some((v) => v === undefined || Number.isNaN(v))) return null;
|
||
|
||
if (!docId || pageNum === undefined) {
|
||
return (
|
||
<div
|
||
className="inline-flex items-center justify-center font-mono text-[9px] text-[#7fdbff] border border-[rgba(127,219,255,0.32)] rounded bg-[#060a13]"
|
||
style={{ width, height }}
|
||
title={label}
|
||
>
|
||
{(w * 100).toFixed(0)}×{(h * 100).toFixed(0)}%
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const padded = String(pageNum).padStart(3, "0");
|
||
const src = `/api/static/processing/png/${docId}/p-${padded}.png`;
|
||
// bbox is normalized 0..1 — to crop, we scale the source so the bbox fills our thumb.
|
||
//
|
||
// CSS background-position with % uses:
|
||
// pixel_offset = pct/100 * (container - image)
|
||
// We need the bbox top-left (x*image_w, y*image_h) at container origin (0,0).
|
||
// Solving: x_target_px = x * image_w = pct/100 * (container - image)
|
||
// With image = container/w (from backgroundSize: 1/w):
|
||
// pct = (x / (1 - w)) * 100 (and similarly for y)
|
||
// Edge case w=1 → bbox covers full width → no shift needed.
|
||
const scaleX = 1 / w;
|
||
const scaleY = 1 / h;
|
||
const bgX = w >= 1 ? "0%" : `${(x / (1 - w)) * 100}%`;
|
||
const bgY = h >= 1 ? "0%" : `${(y / (1 - h)) * 100}%`;
|
||
|
||
return (
|
||
<div
|
||
className="relative inline-block border border-[rgba(127,219,255,0.32)] rounded overflow-hidden bg-[#060a13]"
|
||
style={{ width, height }}
|
||
title={label}
|
||
>
|
||
<div
|
||
style={{
|
||
width: "100%",
|
||
height: "100%",
|
||
backgroundImage: `url(${src})`,
|
||
backgroundRepeat: "no-repeat",
|
||
backgroundSize: `${scaleX * 100}% ${scaleY * 100}%`,
|
||
backgroundPosition: `${bgX} ${bgY}`,
|
||
}}
|
||
/>
|
||
<div className="absolute bottom-0 left-0 right-0 font-mono text-[8px] text-[#7fdbff] bg-black/60 px-1 py-0.5 truncate">
|
||
{(x * 100).toFixed(0)},{(y * 100).toFixed(0)} · {(w * 100).toFixed(0)}×{(h * 100).toFixed(0)}%
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|