mirror of
https://github.com/PR0M3TH3AN/unredact.git
synced 2026-10-27 08:40:29 +00:00
feat: manual guess input + continuous text preview rendering
- Replace redaction marker span with editable input for manual guesses - Typing in the guess field sets r.preview and shows Accept button - drawRedactionPreview now renders left+guess+right as continuous text with natural font kerning (no fixed gap width during preview) - Active preview: green left, yellow highlighted guess, green right - Inactive preview: full merged text in yellow Co-Authored-By: Claude Opus 4.6 <[email protected]>
This commit is contained in:
1 parent
a4863350ef
commit
33b88f078f
6 files changed
+50
-29
No files matched your search
+23
-23
@@ -199,57 +199,57 @@ function drawRedactionPreview(r, isActive) {
|
||||
const fontName = state.fonts.find(f => f.id === (o.fontId ?? a.font.id))?.name ?? a.font.name;
|
||||
const fontSize = o.fontSize ?? a.font.size;
|
||||
const fontStr = `${fontSize}px "${fontName}"`;
|
||||
const gapW = o.gapWidth ?? a.gap.w;
|
||||
|
||||
const leftText = o.leftText ?? "";
|
||||
const rightText = o.rightText ?? "";
|
||||
const mergedText = leftText + r.preview + rightText;
|
||||
|
||||
if (isActive) {
|
||||
const startX = a.line.x + (o.offsetX ?? 0);
|
||||
const startY = a.line.y + (o.offsetY ?? 0);
|
||||
|
||||
ctx.textBaseline = "top";
|
||||
ctx.font = fontStr;
|
||||
|
||||
let cursorX = startX;
|
||||
|
||||
const leftText = o.leftText ?? "";
|
||||
// Draw as one continuous string for natural kerning
|
||||
// Left portion in green
|
||||
if (leftText) {
|
||||
cursorX += drawStyledText(leftText, cursorX, startY, fontName, fontSize, "rgba(0, 200, 0, 0.7)");
|
||||
drawStyledText(leftText, startX, startY, fontName, fontSize, "rgba(0, 200, 0, 0.7)");
|
||||
}
|
||||
|
||||
// Measure where the preview text starts and ends
|
||||
const leftW = leftText ? measureStyledText(leftText, fontName, fontSize) : 0;
|
||||
const previewX = startX + leftW;
|
||||
const previewW = ctx.measureText(r.preview).width;
|
||||
|
||||
// Highlight behind the preview portion
|
||||
const pad = fontSize * 0.15;
|
||||
ctx.fillStyle = "rgba(255, 200, 0, 0.2)";
|
||||
ctx.fillRect(cursorX, startY - pad, gapW, fontSize + pad * 2);
|
||||
ctx.strokeStyle = "rgba(255, 200, 0, 0.8)";
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeRect(cursorX, startY - pad, gapW, fontSize + pad * 2);
|
||||
ctx.fillRect(previewX, startY - pad, previewW, fontSize + pad * 2);
|
||||
|
||||
// Preview text in yellow
|
||||
ctx.fillStyle = "rgba(255, 200, 0, 0.9)";
|
||||
ctx.font = fontStr;
|
||||
ctx.fillText(r.preview, cursorX, startY);
|
||||
ctx.fillText(r.preview, previewX, startY);
|
||||
|
||||
cursorX += gapW;
|
||||
|
||||
const rightText = o.rightText ?? "";
|
||||
// Right portion in green
|
||||
if (rightText) {
|
||||
drawStyledText(rightText, cursorX, startY, fontName, fontSize, "rgba(0, 200, 0, 0.7)");
|
||||
const rightX = previewX + previewW;
|
||||
drawStyledText(rightText, rightX, startY, fontName, fontSize, "rgba(0, 200, 0, 0.7)");
|
||||
}
|
||||
|
||||
ctx.strokeStyle = "rgba(0, 200, 0, 0.3)";
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(a.line.x, a.line.y, a.line.w, a.line.h);
|
||||
} else {
|
||||
const gapX = a.gap.x + (o.offsetX ?? 0);
|
||||
// Inactive: draw full merged text in yellow at line position
|
||||
const startX = a.line.x + (o.offsetX ?? 0);
|
||||
const textY = a.line.y + (o.offsetY ?? 0);
|
||||
const pad = fontSize * 0.1;
|
||||
ctx.fillStyle = "rgba(255, 200, 0, 0.12)";
|
||||
ctx.fillRect(gapX, textY - pad, gapW, fontSize + pad * 2);
|
||||
|
||||
ctx.strokeStyle = "rgba(255, 200, 0, 0.5)";
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(gapX, textY - pad, gapW, fontSize + pad * 2);
|
||||
|
||||
ctx.font = fontStr;
|
||||
ctx.textBaseline = "top";
|
||||
ctx.fillStyle = "rgba(255, 200, 0, 0.9)";
|
||||
ctx.fillText(r.preview, gapX, textY);
|
||||
ctx.fillText(mergedText, startX, textY);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ export const gapValue = document.getElementById("gap-value");
|
||||
export const textEditBar = document.getElementById("text-edit-bar");
|
||||
export const leftTextInput = /** @type {HTMLInputElement} */ (document.getElementById("left-text-input"));
|
||||
export const rightTextInput = /** @type {HTMLInputElement} */ (document.getElementById("right-text-input"));
|
||||
export const redactionMarker = document.getElementById("redaction-marker");
|
||||
export const redactionMarker = /** @type {HTMLInputElement} */ (document.getElementById("redaction-marker"));
|
||||
export const textReset = document.getElementById("text-reset");
|
||||
export const toastContainer = document.getElementById("toast-container");
|
||||
|
||||
|
||||
@@ -135,7 +135,7 @@
|
||||
</div>
|
||||
<div id="text-edit-bar" hidden>
|
||||
<input type="text" id="left-text-input" class="seg-input" spellcheck="false" autocomplete="off" placeholder="left text">
|
||||
<span id="redaction-marker" class="redaction-marker">???</span>
|
||||
<input type="text" id="redaction-marker" class="redaction-marker" spellcheck="false" autocomplete="off" placeholder="???">
|
||||
<input type="text" id="right-text-input" class="seg-input" spellcheck="false" autocomplete="off" placeholder="right text">
|
||||
<button id="text-reset" class="tb-btn" title="Reset to OCR">Reset</button>
|
||||
</div>
|
||||
|
||||
@@ -44,8 +44,6 @@ export function openPopover(id) {
|
||||
r.preview = r.solution.text;
|
||||
r.status = "analyzed";
|
||||
solveAccept.hidden = true;
|
||||
redactionMarker.textContent = r.solution.text;
|
||||
redactionMarker.className = "redaction-marker preview";
|
||||
} else {
|
||||
solveAccept.hidden = r.preview === null;
|
||||
}
|
||||
@@ -62,7 +60,7 @@ export function openPopover(id) {
|
||||
textEditBar.hidden = false;
|
||||
leftTextInput.value = r.overrides.leftText;
|
||||
rightTextInput.value = r.overrides.rightText;
|
||||
redactionMarker.textContent = r.preview || "???";
|
||||
redactionMarker.value = r.preview || "";
|
||||
redactionMarker.className = r.preview ? "redaction-marker preview" : "redaction-marker";
|
||||
}
|
||||
|
||||
@@ -172,6 +170,16 @@ export function initPopover() {
|
||||
|
||||
});
|
||||
|
||||
redactionMarker.addEventListener("input", () => {
|
||||
const r = state.redactions[state.activeRedaction];
|
||||
if (!r) return;
|
||||
const guess = redactionMarker.value.trim();
|
||||
r.preview = guess || null;
|
||||
redactionMarker.className = guess ? "redaction-marker preview" : "redaction-marker";
|
||||
solveAccept.hidden = !guess;
|
||||
renderCanvas();
|
||||
});
|
||||
|
||||
solveTolerance.addEventListener("input", () => {
|
||||
solveTolValue.textContent = solveTolerance.value;
|
||||
});
|
||||
|
||||
@@ -145,7 +145,7 @@ function handleSolveEvent(data, redactionId) {
|
||||
const r = state.redactions[redactionId];
|
||||
if (!r) return;
|
||||
r.preview = data.text;
|
||||
redactionMarker.textContent = data.text;
|
||||
redactionMarker.value = data.text;
|
||||
redactionMarker.className = "redaction-marker preview";
|
||||
renderCanvas();
|
||||
solveResults.querySelectorAll(".solve-result").forEach(el => el.classList.remove("active"));
|
||||
|
||||
@@ -684,12 +684,25 @@ main {
|
||||
|
||||
.redaction-marker {
|
||||
flex-shrink: 0;
|
||||
min-width: 3rem;
|
||||
max-width: 12rem;
|
||||
padding: 0.25rem 0.4rem;
|
||||
background: var(--red);
|
||||
color: #fff;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 700;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.redaction-marker::placeholder {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.redaction-marker:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 1px var(--accent);
|
||||
}
|
||||
|
||||
.redaction-marker.preview {
|
||||
|
||||
Reference in new issue
Block a user