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 [globalBold, setGlobalBold] = useState(false)
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 [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" />
{/* 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
onClick={saveReconstruction}
disabled={status === 'saving'}
@@ -391,167 +421,159 @@ export function OverlayReconstruction({ sessionId, onNext }: OverlayReconstructi
</div>
</div>
{/* Side-by-side: Original + Overlay */}
<div className="grid grid-cols-2 gap-4">
{/* 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>
{/* True overlay: text layer on top of original image */}
<div className="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden bg-gray-50 dark:bg-gray-900">
<div
ref={reconRef}
className="relative"
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 })
}}
/>
{/* 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
ref={reconRef}
className="relative"
style={{ aspectRatio: `${imgW} / ${imgH}` }}
>
{/* Row lines */}
{rows.map((row, i) => (
<div
key={`row-${i}`}
className="absolute left-0 right-0 border-t border-gray-300/50"
style={{ top: `${(row.y / imgH) * 100}%` }}
/>
))}
{/* Text overlay layer */}
<div
className="absolute inset-0"
style={{ opacity: textOpacity / 100 }}
>
{/* Row lines */}
{rows.map((row, i) => (
<div
key={`row-${i}`}
className="absolute left-0 right-0 border-t border-cyan-400/40"
style={{ top: `${(row.y / imgH) * 100}%` }}
/>
))}
{/* Pixel-positioned words / editable inputs */}
{cells.map((cell) => {
const displayText = getDisplayText(cell)
const edited = isEdited(cell)
const wordPos = cellWordPositions.get(cell.cellId)
const bboxPct = cell.bboxPct
const cellHeightPx = containerH * (bboxPct.h / 100)
{/* Pixel-positioned words / editable inputs */}
{cells.map((cell) => {
const displayText = getDisplayText(cell)
const edited = isEdited(cell)
const wordPos = cellWordPositions.get(cell.cellId)
const bboxPct = cell.bboxPct
const cellHeightPx = containerH * (bboxPct.h / 100)
const colorValue = textColor === 'black' ? '#1a1a1a' : textColor
// Pixel-analysed: render word-groups at detected positions
if (wordPos && wordPos.length > 0) {
return wordPos.map((wp, i) => {
const autoFontPx = cellHeightPx * wp.fontRatio * fontScale
const fs = Math.max(6, autoFontPx)
if (wordPos.length > 1) {
return (
<span
key={`${cell.cellId}_wp_${i}`}
className="absolute leading-none pointer-events-none select-none"
style={{
left: `${wp.xPct}%`,
top: `${bboxPct.y}%`,
width: `${wp.wPct}%`,
height: `${bboxPct.h}%`,
fontSize: `${fs}px`,
fontWeight: globalBold ? 'bold' : 'normal',
fontFamily: "'Liberation Sans', Arial, sans-serif",
display: 'flex',
alignItems: 'center',
whiteSpace: 'nowrap',
overflow: 'visible',
color: '#1a1a1a',
}}
>
{wp.text}
</span>
)
}
// Pixel-analysed: render word-groups at detected positions
if (wordPos && wordPos.length > 0) {
return wordPos.map((wp, i) => {
const autoFontPx = cellHeightPx * wp.fontRatio * fontScale
const fs = Math.max(6, autoFontPx)
if (wordPos.length > 1) {
return (
<div key={`${cell.cellId}_wp_${i}`} className="absolute group" style={{
left: `${wp.xPct}%`,
top: `${bboxPct.y}%`,
width: `${wp.wPct}%`,
height: `${bboxPct.h}%`,
}}>
<input
id={`cell-${cell.cellId}`}
type="text"
value={displayText}
onChange={(e) => handleTextChange(cell.cellId, e.target.value)}
onKeyDown={(e) => handleKeyDown(e, cell.cellId)}
className={`w-full h-full bg-transparent border-0 outline-none px-0 transition-colors ${
edited ? 'bg-green-50/30' : ''
}`}
style={{
fontSize: `${fs}px`,
fontWeight: globalBold ? 'bold' : 'normal',
fontFamily: "'Liberation Sans', Arial, sans-serif",
lineHeight: '1',
color: '#1a1a1a',
}}
title={`${cell.cellId} (${cell.colType})`}
/>
{edited && (
<button
onClick={() => resetCell(cell.cellId)}
className="absolute -top-1 -right-1 w-4 h-4 bg-red-500 text-white rounded-full text-[9px] leading-none opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center"
title="Zuruecksetzen"
>
&times;
</button>
)}
</div>
)
})
}
// Fallback: no pixel data — single input at cell bbox
if (!cell.text) return null
const fontSize = Math.max(6, cellHeightPx * fontScale)
return (
<div key={cell.cellId} className="absolute group" style={{
left: `${bboxPct.x}%`,
top: `${bboxPct.y}%`,
width: `${bboxPct.w}%`,
height: `${bboxPct.h}%`,
}}>
<input
id={`cell-${cell.cellId}`}
type="text"
value={displayText}
onChange={(e) => handleTextChange(cell.cellId, e.target.value)}
onKeyDown={(e) => handleKeyDown(e, cell.cellId)}
className={`w-full h-full bg-transparent border-0 outline-none px-0 transition-colors ${
edited ? 'bg-green-50/30' : ''
}`}
style={{
fontSize: `${fontSize}px`,
fontWeight: globalBold ? 'bold' : 'normal',
fontFamily: "'Liberation Sans', Arial, sans-serif",
lineHeight: '1',
color: '#1a1a1a',
}}
title={`${cell.cellId} (${cell.colType})`}
/>
{edited && (
<button
onClick={() => resetCell(cell.cellId)}
className="absolute -top-1 -right-1 w-4 h-4 bg-red-500 text-white rounded-full text-[9px] leading-none opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center"
title="Zuruecksetzen"
<span
key={`${cell.cellId}_wp_${i}`}
className="absolute leading-none pointer-events-none select-none"
style={{
left: `${wp.xPct}%`,
top: `${bboxPct.y}%`,
width: `${wp.wPct}%`,
height: `${bboxPct.h}%`,
fontSize: `${fs}px`,
fontWeight: globalBold ? 'bold' : 'normal',
fontFamily: "'Liberation Sans', Arial, sans-serif",
display: 'flex',
alignItems: 'center',
whiteSpace: 'nowrap',
overflow: 'visible',
color: colorValue,
}}
>
&times;
</button>
)}
</div>
)
})}
</div>
{wp.text}
</span>
)
}
return (
<div key={`${cell.cellId}_wp_${i}`} className="absolute group" style={{
left: `${wp.xPct}%`,
top: `${bboxPct.y}%`,
width: `${wp.wPct}%`,
height: `${bboxPct.h}%`,
}}>
<input
id={`cell-${cell.cellId}`}
type="text"
value={displayText}
onChange={(e) => handleTextChange(cell.cellId, e.target.value)}
onKeyDown={(e) => handleKeyDown(e, cell.cellId)}
className={`w-full h-full bg-transparent border-0 outline-none px-0 transition-colors ${
edited ? 'bg-green-50/30' : ''
}`}
style={{
fontSize: `${fs}px`,
fontWeight: globalBold ? 'bold' : 'normal',
fontFamily: "'Liberation Sans', Arial, sans-serif",
lineHeight: '1',
color: colorValue,
}}
title={`${cell.cellId} (${cell.colType})`}
/>
{edited && (
<button
onClick={() => resetCell(cell.cellId)}
className="absolute -top-1 -right-1 w-4 h-4 bg-red-500 text-white rounded-full text-[9px] leading-none opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center"
title="Zuruecksetzen"
>
&times;
</button>
)}
</div>
)
})
}
// Fallback: no pixel data — single input at cell bbox
if (!cell.text) return null
const fontSize = Math.max(6, cellHeightPx * fontScale)
return (
<div key={cell.cellId} className="absolute group" style={{
left: `${bboxPct.x}%`,
top: `${bboxPct.y}%`,
width: `${bboxPct.w}%`,
height: `${bboxPct.h}%`,
}}>
<input
id={`cell-${cell.cellId}`}
type="text"
value={displayText}
onChange={(e) => handleTextChange(cell.cellId, e.target.value)}
onKeyDown={(e) => handleKeyDown(e, cell.cellId)}
className={`w-full h-full bg-transparent border-0 outline-none px-0 transition-colors ${
edited ? 'bg-green-50/30' : ''
}`}
style={{
fontSize: `${fontSize}px`,
fontWeight: globalBold ? 'bold' : 'normal',
fontFamily: "'Liberation Sans', Arial, sans-serif",
lineHeight: '1',
color: colorValue,
}}
title={`${cell.cellId} (${cell.colType})`}
/>
{edited && (
<button
onClick={() => resetCell(cell.cellId)}
className="absolute -top-1 -right-1 w-4 h-4 bg-red-500 text-white rounded-full text-[9px] leading-none opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center"
title="Zuruecksetzen"
>
&times;
</button>
)}
</div>
)
})}
</div>
</div>
</div>