Fix layout review save flow and respect word font sizes in replica rendering
This commit is contained in:
parent
fbdef46220
commit
9fcef4cacd
|
|
@ -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_width = max(1.0, right - left)
|
||||||
box_height = max(1.0, bottom - top)
|
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(
|
entries.append(
|
||||||
{
|
{
|
||||||
"text": word_text,
|
"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,
|
"pdf_y": page_h - bottom,
|
||||||
"box_width": box_width,
|
"box_width": box_width,
|
||||||
"box_height": box_height,
|
"box_height": box_height,
|
||||||
"font_family_guess": "Helvetica",
|
"font_family_guess": word.get("font_family_guess") or "Helvetica",
|
||||||
"font_size_guess": _fit_font_size_for_bbox_text(word_text, box_width, box_height),
|
"font_size_guess": font_size,
|
||||||
"text_color_guess": "#000000",
|
"text_color_guess": word.get("text_color_guess") or "#000000",
|
||||||
"text_render_mode_clean": 0,
|
"text_render_mode_clean": word.get("text_render_mode_clean", 0),
|
||||||
"text_render_mode_scan_backed": 3,
|
"text_render_mode_scan_backed": word.get("text_render_mode_scan_backed", 3),
|
||||||
"bbox_source": [left, top, right, bottom],
|
"bbox_source": [left, top, right, bottom],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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)):
|
async def save_layout_review(document_id: str, request: Request, db: Session = Depends(get_db)):
|
||||||
form = await request.form()
|
form = await request.form()
|
||||||
payload_raw = form.get("layout_review_json")
|
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:
|
if not payload_raw:
|
||||||
return RedirectResponse(
|
return RedirectResponse(
|
||||||
|
|
|
||||||
|
|
@ -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-fit-width">Fit</button>
|
||||||
<button type="button" class="layout-tool-btn" id="layout-zoom-in">+</button>
|
<button type="button" class="layout-tool-btn" id="layout-zoom-in">+</button>
|
||||||
<span id="layout-zoom-label">100%</span>
|
<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>
|
<span id="layout-review-status">Ready</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -876,17 +876,21 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
return words.find(w => String(w.id) === String(selectedId)) || null;
|
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) : "";
|
const v = Number.isFinite(Number(value)) ? Number(value) : "";
|
||||||
if (fontSizeInput) fontSizeInput.value = v;
|
if (fontSizeInput) fontSizeInput.value = v;
|
||||||
|
if (popoverFontSizeInput) popoverFontSizeInput.value = v;
|
||||||
|
|
||||||
|
if (!word) return;
|
||||||
|
|
||||||
ensureStyle(word);
|
ensureStyle(word);
|
||||||
resolveStyle(word);
|
resolveStyle(word);
|
||||||
|
|
||||||
if (fontFamilyInput) fontFamilyInput.value = word.resolved_style?.font_family || "Helvetica";
|
if (fontFamilyInput) fontFamilyInput.value = word.resolved_style?.font_family || "Helvetica";
|
||||||
if (fontWeightInput) fontWeightInput.value = String(word.resolved_style?.font_weight || 400);
|
if (fontWeightInput) fontWeightInput.value = String(word.resolved_style?.font_weight || 400);
|
||||||
if (fontStyleInput) fontStyleInput.value = word.resolved_style?.font_style || "normal";
|
if (fontStyleInput) fontStyleInput.value = word.resolved_style?.font_style || "normal";
|
||||||
if (letterSpacingInput) letterSpacingInput.value = String(word.resolved_style?.letter_spacing || 0);
|
if (letterSpacingInput) letterSpacingInput.value = String(word.resolved_style?.letter_spacing || 0);
|
||||||
if (textColorInput) textColorInput.value = word.resolved_style?.text_color || "#000000";
|
if (textColorInput) textColorInput.value = word.resolved_style?.text_color || "#000000";
|
||||||
if (popoverFontSizeInput) popoverFontSizeInput.value = v;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function pageToCanvasPoint(px, py, displayWidth, displayHeight) {
|
function pageToCanvasPoint(px, py, displayWidth, displayHeight) {
|
||||||
|
|
@ -988,7 +992,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
for (const word of sortedWords) {
|
for (const word of sortedWords) {
|
||||||
const bbox = normalizeBBox(word.bbox || [0,0,0,0]);
|
const bbox = normalizeBBox(word.bbox || [0,0,0,0]);
|
||||||
const cy = (bbox[1] + bbox[3]) / 2;
|
const cy = (bbox[1] + bbox[3]) / 2;
|
||||||
let placed = False;
|
let placed = false;
|
||||||
for (const group of groups) {
|
for (const group of groups) {
|
||||||
if (Math.abs(cy - group.centerY) <= LINE_GROUP_TOLERANCE) {
|
if (Math.abs(cy - group.centerY) <= LINE_GROUP_TOLERANCE) {
|
||||||
group.words.push(word);
|
group.words.push(word);
|
||||||
|
|
@ -1312,7 +1316,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
|
||||||
if (textInput) textInput.value = nextText;
|
if (textInput) textInput.value = nextText;
|
||||||
if (popoverTextInput) popoverTextInput.value = nextText;
|
if (popoverTextInput) popoverTextInput.value = nextText;
|
||||||
syncFontInputs(w.font_size_guess);
|
syncFontInputs(w.font_size_guess, w);
|
||||||
|
|
||||||
w.bbox = normalizeBBox([
|
w.bbox = normalizeBBox([
|
||||||
Number(x1Input ? x1Input.value : 0),
|
Number(x1Input ? x1Input.value : 0),
|
||||||
|
|
@ -1337,6 +1341,19 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
applyEditorValues(true);
|
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() {
|
function applyFontSizeToAllWords() {
|
||||||
const w = getSelectedWord();
|
const w = getSelectedWord();
|
||||||
if (!w) return;
|
if (!w) return;
|
||||||
|
|
@ -1350,7 +1367,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
item.font_size_guess = chosen;
|
item.font_size_guess = chosen;
|
||||||
if (!item.font_family_guess) item.font_family_guess = getWordFontFamily(w);
|
if (!item.font_family_guess) item.font_family_guess = getWordFontFamily(w);
|
||||||
}
|
}
|
||||||
syncFontInputs(chosen);
|
syncFontInputs(chosen, w);
|
||||||
syncReviewedTextarea();
|
syncReviewedTextarea();
|
||||||
renderCanvas();
|
renderCanvas();
|
||||||
setStatus("Applied font size to all");
|
setStatus("Applied font size to all");
|
||||||
|
|
@ -1393,22 +1410,72 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
page: page.page || 1,
|
page: page.page || 1,
|
||||||
page_width: page.page_width || 1,
|
page_width: page.page_width || 1,
|
||||||
page_height: page.page_height || 1,
|
page_height: page.page_height || 1,
|
||||||
words: words.map((w, idx) => ({
|
words: words.map((w, idx) => {
|
||||||
|
ensureStyle(w);
|
||||||
|
resolveStyle(w);
|
||||||
|
return {
|
||||||
id: Number(w.id || (idx + 1)),
|
id: Number(w.id || (idx + 1)),
|
||||||
text: w.text || "",
|
text: w.text || "",
|
||||||
font_size_guess: getWordFontSize(w),
|
font_size_guess: getWordFontSize(w),
|
||||||
font_family_guess: getWordFontFamily(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([
|
bbox: normalizeBBox([
|
||||||
Number((w.bbox || [0,0,0,0])[0] || 0),
|
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])[1] || 0),
|
||||||
Number((w.bbox || [0,0,0,0])[2] || 0),
|
Number((w.bbox || [0,0,0,0])[2] || 0),
|
||||||
Number((w.bbox || [0,0,0,0])[3] || 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) {
|
function beginPan(ev) {
|
||||||
dragState = {
|
dragState = {
|
||||||
mode: "pan",
|
mode: "pan",
|
||||||
|
|
@ -1665,6 +1732,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
document.getElementById("layout-font-down")?.addEventListener("click", () => changeSelectedFontSize(-0.5));
|
document.getElementById("layout-font-down")?.addEventListener("click", () => changeSelectedFontSize(-0.5));
|
||||||
document.getElementById("layout-font-up")?.addEventListener("click", () => changeSelectedFontSize(0.5));
|
document.getElementById("layout-font-up")?.addEventListener("click", () => changeSelectedFontSize(0.5));
|
||||||
document.getElementById("layout-font-all")?.addEventListener("click", applyFontSizeToAllWords);
|
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-down")?.addEventListener("click", () => changeSelectedFontSize(-0.5));
|
||||||
document.getElementById("layout-popover-font-up")?.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);
|
document.getElementById("layout-popover-font-all")?.addEventListener("click", applyFontSizeToAllWords);
|
||||||
|
|
@ -1708,10 +1777,19 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
window.addEventListener("keydown", handleKeyDown);
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
|
|
||||||
if (saveForm) {
|
if (saveForm) {
|
||||||
saveForm.addEventListener("submit", function () {
|
saveForm.addEventListener("submit", function (ev) {
|
||||||
|
try {
|
||||||
applyEditorValues(false);
|
applyEditorValues(false);
|
||||||
syncReviewedTextarea();
|
syncReviewedTextarea();
|
||||||
if (saveJsonInput) saveJsonInput.value = buildLayoutReviewPayload();
|
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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1997,7 +2075,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for i in range(row_count) %}
|
{% 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">
|
<tr class="line-item-row">
|
||||||
<td style="padding:0.35rem; white-space:nowrap;">
|
<td style="padding:0.35rem; white-space:nowrap;">
|
||||||
<span class="line-item-row-number">{{ i + 1 }}</span>
|
<span class="line-item-row-number">{{ i + 1 }}</span>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue