Fix layout review save flow and respect word font sizes in replica rendering

This commit is contained in:
Sean McElwain 2026-05-24 14:28:26 -05:00
parent fbdef46220
commit 9fcef4cacd
3 changed files with 120 additions and 30 deletions

View File

@ -384,6 +384,15 @@ def _build_word_entries_for_page(page_layout: dict, page_h: float) -> list[dict]
box_width = max(1.0, right - left)
box_height = max(1.0, bottom - top)
source_font_size = word.get("font_size_guess")
try:
font_size = float(source_font_size)
except (TypeError, ValueError):
font_size = _fit_font_size_for_bbox_text(word_text, box_width, box_height)
if font_size <= 0:
font_size = _fit_font_size_for_bbox_text(word_text, box_width, box_height)
entries.append(
{
"text": word_text,
@ -391,11 +400,11 @@ def _build_word_entries_for_page(page_layout: dict, page_h: float) -> list[dict]
"pdf_y": page_h - bottom,
"box_width": box_width,
"box_height": box_height,
"font_family_guess": "Helvetica",
"font_size_guess": _fit_font_size_for_bbox_text(word_text, box_width, box_height),
"text_color_guess": "#000000",
"text_render_mode_clean": 0,
"text_render_mode_scan_backed": 3,
"font_family_guess": word.get("font_family_guess") or "Helvetica",
"font_size_guess": font_size,
"text_color_guess": word.get("text_color_guess") or "#000000",
"text_render_mode_clean": word.get("text_render_mode_clean", 0),
"text_render_mode_scan_backed": word.get("text_render_mode_scan_backed", 3),
"bbox_source": [left, top, right, bottom],
}
)

View File

