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,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">
|
||||||
|
|||||||
Reference in New Issue
Block a user