From 2055597ba4cef8849f70f8e728ff06cb06aaac0e Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Tue, 10 Mar 2026 13:25:16 +0100 Subject: [PATCH] fix: Pixel-Overlay fuer alle Zellen + Auto-Schriftgroesse + kein contentEditable - Auch Single-Group-Zellen (z.B. Ueberschriften) per Pixel positionieren - Auto font-size per canvas measureText (Text fuellt Cluster-Breite aus) - contentEditable entfernt (pointer-events-none), Tabelle zum Editieren - overflow:visible statt hidden verhindert Klick-Shift-Bug Co-Authored-By: Claude Opus 4.6 --- .../components/ocr-pipeline/StepLlmReview.tsx | 121 +++++++++++------- 1 file changed, 75 insertions(+), 46 deletions(-) diff --git a/admin-lehrer/components/ocr-pipeline/StepLlmReview.tsx b/admin-lehrer/components/ocr-pipeline/StepLlmReview.tsx index eecff9b..9f9161c 100644 --- a/admin-lehrer/components/ocr-pipeline/StepLlmReview.tsx +++ b/admin-lehrer/components/ocr-pipeline/StepLlmReview.tsx @@ -92,8 +92,8 @@ export function StepLlmReview({ sessionId, onNext }: StepLlmReviewProps) { const reconRef = useRef(null) const [reconWidth, setReconWidth] = useState(0) - // Pixel-analysed word positions: cell_id → [{xPct, wPct, text}] - const [cellWordPositions, setCellWordPositions] = useState>(new Map()) + // Pixel-analysed word positions: cell_id → [{xPct, wPct, text, fontRatio}] + const [cellWordPositions, setCellWordPositions] = useState>(new Map()) const tableRef = useRef(null) const activeRowRef = useRef(null) @@ -124,14 +124,17 @@ export function StepLlmReview({ sessionId, onNext }: StepLlmReviewProps) { if (!ctx) return ctx.drawImage(img, 0, 0) - const positions = new Map() + const refFontSize = 40 + const fontFam = "'Liberation Sans', Arial, sans-serif" + ctx.font = `${refFontSize}px ${fontFam}` + + const positions = new Map() for (const cell of cells) { if (!cell.bbox_pct || !cell.text) continue - // Split by 3+ whitespace — only analyse cells with multiple word-groups + // Split by 3+ whitespace into word-groups const groups = cell.text.split(/\s{3,}/).map(s => s.trim()).filter(Boolean) - if (groups.length <= 1) continue // Pixel region for this cell const imgW = img.naturalWidth @@ -177,19 +180,44 @@ export function StepLlmReview({ sessionId, onNext }: StepLlmReviewProps) { } if (inCluster) clusters.push({ start: clStart, end: cw - 1 - gap }) - // Need enough clusters for all word groups - if (clusters.length < groups.length) continue + if (clusters.length === 0) continue - // Match word-groups to clusters left-to-right - const wordPos: { xPct: number; wPct: number; text: string }[] = [] - for (let i = 0; i < groups.length; i++) { - const cl = clusters[i] + const wordPos: { xPct: number; wPct: number; text: string; fontRatio: number }[] = [] + + if (groups.length <= 1) { + // Single group: position at first cluster, merge all clusters for width + const firstCl = clusters[0] + const lastCl = clusters[clusters.length - 1] + const clusterW = lastCl.end - firstCl.start + 1 + // Auto font-size: fit text width to cluster width + const measured = ctx.measureText(cell.text.trim()) + const autoFontPx = refFontSize * (clusterW / measured.width) + const fontRatio = Math.min(autoFontPx / ch, 1.0) // ratio of cell height wordPos.push({ - xPct: cell.bbox_pct.x + (cl.start / cw) * cell.bbox_pct.w, - wPct: ((cl.end - cl.start + 1) / cw) * cell.bbox_pct.w, - text: groups[i], + xPct: cell.bbox_pct.x + (firstCl.start / cw) * cell.bbox_pct.w, + wPct: ((lastCl.end - firstCl.start + 1) / cw) * cell.bbox_pct.w, + text: cell.text.trim(), + fontRatio, }) + } else if (clusters.length >= groups.length) { + // Multiple groups: match to clusters left-to-right + for (let i = 0; i < groups.length; i++) { + const cl = clusters[i] + const clusterW = cl.end - cl.start + 1 + const measured = ctx.measureText(groups[i]) + const autoFontPx = refFontSize * (clusterW / measured.width) + const fontRatio = Math.min(autoFontPx / ch, 1.0) + wordPos.push({ + xPct: cell.bbox_pct.x + (cl.start / cw) * cell.bbox_pct.w, + wPct: ((cl.end - cl.start + 1) / cw) * cell.bbox_pct.w, + text: groups[i], + fontRatio, + }) + } + } else { + continue // fewer clusters than groups — skip } + positions.set(cell.cell_id, wordPos) } @@ -792,45 +820,46 @@ export function StepLlmReview({ sessionId, onNext }: StepLlmReviewProps) { const aspect = imageNaturalSize ? imageNaturalSize.h / imageNaturalSize.w : 4 / 3 const containerH = reconWidth * aspect const cellHeightPx = containerH * (cell.bbox_pct.h / 100) - const fontSize = Math.max(6, cellHeightPx * fontScale) const wordPos = cellWordPositions.get(cell.cell_id) - // Pixel-analysed: render each word-group at its detected position - if (wordPos && wordPos.length > 1) { - return wordPos.map((wp, i) => ( - handleCellEdit(cell.cell_id, cell.row_index, e.currentTarget.textContent)} - > - {wp.text} - - )) + // Pixel-analysed: render word-groups at detected positions + if (wordPos) { + return wordPos.map((wp, i) => { + // Auto font-size from pixel analysis, scaled by user slider + const autoFontPx = cellHeightPx * wp.fontRatio * fontScale + const fs = Math.max(6, autoFontPx) + return ( + + {wp.text} + + ) + }) } - // Fallback: single span for entire cell + // Fallback: no pixel data — single span for entire cell + const fontSize = Math.max(6, cellHeightPx * fontScale) return ( handleCellEdit(cell.cell_id, cell.row_index, e.currentTarget.textContent)} > {cell.text}