disclosure-bureau/web/components/fm/bbox-thumb.tsx

78 lines
2.5 KiB
TypeScript
Raw Permalink Normal View History

/**
* 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>
);
}