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

77 lines
2.5 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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