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 [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,43 +421,35 @@ 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>
|
||||
|
||||
{/* 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">
|
||||
{/* 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 })
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 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-gray-300/50"
|
||||
className="absolute left-0 right-0 border-t border-cyan-400/40"
|
||||
style={{ top: `${(row.y / imgH) * 100}%` }}
|
||||
/>
|
||||
))}
|
||||
@@ -439,6 +461,7 @@ export function OverlayReconstruction({ sessionId, onNext }: OverlayReconstructi
|
||||
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) {
|
||||
@@ -463,7 +486,7 @@ export function OverlayReconstruction({ sessionId, onNext }: OverlayReconstructi
|
||||
alignItems: 'center',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'visible',
|
||||
color: '#1a1a1a',
|
||||
color: colorValue,
|
||||
}}
|
||||
>
|
||||
{wp.text}
|
||||
@@ -492,7 +515,7 @@ export function OverlayReconstruction({ sessionId, onNext }: OverlayReconstructi
|
||||
fontWeight: globalBold ? 'bold' : 'normal',
|
||||
fontFamily: "'Liberation Sans', Arial, sans-serif",
|
||||
lineHeight: '1',
|
||||
color: '#1a1a1a',
|
||||
color: colorValue,
|
||||
}}
|
||||
title={`${cell.cellId} (${cell.colType})`}
|
||||
/>
|
||||
@@ -535,7 +558,7 @@ export function OverlayReconstruction({ sessionId, onNext }: OverlayReconstructi
|
||||
fontWeight: globalBold ? 'bold' : 'normal',
|
||||
fontFamily: "'Liberation Sans', Arial, sans-serif",
|
||||
lineHeight: '1',
|
||||
color: '#1a1a1a',
|
||||
color: colorValue,
|
||||
}}
|
||||
title={`${cell.cellId} (${cell.colType})`}
|
||||
/>
|
||||
@@ -554,7 +577,6 @@ export function OverlayReconstruction({ sessionId, onNext }: OverlayReconstructi
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom action */}
|
||||
<div className="flex justify-end">
|
||||
|
||||
Reference in New Issue
Block a user