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
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:
@@ -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,167 +421,159 @@ 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>
|
ref={reconRef}
|
||||||
<div className="text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">
|
className="relative"
|
||||||
Originalbild
|
style={{ aspectRatio: `${imgW} / ${imgH}` }}
|
||||||
</div>
|
>
|
||||||
<div className="border rounded-lg overflow-hidden dark:border-gray-700 bg-gray-50 dark:bg-gray-900 sticky top-4">
|
{/* Background: original image */}
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
<img
|
<img
|
||||||
src={dewarpedUrl}
|
src={dewarpedUrl}
|
||||||
alt="Original"
|
alt="Original"
|
||||||
className="w-full h-auto"
|
className="absolute inset-0 w-full h-full object-contain"
|
||||||
onLoad={(e) => {
|
onLoad={(e) => {
|
||||||
const img = e.target as HTMLImageElement
|
const img = e.target as HTMLImageElement
|
||||||
setImageNaturalSize({ w: img.naturalWidth, h: img.naturalHeight })
|
setImageNaturalSize({ w: img.naturalWidth, h: img.naturalHeight })
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right: Reconstructed overlay */}
|
{/* Text overlay layer */}
|
||||||
<div>
|
<div
|
||||||
<div className="text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">
|
className="absolute inset-0"
|
||||||
Rekonstruktion ({cells.length} Zellen)
|
style={{ opacity: textOpacity / 100 }}
|
||||||
</div>
|
>
|
||||||
<div className="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden bg-white">
|
{/* Row lines */}
|
||||||
<div
|
{rows.map((row, i) => (
|
||||||
ref={reconRef}
|
<div
|
||||||
className="relative"
|
key={`row-${i}`}
|
||||||
style={{ aspectRatio: `${imgW} / ${imgH}` }}
|
className="absolute left-0 right-0 border-t border-cyan-400/40"
|
||||||
>
|
style={{ top: `${(row.y / imgH) * 100}%` }}
|
||||||
{/* 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}%` }}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{/* Pixel-positioned words / editable inputs */}
|
{/* Pixel-positioned words / editable inputs */}
|
||||||
{cells.map((cell) => {
|
{cells.map((cell) => {
|
||||||
const displayText = getDisplayText(cell)
|
const displayText = getDisplayText(cell)
|
||||||
const edited = isEdited(cell)
|
const edited = isEdited(cell)
|
||||||
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) {
|
||||||
return wordPos.map((wp, i) => {
|
return wordPos.map((wp, i) => {
|
||||||
const autoFontPx = cellHeightPx * wp.fontRatio * fontScale
|
const autoFontPx = cellHeightPx * wp.fontRatio * fontScale
|
||||||
const fs = Math.max(6, autoFontPx)
|
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (wordPos.length > 1) {
|
||||||
return (
|
return (
|
||||||
<div key={`${cell.cellId}_wp_${i}`} className="absolute group" style={{
|
<span
|
||||||
left: `${wp.xPct}%`,
|
key={`${cell.cellId}_wp_${i}`}
|
||||||
top: `${bboxPct.y}%`,
|
className="absolute leading-none pointer-events-none select-none"
|
||||||
width: `${wp.wPct}%`,
|
style={{
|
||||||
height: `${bboxPct.h}%`,
|
left: `${wp.xPct}%`,
|
||||||
}}>
|
top: `${bboxPct.y}%`,
|
||||||
<input
|
width: `${wp.wPct}%`,
|
||||||
id={`cell-${cell.cellId}`}
|
height: `${bboxPct.h}%`,
|
||||||
type="text"
|
fontSize: `${fs}px`,
|
||||||
value={displayText}
|
fontWeight: globalBold ? 'bold' : 'normal',
|
||||||
onChange={(e) => handleTextChange(cell.cellId, e.target.value)}
|
fontFamily: "'Liberation Sans', Arial, sans-serif",
|
||||||
onKeyDown={(e) => handleKeyDown(e, cell.cellId)}
|
display: 'flex',
|
||||||
className={`w-full h-full bg-transparent border-0 outline-none px-0 transition-colors ${
|
alignItems: 'center',
|
||||||
edited ? 'bg-green-50/30' : ''
|
whiteSpace: 'nowrap',
|
||||||
}`}
|
overflow: 'visible',
|
||||||
style={{
|
color: colorValue,
|
||||||
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"
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</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"
|
|
||||||
>
|
>
|
||||||
×
|
{wp.text}
|
||||||
</button>
|
</span>
|
||||||
)}
|
)
|
||||||
</div>
|
}
|
||||||
)
|
|
||||||
})}
|
return (
|
||||||
</div>
|
<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"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</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"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user