Add layout style editing controls and normalize review style data
This commit is contained in:
parent
d24e144490
commit
fbdef46220
|
|
@ -644,6 +644,78 @@ def _get_current_text_versions(document: Document) -> tuple[TextVersion | None,
|
||||||
return raw_ocr, reviewed_ocr
|
return raw_ocr, reviewed_ocr
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def _default_word_style() -> dict:
|
||||||
|
return {
|
||||||
|
"font_family": "Helvetica",
|
||||||
|
"font_postscript_name": None,
|
||||||
|
"font_weight": 400,
|
||||||
|
"font_style": "normal",
|
||||||
|
"font_stretch": "normal",
|
||||||
|
"font_size": 10.0,
|
||||||
|
"line_height": None,
|
||||||
|
"letter_spacing": 0.0,
|
||||||
|
"word_spacing": 0.0,
|
||||||
|
"text_color": "#000000",
|
||||||
|
"opacity": 1.0,
|
||||||
|
"render_mode": "fill",
|
||||||
|
"text_align": "left",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _merge_style_layers(inferred_style: dict | None, override_style: dict | None) -> dict:
|
||||||
|
base = _default_word_style()
|
||||||
|
if isinstance(inferred_style, dict):
|
||||||
|
base.update({k: v for k, v in inferred_style.items() if v is not None})
|
||||||
|
if isinstance(override_style, dict):
|
||||||
|
base.update({k: v for k, v in override_style.items() if v is not None})
|
||||||
|
return base
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_word_style(word: dict) -> dict:
|
||||||
|
inferred = word.get("inferred_style") if isinstance(word.get("inferred_style"), dict) else {}
|
||||||
|
override = word.get("override_style") if isinstance(word.get("override_style"), dict) else {}
|
||||||
|
resolved = _merge_style_layers(inferred, override)
|
||||||
|
|
||||||
|
word["inferred_style"] = _merge_style_layers({}, inferred)
|
||||||
|
word["override_style"] = override
|
||||||
|
word["resolved_style"] = resolved
|
||||||
|
|
||||||
|
manual_flags = word.get("manual_flags") if isinstance(word.get("manual_flags"), dict) else {}
|
||||||
|
manual_flags.setdefault("text_edited", False)
|
||||||
|
manual_flags.setdefault("geometry_edited", False)
|
||||||
|
manual_flags.setdefault("style_edited", False)
|
||||||
|
word["manual_flags"] = manual_flags
|
||||||
|
return word
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_layout_review_payload(layout_json: dict | None) -> dict:
|
||||||
|
layout_json = layout_json if isinstance(layout_json, dict) else {}
|
||||||
|
layout_json.setdefault("schema_version", 2)
|
||||||
|
layout_json.setdefault("edit_log", [])
|
||||||
|
pages = layout_json.get("pages")
|
||||||
|
if not isinstance(pages, list):
|
||||||
|
pages = []
|
||||||
|
layout_json["pages"] = pages
|
||||||
|
|
||||||
|
for page in pages:
|
||||||
|
words = page.get("words")
|
||||||
|
if not isinstance(words, list):
|
||||||
|
page["words"] = []
|
||||||
|
continue
|
||||||
|
for word in words:
|
||||||
|
if isinstance(word, dict):
|
||||||
|
_normalize_word_style(word)
|
||||||
|
|
||||||
|
return layout_json
|
||||||
|
|
||||||
|
|
||||||
|
def _append_layout_edit_event(layout_json: dict, event: dict) -> None:
|
||||||
|
edit_log = layout_json.setdefault("edit_log", [])
|
||||||
|
if isinstance(edit_log, list):
|
||||||
|
edit_log.append(event)
|
||||||
|
|
||||||
|
|
||||||
def _extract_line_texts_from_layout(layout_json: dict | None) -> list[str]:
|
def _extract_line_texts_from_layout(layout_json: dict | None) -> list[str]:
|
||||||
if not layout_json:
|
if not layout_json:
|
||||||
return []
|
return []
|
||||||
|
|
@ -1988,6 +2060,16 @@ async def save_layout_review(document_id: str, request: Request, db: Session = D
|
||||||
new_layout_json["layout_sync_status"] = "synced"
|
new_layout_json["layout_sync_status"] = "synced"
|
||||||
new_layout_json["layout_sync_source"] = "layout_review"
|
new_layout_json["layout_sync_source"] = "layout_review"
|
||||||
new_layout_json["layout_needs_review"] = False
|
new_layout_json["layout_needs_review"] = False
|
||||||
|
new_layout_json = _normalize_layout_review_payload(new_layout_json)
|
||||||
|
_append_layout_edit_event(
|
||||||
|
new_layout_json,
|
||||||
|
{
|
||||||
|
"event_type": "layout_review_save",
|
||||||
|
"actor": "user",
|
||||||
|
"source": "layout_review_editor",
|
||||||
|
"timestamp": datetime.utcnow().isoformat() + "Z",
|
||||||
|
},
|
||||||
|
)
|
||||||
new_text_content = "\n".join(rebuilt_text_lines).strip()
|
new_text_content = "\n".join(rebuilt_text_lines).strip()
|
||||||
|
|
||||||
next_version_number = max(
|
next_version_number = max(
|
||||||
|
|
|
||||||
|
|
@ -625,6 +625,45 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="layout-word-font-family">Font family</label>
|
||||||
|
<input id="layout-word-font-family" type="text" style="width:100%;" placeholder="Helvetica">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="layout-field-grid">
|
||||||
|
<div>
|
||||||
|
<label for="layout-word-font-weight">Weight</label>
|
||||||
|
<select id="layout-word-font-weight" style="width:100%;">
|
||||||
|
<option value="300">300</option>
|
||||||
|
<option value="400" selected>400</option>
|
||||||
|
<option value="500">500</option>
|
||||||
|
<option value="700">700</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="layout-word-font-style">Style</label>
|
||||||
|
<select id="layout-word-font-style" style="width:100%;">
|
||||||
|
<option value="normal" selected>Normal</option>
|
||||||
|
<option value="italic">Italic</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="layout-word-letter-spacing">Letter spacing</label>
|
||||||
|
<input id="layout-word-letter-spacing" type="number" step="0.1" style="width:100%;">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="layout-word-text-color">Text color</label>
|
||||||
|
<input id="layout-word-text-color" type="color" value="#000000" style="width:100%;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="layout-props-actions">
|
||||||
|
<button type="button" id="layout-apply-style-word">Apply Style</button>
|
||||||
|
<button type="button" id="layout-apply-style-line">Style Line</button>
|
||||||
|
<button type="button" id="layout-apply-style-page">Style Page</button>
|
||||||
|
<button type="button" id="layout-reset-style-word">Reset Style</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="layout-field-grid">
|
<div class="layout-field-grid">
|
||||||
<div>
|
<div>
|
||||||
<label for="layout-x1">x1</label>
|
<label for="layout-x1">x1</label>
|
||||||
|
|
@ -682,6 +721,11 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
const idInput = document.getElementById("layout-word-id");
|
const idInput = document.getElementById("layout-word-id");
|
||||||
const textInput = document.getElementById("layout-word-text");
|
const textInput = document.getElementById("layout-word-text");
|
||||||
const fontSizeInput = document.getElementById("layout-word-font-size");
|
const fontSizeInput = document.getElementById("layout-word-font-size");
|
||||||
|
const fontFamilyInput = document.getElementById("layout-word-font-family");
|
||||||
|
const fontWeightInput = document.getElementById("layout-word-font-weight");
|
||||||
|
const fontStyleInput = document.getElementById("layout-word-font-style");
|
||||||
|
const letterSpacingInput = document.getElementById("layout-word-letter-spacing");
|
||||||
|
const textColorInput = document.getElementById("layout-word-text-color");
|
||||||
const x1Input = document.getElementById("layout-x1");
|
const x1Input = document.getElementById("layout-x1");
|
||||||
const y1Input = document.getElementById("layout-y1");
|
const y1Input = document.getElementById("layout-y1");
|
||||||
const x2Input = document.getElementById("layout-x2");
|
const x2Input = document.getElementById("layout-x2");
|
||||||
|
|
@ -691,6 +735,11 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
const popoverTextInput = document.getElementById("layout-popover-text");
|
const popoverTextInput = document.getElementById("layout-popover-text");
|
||||||
const popoverFontSizeInput = document.getElementById("layout-popover-font-size");
|
const popoverFontSizeInput = document.getElementById("layout-popover-font-size");
|
||||||
|
|
||||||
|
const applyStyleWordBtn = document.getElementById("layout-apply-style-word");
|
||||||
|
const applyStyleLineBtn = document.getElementById("layout-apply-style-line");
|
||||||
|
const applyStylePageBtn = document.getElementById("layout-apply-style-page");
|
||||||
|
const resetStyleWordBtn = document.getElementById("layout-reset-style-word");
|
||||||
|
|
||||||
const HANDLE_SIZE_PX = 14;
|
const HANDLE_SIZE_PX = 14;
|
||||||
const HANDLE_HIT_PX = 26;
|
const HANDLE_HIT_PX = 26;
|
||||||
const SNAP_THRESHOLD_PX = 10;
|
const SNAP_THRESHOLD_PX = 10;
|
||||||
|
|
@ -707,6 +756,48 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
if (statusEl) statusEl.textContent = msg;
|
if (statusEl) statusEl.textContent = msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ensureStyle(word) {
|
||||||
|
word.inferred_style = word.inferred_style || {};
|
||||||
|
word.override_style = word.override_style || {};
|
||||||
|
word.resolved_style = word.resolved_style || {};
|
||||||
|
word.manual_flags = word.manual_flags || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveStyle(word) {
|
||||||
|
const defaults = {
|
||||||
|
font_family: "Helvetica",
|
||||||
|
font_postscript_name: null,
|
||||||
|
font_weight: 400,
|
||||||
|
font_style: "normal",
|
||||||
|
font_stretch: "normal",
|
||||||
|
font_size: 10,
|
||||||
|
line_height: null,
|
||||||
|
letter_spacing: 0,
|
||||||
|
word_spacing: 0,
|
||||||
|
text_color: "#000000",
|
||||||
|
opacity: 1,
|
||||||
|
render_mode: "fill",
|
||||||
|
text_align: "left",
|
||||||
|
};
|
||||||
|
word.resolved_style = {
|
||||||
|
...defaults,
|
||||||
|
...(word.inferred_style || {}),
|
||||||
|
...(word.override_style || {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelectedWord() {
|
||||||
|
return words.find(w => String(w.id) === String(selectedId)) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyStyleToWord(word, stylePatch) {
|
||||||
|
if (!word) return;
|
||||||
|
ensureStyle(word);
|
||||||
|
Object.assign(word.override_style, stylePatch);
|
||||||
|
word.manual_flags.style_edited = true;
|
||||||
|
resolveStyle(word);
|
||||||
|
}
|
||||||
|
|
||||||
let pages = [];
|
let pages = [];
|
||||||
try {
|
try {
|
||||||
pages = JSON.parse(dataTag.textContent || "[]");
|
pages = JSON.parse(dataTag.textContent || "[]");
|
||||||
|
|
@ -788,6 +879,13 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
function syncFontInputs(value) {
|
function syncFontInputs(value) {
|
||||||
const v = Number.isFinite(Number(value)) ? Number(value) : "";
|
const v = Number.isFinite(Number(value)) ? Number(value) : "";
|
||||||
if (fontSizeInput) fontSizeInput.value = v;
|
if (fontSizeInput) fontSizeInput.value = v;
|
||||||
|
ensureStyle(word);
|
||||||
|
resolveStyle(word);
|
||||||
|
if (fontFamilyInput) fontFamilyInput.value = word.resolved_style?.font_family || "Helvetica";
|
||||||
|
if (fontWeightInput) fontWeightInput.value = String(word.resolved_style?.font_weight || 400);
|
||||||
|
if (fontStyleInput) fontStyleInput.value = word.resolved_style?.font_style || "normal";
|
||||||
|
if (letterSpacingInput) letterSpacingInput.value = String(word.resolved_style?.letter_spacing || 0);
|
||||||
|
if (textColorInput) textColorInput.value = word.resolved_style?.text_color || "#000000";
|
||||||
if (popoverFontSizeInput) popoverFontSizeInput.value = v;
|
if (popoverFontSizeInput) popoverFontSizeInput.value = v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1632,6 +1730,65 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
if (image.complete) initialRender();
|
if (image.complete) initialRender();
|
||||||
else image.addEventListener("load", initialRender, { once: true });
|
else image.addEventListener("load", initialRender, { once: true });
|
||||||
window.addEventListener("resize", renderCanvas);
|
window.addEventListener("resize", renderCanvas);
|
||||||
|
|
||||||
|
function collectStylePatchFromInputs() {
|
||||||
|
return {
|
||||||
|
font_family: (fontFamilyInput?.value || "Helvetica").trim() || "Helvetica",
|
||||||
|
font_size: Number(fontSizeInput?.value || 10),
|
||||||
|
font_weight: Number(fontWeightInput?.value || 400),
|
||||||
|
font_style: fontStyleInput?.value || "normal",
|
||||||
|
letter_spacing: Number(letterSpacingInput?.value || 0),
|
||||||
|
text_color: textColorInput?.value || "#000000",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (applyStyleWordBtn) {
|
||||||
|
applyStyleWordBtn.addEventListener("click", function () {
|
||||||
|
const word = getSelectedWord();
|
||||||
|
if (!word) return;
|
||||||
|
applyStyleToWord(word, collectStylePatchFromInputs());
|
||||||
|
pushHistoryState("style-word");
|
||||||
|
redraw();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (applyStyleLineBtn) {
|
||||||
|
applyStyleLineBtn.addEventListener("click", function () {
|
||||||
|
const word = getSelectedWord();
|
||||||
|
if (!word) return;
|
||||||
|
const y = ((word.bbox || [0,0,0,0])[1] + (word.bbox || [0,0,0,0])[3]) / 2;
|
||||||
|
const patch = collectStylePatchFromInputs();
|
||||||
|
for (const w of words) {
|
||||||
|
const bbox = w.bbox || [0,0,0,0];
|
||||||
|
const wy = (bbox[1] + bbox[3]) / 2;
|
||||||
|
if (Math.abs(wy - y) <= 12) applyStyleToWord(w, patch);
|
||||||
|
}
|
||||||
|
pushHistoryState("style-line");
|
||||||
|
redraw();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (applyStylePageBtn) {
|
||||||
|
applyStylePageBtn.addEventListener("click", function () {
|
||||||
|
const patch = collectStylePatchFromInputs();
|
||||||
|
for (const w of words) applyStyleToWord(w, patch);
|
||||||
|
pushHistoryState("style-page");
|
||||||
|
redraw();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resetStyleWordBtn) {
|
||||||
|
resetStyleWordBtn.addEventListener("click", function () {
|
||||||
|
const word = getSelectedWord();
|
||||||
|
if (!word) return;
|
||||||
|
ensureStyle(word);
|
||||||
|
word.override_style = {};
|
||||||
|
word.manual_flags.style_edited = false;
|
||||||
|
resolveStyle(word);
|
||||||
|
pushHistoryState("style-reset");
|
||||||
|
redraw();
|
||||||
|
});
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue