O sua dung FONT NHUNG cua PDF (FontFace): het "mat in dam" do may thieu font
Build Windows portable / build (push) Canceled after 0s
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:
@@ -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
@@ -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";
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user