@ -1938,6 +1938,9 @@ def _layout_review_group_words_into_lines(words, y_tol: float = 12.0):
async def save_layout_review(document_id: str, request: Request, db: Session = Depends(get_db)):
form = await request.form()
payload_raw = form.get("layout_review_json")
print("[save_layout_review] payload present:", bool(payload_raw))
print("[save_layout_review] payload length:", len(payload_raw) if payload_raw else 0)
print(f"[save_layout_review] document_id={document_id} payload_present={bool(payload_raw)} payload_len={len(payload_raw) if payload_raw else 0}", flush=True)
if not payload_raw:
return RedirectResponse(

View File

@ -557,7 +557,7 @@ document.addEventListener("DOMContentLoaded", () => {
<button type="button" class="layout-tool-btn" id="layout-fit-width">Fit</button>
<button type="button" class="layout-tool-btn" id="layout-zoom-in">+</button>
<span id="layout-zoom-label">100%</span>
<button type="submit" form="layout-review-save-form" class="layout-tool-btn primary">Save</button>
<button type="submit" form="layout-review-save-form" class="layout-tool-btn primary" onclick="window.prepareLayoutReviewSubmit && window.prepareLayoutReviewSubmit()">Save</button>
<span id="layout-review-status">Ready</span>
</div>
@ -876,17 +876,21 @@ document.addEventListener("DOMContentLoaded", () => {
return words.find(w => String(w.id) === String(selectedId)) || null;
}
function syncFontInputs(value) {
function syncFontInputs(value, word = null) {
const v = Number.isFinite(Number(value)) ? Number(value) : "";
if (fontSizeInput) fontSizeInput.value = v;
if (popoverFontSizeInput) popoverFontSizeInput.value = v;
if (!word) return;
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;
}
function pageToCanvasPoint(px, py, displayWidth, displayHeight) {
@ -988,7 +992,7 @@ document.addEventListener("DOMContentLoaded", () => {
for (const word of sortedWords) {
const bbox = normalizeBBox(word.bbox || [0,0,0,0]);
const cy = (bbox[1] + bbox[3]) / 2;
let placed = False;
let placed = false;
for (const group of groups) {
if (Math.abs(cy - group.centerY) <= LINE_GROUP_TOLERANCE) {
group.words.push(word);
@ -1312,7 +1316,7 @@ document.addEventListener("DOMContentLoaded", () => {
if (textInput) textInput.value = nextText;
if (popoverTextInput) popoverTextInput.value = nextText;
syncFontInputs(w.font_size_guess);
syncFontInputs(w.font_size_guess, w);
w.bbox = normalizeBBox([
Number(x1Input ? x1Input.value : 0),
@ -1337,6 +1341,19 @@ document.addEventListener("DOMContentLoaded", () => {
applyEditorValues(true);
}
function applyFontSizeInputToSelected(push = true) {
const w = getSelectedWord();
if (!w || !fontSizeInput) return;
const next = Number(fontSizeInput.value);
if (!Number.isFinite(next) || next <= 0) return;
if (push) pushHistory();
w.font_size_guess = next;
syncFontInputs(next, w);
syncReviewedTextarea();
renderCanvas();
setStatus("Font size applied: " + next);
}
function applyFontSizeToAllWords() {
const w = getSelectedWord();
if (!w) return;
@ -1350,7 +1367,7 @@ document.addEventListener("DOMContentLoaded", () => {
item.font_size_guess = chosen;
if (!item.font_family_guess) item.font_family_guess = getWordFontFamily(w);
}
syncFontInputs(chosen);
syncFontInputs(chosen, w);
syncReviewedTextarea();
renderCanvas();
setStatus("Applied font size to all");
@ -1393,22 +1410,72 @@ document.addEventListener("DOMContentLoaded", () => {
page: page.page || 1,
page_width: page.page_width || 1,
page_height: page.page_height || 1,
words: words.map((w, idx) => ({
id: Number(w.id || (idx + 1)),
text: w.text || "",
font_size_guess: getWordFontSize(w),
font_family_guess: getWordFontFamily(w),
bbox: normalizeBBox([
Number((w.bbox || [0,0,0,0])[0] || 0),
Number((w.bbox || [0,0,0,0])[1] || 0),
Number((w.bbox || [0,0,0,0])[2] || 0),
Number((w.bbox || [0,0,0,0])[3] || 0),
]),
})),
words: words.map((w, idx) => {
ensureStyle(w);
resolveStyle(w);
return {
id: Number(w.id || (idx + 1)),
text: w.text || "",
font_size_guess: getWordFontSize(w),
font_family_guess: getWordFontFamily(w),
font_weight_guess: Number((w.resolved_style && w.resolved_style.font_weight) || 400),
font_style_guess: String((w.resolved_style && w.resolved_style.font_style) || "normal"),
letter_spacing_guess: Number((w.resolved_style && w.resolved_style.letter_spacing) || 0),
text_color_guess: String((w.resolved_style && w.resolved_style.text_color) || "#000000"),
bbox: normalizeBBox([
Number((w.bbox || [0,0,0,0])[0] || 0),
Number((w.bbox || [0,0,0,0])[1] || 0),
Number((w.bbox || [0,0,0,0])[2] || 0),
Number((w.bbox || [0,0,0,0])[3] || 0),
]),
};
}),
}],
});
}
window.prepareLayoutReviewSubmit = function () {
applyEditorValues(false);
syncReviewedTextarea();
const payload = buildLayoutReviewPayload();
if (saveJsonInput) saveJsonInput.value = payload;
if (statusEl) statusEl.textContent = "[layout-review] manual payload bytes: " + (payload ? payload.length : -1);
return payload;
};
function prepareLayoutReviewSubmit() {
try { applyEditorValues(false); } catch (e) { console.error("[layout-review] applyEditorValues failed", e); }
try { syncReviewedTextarea(); } catch (e) { console.error("[layout-review] syncReviewedTextarea failed", e); }
try {
if (saveJsonInput) {
saveJsonInput.value = buildLayoutReviewPayload();
console.log("[layout-review] prepared payload length", saveJsonInput.value.length);
}
} catch (e) {
console.error("[layout-review] build payload failed", e);
}
}
window.buildLayoutReviewPayload = buildLayoutReviewPayload;
window.prepareLayoutReviewSubmit = prepareLayoutReviewSubmit;
function prepareLayoutReviewSubmit() {
try { applyEditorValues(false); } catch (e) { console.error("[layout-review] applyEditorValues failed", e); }
try { syncReviewedTextarea(); } catch (e) { console.error("[layout-review] syncReviewedTextarea failed", e); }
try {
if (saveJsonInput) {
saveJsonInput.value = buildLayoutReviewPayload();
console.log("[layout-review] prepared payload length", saveJsonInput.value.length);
}
} catch (e) {
console.error("[layout-review] build payload failed", e);
}
}
window.buildLayoutReviewPayload = buildLayoutReviewPayload;
window.prepareLayoutReviewSubmit = prepareLayoutReviewSubmit;
function beginPan(ev) {
dragState = {
mode: "pan",
@ -1665,6 +1732,8 @@ document.addEventListener("DOMContentLoaded", () => {
document.getElementById("layout-font-down")?.addEventListener("click", () => changeSelectedFontSize(-0.5));
document.getElementById("layout-font-up")?.addEventListener("click", () => changeSelectedFontSize(0.5));
document.getElementById("layout-font-all")?.addEventListener("click", applyFontSizeToAllWords);
fontSizeInput?.addEventListener("change", () => applyFontSizeInputToSelected(true));
fontSizeInput?.addEventListener("blur", () => applyFontSizeInputToSelected(true));
document.getElementById("layout-popover-font-down")?.addEventListener("click", () => changeSelectedFontSize(-0.5));
document.getElementById("layout-popover-font-up")?.addEventListener("click", () => changeSelectedFontSize(0.5));
document.getElementById("layout-popover-font-all")?.addEventListener("click", applyFontSizeToAllWords);
@ -1708,12 +1777,21 @@ document.addEventListener("DOMContentLoaded", () => {
window.addEventListener("keydown", handleKeyDown);
if (saveForm) {
saveForm.addEventListener("submit", function () {
applyEditorValues(false);
syncReviewedTextarea();
if (saveJsonInput) saveJsonInput.value = buildLayoutReviewPayload();
});
}
saveForm.addEventListener("submit", function (ev) {
try {
applyEditorValues(false);
syncReviewedTextarea();
const payload = buildLayoutReviewPayload();
if (saveJsonInput) saveJsonInput.value = payload;
console.log("[layout-review] payload bytes", payload ? payload.length : -1);
if (statusEl) statusEl.textContent = "[layout-review] payload bytes: " + (payload ? payload.length : -1);
} catch (err) {
ev.preventDefault();
console.error("[layout-review] submit error", err);
if (statusEl) statusEl.textContent = "[layout-review] submit error: " + (err && err.message ? err.message : err);
}
});
}
pushHistory();
setTool("select");
@ -1997,7 +2075,7 @@ document.addEventListener("DOMContentLoaded", () => {
</thead>
<tbody>
{% for i in range(row_count) %}
{% set item = line_items[i] if i < line_items|length else None %}
{% set item = line_items[i] if i < line_items|length else none %}
<tr class="line-item-row">
<td style="padding:0.35rem; white-space:nowrap;">
<span class="line-item-row-number">{{ i + 1 }}</span>