feat: Echtes Overlay — Text direkt ueber dem Originalbild
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 33s
CI / test-go-edu-search (push) Successful in 36s
CI / test-python-klausur (push) Failing after 2m11s
CI / test-python-agent-core (push) Successful in 25s
CI / test-nodejs-website (push) Successful in 26s

Statt Side-by-Side wird der erkannte Text jetzt direkt ueber das
Originalbild gelegt. Textfarbe (rot/blau/schwarz) und Deckkraft
per Slider einstellbar fuer einfache visuelle Fehlersuche.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-11 00:25:11 +01:00
parent e2ad93fd57
commit 2df2a01a8b

View File

@@ -40,6 +40,8 @@ export function OverlayReconstruction({ sessionId, onNext }: OverlayReconstructi
const [fontScale, setFontScale] = useState(0.7) const [fontScale, setFontScale] = useState(0.7)
const [globalBold, setGlobalBold] = useState(false) const [globalBold, setGlobalBold] = useState(false)
const [imageRotation, setImageRotation] = useState<0 | 180>(0) const [imageRotation, setImageRotation] = useState<0 | 180>(0)
const [textOpacity, setTextOpacity] = useState(100)
const [textColor, setTextColor] = useState<'red' | 'blue' | 'black'>('red')
const reconRef = useRef<HTMLDivElement>(null) const reconRef = useRef<HTMLDivElement>(null)
const [reconWidth, setReconWidth] = useState(0) const [reconWidth, setReconWidth] = useState(0)
@@ -381,6 +383,34 @@ export function OverlayReconstruction({ sessionId, onNext }: OverlayReconstructi
<div className="w-px h-5 bg-gray-300 dark:bg-gray-600 mx-1" /> <div className="w-px h-5 bg-gray-300 dark:bg-gray-600 mx-1" />
{/* Text color */}
{(['red', 'blue', 'black'] as const).map(c => (
<button
key={c}
onClick={() => setTextColor(c)}
className={`w-5 h-5 rounded-full border-2 transition-colors ${
textColor === c ? 'border-teal-500 ring-1 ring-teal-300' : 'border-gray-300 dark:border-gray-600'
}`}
style={{ backgroundColor: c === 'black' ? '#1a1a1a' : c }}
title={`Textfarbe: ${c}`}
/>
))}
<div className="w-px h-5 bg-gray-300 dark:bg-gray-600 mx-1" />
{/* Text opacity */}
<label className="flex items-center gap-1 text-xs text-gray-600 dark:text-gray-400">
Text
<input
type="range" min={0} max={100} value={textOpacity}
onChange={e => setTextOpacity(Number(e.target.value))}
className="w-16 h-1 accent-teal-600"
/>
<span className="w-8 text-right font-mono">{textOpacity}%</span>
</label>
<div className="w-px h-5 bg-gray-300 dark:bg-gray-600 mx-1" />
<button <button
onClick={saveReconstruction} onClick={saveReconstruction}
disabled={status === 'saving'} disabled={status === 'saving'}
@@ -391,43 +421,35 @@ export function OverlayReconstruction({ sessionId, onNext }: OverlayReconstructi
</div> </div>
</div> </div>
{/* Side-by-side: Original + Overlay */} {/* True overlay: text layer on top of original image */}
<div className="grid grid-cols-2 gap-4"> <div className="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden bg-gray-50 dark:bg-gray-900">
{/* Left: Original image */}
<div>
<div className="text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">
Originalbild
</div>
<div className="border rounded-lg overflow-hidden dark:border-gray-700 bg-gray-50 dark:bg-gray-900 sticky top-4">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={dewarpedUrl}
alt="Original"
className="w-full h-auto"
onLoad={(e) => {
const img = e.target as HTMLImageElement
setImageNaturalSize({ w: img.naturalWidth, h: img.naturalHeight })
}}
/>
</div>
</div>
{/* Right: Reconstructed overlay */}
<div>
<div className="text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">
Rekonstruktion ({cells.length} Zellen)
</div>
<div className="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden bg-white">
<div <div
ref={reconRef} ref={reconRef}
className="relative" className="relative"
style={{ aspectRatio: `${imgW} / ${imgH}` }} style={{ aspectRatio: `${imgW} / ${imgH}` }}
>
{/* Background: original image */}
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={dewarpedUrl}
alt="Original"
className="absolute inset-0 w-full h-full object-contain"
onLoad={(e) => {
const img = e.target as HTMLImageElement
setImageNaturalSize({ w: img.naturalWidth, h: img.naturalHeight })
}}
/>
{/* Text overlay layer */}
<div
className="absolute inset-0"
style={{ opacity: textOpacity / 100 }}
> >
{/* Row lines */} {/* Row lines */}
{rows.map((row, i) => ( {rows.map((row, i) => (
<div <div
key={`row-${i}`} key={`row-${i}`}
className="absolute left-0 right-0 border-t border-gray-300/50" className="absolute left-0 right-0 border-t border-cyan-400/40"
style={{ top: `${(row.y / imgH) * 100}%` }} style={{ top: `${(row.y / imgH) * 100}%` }}
/> />
))} ))}
@@ -439,6 +461,7 @@ export function OverlayReconstruction({ sessionId, onNext }: OverlayReconstructi
const wordPos = cellWordPositions.get(cell.cellId) const wordPos = cellWordPositions.get(cell.cellId)
const bboxPct = cell.bboxPct const bboxPct = cell.bboxPct
const cellHeightPx = containerH * (bboxPct.h / 100) const cellHeightPx = containerH * (bboxPct.h / 100)
const colorValue = textColor === 'black' ? '#1a1a1a' : textColor
// Pixel-analysed: render word-groups at detected positions // Pixel-analysed: render word-groups at detected positions
if (wordPos && wordPos.length > 0) { if (wordPos && wordPos.length > 0) {
@@ -463,7 +486,7 @@ export function OverlayReconstruction({ sessionId, onNext }: OverlayReconstructi
alignItems: 'center', alignItems: 'center',
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
overflow: 'visible', overflow: 'visible',
color: '#1a1a1a', color: colorValue,
}} }}
> >
{wp.text} {wp.text}
@@ -492,7 +515,7 @@ export function OverlayReconstruction({ sessionId, onNext }: OverlayReconstructi
fontWeight: globalBold ? 'bold' : 'normal', fontWeight: globalBold ? 'bold' : 'normal',
fontFamily: "'Liberation Sans', Arial, sans-serif", fontFamily: "'Liberation Sans', Arial, sans-serif",
lineHeight: '1', lineHeight: '1',
color: '#1a1a1a', color: colorValue,
}} }}
title={`${cell.cellId} (${cell.colType})`} title={`${cell.cellId} (${cell.colType})`}
/> />
@@ -535,7 +558,7 @@ export function OverlayReconstruction({ sessionId, onNext }: OverlayReconstructi
fontWeight: globalBold ? 'bold' : 'normal', fontWeight: globalBold ? 'bold' : 'normal',
fontFamily: "'Liberation Sans', Arial, sans-serif", fontFamily: "'Liberation Sans', Arial, sans-serif",
lineHeight: '1', lineHeight: '1',
color: '#1a1a1a', color: colorValue,
}} }}
title={`${cell.cellId} (${cell.colType})`} title={`${cell.cellId} (${cell.colType})`}
/> />
@@ -554,7 +577,6 @@ export function OverlayReconstruction({ sessionId, onNext }: OverlayReconstructi
</div> </div>
</div> </div>
</div> </div>
</div>
{/* Bottom action */} {/* Bottom action */}
<div className="flex justify-end"> <div className="flex justify-end">