Phase 3 (Organize + robust save) + Phase 4 (Edit content); update docs

Engine:
- organize.rs: build_document(plan) core — insert/delete/rotate/crop/extract/merge/split
- watermark.rs: watermark + header/footer via PdfPages::watermark, CID Unicode fonts
- qpdf.rs: QPDF CLI wrapper — repair corrupt files, re-encrypt after edits, ensure_openable
- edit.rs: MOAT — list_objects + apply_edits (SetText VN, Transform, Delete, Add/ReplaceImage)
- Tests: organize/watermark/qpdf_safety/edit roundtrip — 43/43 engine tests green

UI (Tauri): "Tổ chức trang" mode (grid + drag-reorder + multi-select), "Sửa nội dung"
mode (WYSIWYG object overlay, edit-in-place), global undo/redo, robust file open.

Corpus: add encrypted.pdf + corrupt-truncated.pdf fixtures.

Docs: mark Phase 1-4 done in roadmap; document QPDF setup + engine module layout in
05-dev-setup; add 10-13 phase summaries & user-test checklists; README status.
This commit is contained in:
2026-07-14 22:07:38 +07:00
parent 274012f854
commit e442a9f678
26 changed files with 3924 additions and 40 deletions
+54 -5
View File
@@ -30,11 +30,17 @@
<button id="searchNext" title="Kết quả sau (Enter)">▶</button>
<span class="sep"></span>
<button id="copyPage" title="Copy text trang hiện tại">⧉ Copy</button>
<span class="sep"></span>
<button id="undoBtn" title="Hoàn tác (Ctrl+Z)" disabled>↶ Hoàn tác</button>
<button id="redoBtn" title="Làm lại (Ctrl+Y)" disabled>↷ Làm lại</button>
<span class="sep"></span>
<button id="organizeModeBtn" title="Tổ chức trang">🗂 Tổ chức trang</button>
<button id="editModeBtn" title="Sửa nội dung trang">✏️ Sửa nội dung</button>
<div class="spacer"></div>
<span id="status" class="status"></span>
</header>
<div class="annobar">
<div id="annobar" class="annobar">
<span class="annolabel">Chú thích:</span>
<button class="atool" data-tool="highlight" title="Tô sáng (kéo vùng)">🖍️ Tô sáng</button>
<button class="atool" data-tool="underline" title="Gạch chân">U̲ Gạch chân</button>
@@ -42,18 +48,48 @@
<button class="atool" data-tool="square" title="Khung chữ nhật">▭ Khung</button>
<button class="atool" data-tool="freetext" title="Hộp văn bản">🅣 Text box</button>
<button class="atool" data-tool="note" title="Ghi chú dán">📌 Ghi chú</button>
<button class="atool" data-tool="crop" title="Crop trang">✂️ Crop</button>
<span class="sep"></span>
<button id="annotColorBtn" class="atool" title="Màu chú thích"><span class="sw" id="annotSw"></span> Màu</button>
<span class="sep"></span>
<button id="undoBtn" title="Hoàn tác (Ctrl+Z)" disabled>↶ Hoàn tác</button>
<button id="redoBtn" title="Làm lại (Ctrl+Y)" disabled>↷ Làm lại</button>
<span class="spacer"></span>
<span id="annotHint" class="status"></span>
<button id="saveAnnots" disabled>💾 Lưu chú thích (<span id="annotCount">0</span>)</button>
</div>
<div id="organizeBar" class="organizebar hidden">
<span class="annolabel">Tổ chức trang:</span>
<button id="orgInsert" title="Chèn trang">➕ Chèn</button>
<button id="orgDelete" title="Xoá trang đã chọn (Delete)">🗑 Xoá</button>
<button id="orgRotateL" title="Xoay trái">↺ Xoay trái</button>
<button id="orgRotateR" title="Xoay phải">↻ Xoay phải</button>
<button id="orgExtract" title="Trích trang đã chọn">📤 Trích</button>
<button id="orgReplace" title="Thay trang đã chọn">🔁 Thay</button>
<span class="sep"></span>
<button id="orgMerge" title="Trộn nhiều file PDF">🔀 Trộn file</button>
<button id="orgSplit" title="Tách thành nhiều file">✂ Tách file</button>
<button id="orgWatermark" title="Thêm watermark">💧 Watermark</button>
<button id="orgHeaderFooter" title="Thêm header/footer">📋 Header/Footer</button>
<span class="spacer"></span>
<span id="organizeHint" class="status"></span>
<button id="orgSave" disabled>💾 Lưu thay đổi tổ chức trang</button>
</div>
<div id="editBar" class="editbar hidden">
<span class="annolabel">Sửa nội dung:</span>
<button id="edAddText" title="Thêm chữ — bấm rồi click lên trang">🆎 Thêm chữ</button>
<button id="edAddImage" title="Thêm ảnh — chọn ảnh rồi click lên trang">🖼 Thêm ảnh</button>
<button id="edDelete" title="Xoá đối tượng đang chọn (Delete)" disabled>🗑 Xoá</button>
<button id="edReplaceImage" title="Thay ảnh đang chọn" disabled>🔁 Thay ảnh</button>
<span class="sep"></span>
<label class="ed-prop">Cỡ chữ <input id="edFontSize" type="number" min="1" max="400" disabled></label>
<button id="edColorBtn" class="atool" title="Màu chữ" disabled><span class="sw" id="edSw"></span> Màu</button>
<span class="spacer"></span>
<span id="editHint" class="status"></span>
<button id="edSave" disabled>💾 Lưu thay đổi nội dung</button>
</div>
<div class="body">
<aside class="sidebar">
<aside id="sidebar" class="sidebar">
<div class="tabs">
<button id="tabThumbs" class="tab active">Thumbnails</button>
<button id="tabOutline" class="tab">Outline</button>
@@ -67,6 +103,19 @@
<main id="viewport" class="viewport">
<div id="pages" class="pages"></div>
</main>
<main id="organizeGrid" class="organize-grid hidden"></main>
<main id="editStage" class="edit-stage hidden">
<div id="editPageWrap" class="edit-page-wrap">
<img id="editImg" class="edit-img" alt="trang đang sửa"/>
<div id="editOverlay" class="edit-overlay"></div>
</div>
</main>
</div>
<div id="modalOverlay" class="modal-overlay hidden">
<div id="modalBox" class="modal-box"></div>
</div>
<script src="main.js"></script>
+1168 -19
View File
File diff suppressed because it is too large Load Diff
+144
View File
@@ -1,5 +1,6 @@
:root { color-scheme: dark; --accent: #4d8bff; }
* { box-sizing: border-box; }
.hidden { display: none !important; }
html, body { height: 100%; margin: 0; }
body {
font-family: "Segoe UI", system-ui, sans-serif;
@@ -12,6 +13,7 @@ body {
/* Toolbar */
.toolbar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
padding: 6px 12px;
@@ -47,6 +49,7 @@ button {
button:hover { background: #595f63; }
button:disabled { opacity: .4; cursor: default; }
#copyPage { background: var(--accent); }
#organizeModeBtn.active { background: var(--accent); }
select.zoom {
background: #1f2224; color: #eee; border: 1px solid #4a4f52;
border-radius: 6px; padding: 4px 4px; font-size: 13px; min-width: 64px;
@@ -63,6 +66,7 @@ input[type="search"] {
/* Annotation toolbar */
.annobar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
padding: 5px 12px;
@@ -266,3 +270,143 @@ body.tool-active .annotlayer * { pointer-events: none; }
outline: 1px solid rgba(255, 184, 0, .9);
}
.hl.current { background: rgba(255, 138, 0, .55); }
/* ===== Phase 3: Tổ chức trang ===== */
.organizebar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
padding: 5px 12px;
background: #2b2f31;
border-bottom: 1px solid #1c1f21;
flex: 0 0 auto;
}
#orgSave { background: #2e7d32; }
#orgSave:disabled { background: #555; }
.organize-grid {
flex: 1;
overflow: auto;
display: flex;
flex-wrap: wrap;
align-content: flex-start;
gap: 18px;
padding: 24px;
background: #3a3e41;
}
.org-card {
width: 160px;
text-align: center;
cursor: pointer;
user-select: none;
border: 2px solid transparent;
border-radius: 6px;
padding: 6px;
background: #2b2f31;
}
.org-card.selected { border-color: var(--accent); background: #2f3a4d; }
.org-card.drag-over { outline: 3px dashed var(--accent); }
.org-card .org-thumb-wrap {
position: relative;
background: #fff;
box-shadow: 0 2px 8px rgba(0,0,0,.5);
overflow: hidden;
display: flex; align-items: center; justify-content: center;
min-height: 60px;
}
.org-card img { display: block; width: 100%; transition: transform .15s; }
.org-card .org-n { font-size: 12px; color: #ccc; margin-top: 4px; }
.org-card .org-crop-mark {
position: absolute; inset: 10%; border: 1.5px dashed #e53935; pointer-events: none;
}
/* Modal dialog dùng chung cho Insert/Extract/Replace/Crop/Merge/Split/Watermark/Header-Footer */
.modal-overlay {
position: fixed; inset: 0; background: rgba(0,0,0,.55);
display: flex; align-items: center; justify-content: center; z-index: 100;
}
.modal-box {
background: #2b2f31; border-radius: 8px; padding: 16px 20px;
min-width: 340px; max-width: 560px; max-height: 84vh; overflow: auto;
box-shadow: 0 10px 40px rgba(0,0,0,.6); color: #eee;
}
.modal-box h3 { margin: 0 0 12px; font-size: 15px; color: #fff; }
.modal-box label { display: block; font-size: 12px; color: #9aa0a6; margin: 10px 0 3px; }
.modal-box input[type="text"], .modal-box input[type="number"], .modal-box select, .modal-box textarea {
width: 100%; background: #1f2224; color: #eee; border: 1px solid #4a4f52;
border-radius: 6px; padding: 6px 8px; font-size: 13px; box-sizing: border-box;
}
.modal-box .row { display: flex; gap: 8px; }
.modal-box .row > * { flex: 1; }
.modal-box .radiorow { display: flex; gap: 14px; align-items: center; font-size: 13px; margin: 4px 0; }
.modal-box .foot { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; }
.modal-box .foot button.primary { background: var(--accent); }
.modal-box .anchor9 { display: grid; grid-template-columns: repeat(3, 32px); gap: 4px; }
.modal-box .anchor9 button { width: 32px; height: 28px; padding: 0; background: #3a3f42; }
.modal-box .anchor9 button.cur { background: var(--accent); }
.modal-box .err { color: #ef5350; font-size: 12px; margin-top: 8px; }
/* ===== Phase 4: Sửa nội dung (Edit) ===== */
#editModeBtn.active { background: var(--accent); }
.editbar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
padding: 5px 12px;
background: #2b2f31;
border-bottom: 1px solid #1c1f21;
flex: 0 0 auto;
}
.editbar .ed-prop { display: flex; align-items: center; gap: 4px; font-size: 13px; }
.editbar .ed-prop input { width: 56px; }
.editbar button.armed { background: var(--accent); }
#edSave { background: #2e7d32; }
#edSave:disabled { background: #555; }
.edit-stage {
flex: 1;
overflow: auto;
background: #444;
display: flex;
justify-content: center;
align-items: flex-start;
padding: 20px;
user-select: none;
}
.edit-inline { user-select: text; }
.edit-page-wrap { position: relative; box-shadow: 0 2px 12px rgba(0,0,0,.5); background: #fff; }
.edit-img { display: block; }
.edit-overlay { position: absolute; inset: 0; }
.edit-overlay.armed { cursor: crosshair; }
.edit-box {
position: absolute;
border: 1px solid rgba(77,139,255,.5);
box-sizing: border-box;
cursor: move;
}
.edit-box:hover { border-color: var(--accent); background: rgba(77,139,255,.08); }
.edit-box.kind-text { border-style: dashed; }
.edit-box.selected { border: 2px solid var(--accent); background: rgba(77,139,255,.12); }
.edit-box .ed-handle {
position: absolute;
width: 10px; height: 10px;
right: -6px; bottom: -6px;
background: var(--accent);
border: 1px solid #fff;
cursor: nwse-resize;
}
.edit-inline {
position: absolute;
z-index: 5;
border: 2px solid var(--accent);
background: #fff;
color: #000;
padding: 1px 2px;
outline: none;
font-family: inherit;
overflow: hidden;
white-space: pre;
}