/** * Visualizes a bbox by cropping the source page PNG via CSS background. * Uses /api/static/processing/png//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 (
{(w * 100).toFixed(0)}×{(h * 100).toFixed(0)}%
); } 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 (
{(x * 100).toFixed(0)},{(y * 100).toFixed(0)} · {(w * 100).toFixed(0)}×{(h * 100).toFixed(0)}%
); }