O sua dung FONT NHUNG cua PDF (FontFace): het "mat in dam" do may thieu font
Build Windows portable / build (push) Canceled after 0s

User bao: chu mono von in dam (NotoSansMono-Bold), mo o sua thi "ve chu
thuong". Chan doan bang CSS.getPlatformFontsForNode: span da co
font-weight:700 dung, nhung may khong cai font cua tai lieu nen WebView thay
the — mono roi ve Consolas-Bold (net manh hon han NotoSansMono-Bold, nhin
nhu khong dam), chu thuong roi ve Arial.

Fix WYSIWYG font that:
- Engine: ff_engine::font_data(input, page, index) — bytes font nhung (sau
  giai nen) cua 1 text run, index PHANG nhu list_objects (vao duoc run trong
  Form XObject).
- Tauri: command edit_font_data tra base64.
- UI: nap luoi theo TEN font khi loadEditPage (dedup, 1 lan/font), dang ky
  FontFace voi weight/style theo run (khong synthesize chong), dat family
  nhung LEN DAU stack cua div/span trong o sua; glyph thieu trong subset tu
  roi xuong stack he thong; doi phien sua thi bo face cu (subset khac
  coverage). Loi (font khong nhung/khong parse) -> giu stack cu.

Smoke test tren app that (CDP): FontFace tu ArrayBuffer + gan vao span mono
=> getPlatformFontsForNode tra isCustomFont:true. E2E day du sau build CI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-10 11:02:27 +07:00
co-authored by Claude Fable 5
parent c885567b26
commit f2ebf2deff
4 changed files with 110 additions and 3 deletions
+11
View File
@@ -703,6 +703,16 @@ fn edit_list_objects(path: String, page: u16, password: Option<String>) -> Resul
.collect()) .collect())
} }
/// Bytes font NHÚNG của 1 text run (base64) — UI đăng ký FontFace để ô sửa
/// hiển thị đúng font tài liệu thay vì font hệ thống gần giống.
#[tauri::command]
fn edit_font_data(path: String, page: u16, index: u16, password: Option<String>) -> Result<String, String> {
let pdfium = pdfium()?;
let data = ff_engine::font_data(&pdfium, std::path::Path::new(&path), page, index, password.as_deref())
.map_err(|e| e.to_string())?;
Ok(base64::engine::general_purpose::STANDARD.encode(&data))
}
/// 1 thao tác sửa nội dung (tagged theo field `op`). Field thừa cho mỗi loại để None/mặc định. /// 1 thao tác sửa nội dung (tagged theo field `op`). Field thừa cho mỗi loại để None/mặc định.
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
@@ -1424,6 +1434,7 @@ fn main() {
preview_watermark, preview_watermark,
preview_header_footer, preview_header_footer,
edit_list_objects, edit_list_objects,
edit_font_data,
edit_apply, edit_apply,
edit_apply_to_temp, edit_apply_to_temp,
edit_preview, edit_preview,
+61 -2
View File
@@ -2257,6 +2257,7 @@ function enterEditMode(pageIndex) {
$("sidebar").classList.add("hidden"); $("sidebar").classList.add("hidden");
$("editModeBtn").classList.add("active"); $("editModeBtn").classList.add("active");
setTool(null); setTool(null);
resetDocFonts(); // font nhúng nạp theo phiên sửa (subset mỗi file khác nhau)
buildEditColumn(); buildEditColumn();
// Đứng ngay tại trang vào sửa (không smooth — vừa mở chế độ, chưa có ngữ cảnh cuộn). // Đứng ngay tại trang vào sửa (không smooth — vừa mở chế độ, chưa có ngữ cảnh cuộn).
const slot0 = state.editSlots[state.editPage]; const slot0 = state.editSlots[state.editPage];
@@ -2339,6 +2340,11 @@ async function loadEditPage() {
img.src = url; img.src = url;
img.style.width = stageW + "px"; img.style.width = stageW + "px";
state.editObjects = objs; state.editObjects = objs;
// Nạp trước font nhúng của mọi run text (dedup theo tên font) — đến lúc
// đúp mở ô sửa thì face đã sẵn, đo đạc đúng ngay từ đầu.
for (const o of objs) {
if (o.kind === "text") ensureDocFont(o, state.editBase, state.editPage);
}
buildEditOverlay(); buildEditOverlay();
$("pageInput").value = state.editPage + 1; $("pageInput").value = state.editPage + 1;
updateZoomLabel(); updateZoomLabel();
@@ -2583,6 +2589,59 @@ const FONT_CSS_ALIASES = {
couriernewps: "Courier New", couriernewps: "Courier New",
segoeui: "Segoe UI", segoeui: "Segoe UI",
}; };
// ---------- Font NHÚNG của tài liệu cho ô sửa ----------
// Máy người dùng thường KHÔNG cài font của PDF: WebView thay bằng font hệ
// thống gần giống (NotoSansMono-Bold → Consolas-Bold nét mảnh — user thấy
// như "mất in đậm"; NotoSans → Arial). Nạp bytes font nhúng từ engine vào
// WebView (FontFace) rồi đặt family đó LÊN ĐẦU stack của ô sửa → chữ trong ô
// đúng font thật của tài liệu. Glyph thiếu trong subset tự rơi xuống stack cũ.
let docFontSeq = 0; // đổi phiên sửa = bỏ face cũ (subset khác coverage)
const docFonts = new Map(); // fontName -> family đã nạp | null (lỗi) | Promise
const docFontFaces = [];
function resetDocFonts() {
docFontSeq++;
for (const f of docFontFaces) { try { document.fonts.delete(f); } catch (_) {} }
docFontFaces.length = 0;
docFonts.clear();
}
// Nạp lười theo TÊN font (NotoSansMono-Bold...): mỗi tên chỉ đọc bytes 1 lần,
// dùng 1 run đại diện để lấy dữ liệu. Lỗi (font không nhúng, không parse
// được) → đánh dấu null, ô sửa dùng stack hệ thống như cũ.
function ensureDocFont(run, path, page) {
const name = run && run.fontName;
if (!name || docFonts.has(name)) return;
const fam = "ff-doc-" + docFontSeq + "-" + name.replace(/[^A-Za-z0-9_-]/g, "");
const p = (async () => {
try {
const b64 = await invoke("edit_font_data", { path, page, index: run.index, password: null });
const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
const face = new FontFace(fam, bytes.buffer, {
weight: run.fontBold ? "700" : "400",
style: run.fontItalic ? "italic" : "normal",
});
await face.load();
document.fonts.add(face);
docFontFaces.push(face);
docFonts.set(name, fam);
return fam;
} catch (_) {
docFonts.set(name, null);
return null;
}
})();
docFonts.set(name, p);
}
// Stack font cho 1 run trong ô sửa: font nhúng (nếu đã nạp xong) đứng đầu,
// stack hệ thống theo sau làm lưới an toàn.
function editFontStack(run, fallbackFamily) {
const base = cssFontStack(fallbackFamily);
const fam = run && run.fontName ? docFonts.get(run.fontName) : null;
return typeof fam === "string" && fam ? '"' + fam + '", ' + base : base;
}
function cssFontStack(family) { function cssFontStack(family) {
if (!family) return "sans-serif"; if (!family) return "sans-serif";
const k = family.toLowerCase().replace(/[^a-z0-9]/g, ""); const k = family.toLowerCase().replace(/[^a-z0-9]/g, "");
@@ -2932,7 +2991,7 @@ function startBlockTextEdit(o, lines, ev) {
const divFs = Math.max(6, (l.fs || 12) * s); const divFs = Math.max(6, (l.fs || 12) * s);
div.style.fontSize = divFs + "px"; div.style.fontSize = divFs + "px";
div.style.lineHeight = perLineAdvances[i] + "px"; div.style.lineHeight = perLineAdvances[i] + "px";
div.style.fontFamily = cssFontStack(rep.fontFamily || o.fontFamily); div.style.fontFamily = editFontStack(rep, rep.fontFamily || o.fontFamily);
div.style.fontWeight = (rep.fontBold != null ? rep.fontBold : o.fontBold) ? "bold" : "normal"; div.style.fontWeight = (rep.fontBold != null ? rep.fontBold : o.fontBold) ? "bold" : "normal";
div.style.fontStyle = (rep.fontItalic != null ? rep.fontItalic : o.fontItalic) ? "italic" : "normal"; div.style.fontStyle = (rep.fontItalic != null ? rep.fontItalic : o.fontItalic) ? "italic" : "normal";
if (rep.color || o.color) div.style.color = rgbCss(rep.color || o.color); if (rep.color || o.color) div.style.color = rgbCss(rep.color || o.color);
@@ -2946,7 +3005,7 @@ function startBlockTextEdit(o, lines, ev) {
const span = document.createElement("span"); const span = document.createElement("span");
span.textContent = g.text; span.textContent = g.text;
span.dataset.styleRun = String(g.rep.index); span.dataset.styleRun = String(g.rep.index);
span.style.fontFamily = cssFontStack(g.rep.fontFamily || o.fontFamily); span.style.fontFamily = editFontStack(g.rep, g.rep.fontFamily || o.fontFamily);
span.style.fontWeight = g.rep.fontBold ? "bold" : "normal"; span.style.fontWeight = g.rep.fontBold ? "bold" : "normal";
span.style.fontStyle = g.rep.fontItalic ? "italic" : "normal"; span.style.fontStyle = g.rep.fontItalic ? "italic" : "normal";
span.style.fontSize = Math.max(6, Math.abs(g.rep.fontSize || l.fs || 12) * s) + "px"; span.style.fontSize = Math.max(6, Math.abs(g.rep.fontSize || l.fs || 12) * s) + "px";
+37
View File
@@ -488,6 +488,43 @@ fn text_object_style(t: &PdfPageTextObject) -> (String, bool, bool) {
} }
/// Liệt kê các page object của 1 trang để UI vẽ overlay chỉnh sửa. /// Liệt kê các page object của 1 trang để UI vẽ overlay chỉnh sửa.
/// Bytes font NHÚNG của 1 text object (sau giải nén). UI nạp vào WebView qua
/// FontFace để ô sửa hiển thị ĐÚNG font tài liệu: máy người dùng thường thiếu
/// font (NotoSansMono-Bold rơi về Consolas nét mảnh → trông như "mất in
/// đậm"). `object_index` = index PHẲNG như list_objects (vào được cả run
/// trong Form XObject).
pub fn font_data(
pdfium: &Pdfium,
input: &Path,
page_index: u16,
object_index: u16,
password: Option<&str>,
) -> Result<Vec<u8>, EngineError> {
let document = pdfium
.load_pdf_from_file(input, password)
.map_err(|e| EngineError::Pdfium(e.to_string()))?;
let page = document
.pages()
.get(page_index)
.map_err(|e| EngineError::Pdfium(format!("không lấy được trang {page_index}: {e}")))?;
let entries = collect_flat(&page);
let entry = entries
.get(object_index as usize)
.ok_or_else(|| EngineError::Pdfium(format!("object {object_index} ngoài phạm vi")))?;
let obj = object_at_path(&page, &entry.path)?;
let t = obj
.as_text_object()
.ok_or_else(|| EngineError::Pdfium("object không phải text".into()))?;
let data = t
.font()
.data()
.map_err(|e| EngineError::Pdfium(format!("không đọc được font nhúng: {e}")))?;
if data.is_empty() {
return Err(EngineError::Pdfium("font không nhúng trong file".into()));
}
Ok(data.to_vec())
}
pub fn list_objects( pub fn list_objects(
pdfium: &Pdfium, pdfium: &Pdfium,
input: &Path, input: &Path,
+1 -1
View File
@@ -23,7 +23,7 @@ pub mod watermark;
pub use annot::{ pub use annot::{
apply_annotations, count_annotations, list_annotations, AnnotInfo, AnnotKind, AnnotSpec, apply_annotations, count_annotations, list_annotations, AnnotInfo, AnnotKind, AnnotSpec,
}; };
pub use edit::{apply_edits, flatten_form_xobjects, list_objects, EditOp, ObjectInfo, ObjectKind, RichSeg}; pub use edit::{apply_edits, flatten_form_xobjects, font_data, list_objects, EditOp, ObjectInfo, ObjectKind, RichSeg};
pub use meta::{outline, page_dims, strip_metadata, OutlineItem, PageDim}; pub use meta::{outline, page_dims, strip_metadata, OutlineItem, PageDim};
pub use organize::{ pub use organize::{
build_document, delete_pages, extract_pages, identity_plan, merge_files, rotate_pages, build_document, delete_pages, extract_pages, identity_plan, merge_files, rotate_pages,