DONG GOI bo font Noto theo app: khong thoa hiep font he thong o bat ky tang nao
Build Windows portable / build (push) Canceled after 0s
Build Windows portable / build (push) Canceled after 0s
User yeu cau: khach hang khong co font chuan san — phan mem phai tu chuan bi
font, khong duoc thay bang font "gan giong". Truoc do font nhung cua PDF da
dung cho o sua (build 35), nhung con 3 khe ho roi ve font he thong: font
khong nhung trong PDF, font CFF khong va duoc, va KY TU MOI GO VAO khong co
trong subset (subset chi chua glyph da dung).
- scripts/fetch-fonts.ps1: tai 10 file Noto hinted static (Sans/Serif/Mono,
du Regular/Bold/Italic/BoldItalic, phu tieng Viet, ~6.7MB) + OFL license
vao fonts/ (gitignore; CI cache theo hash script).
- CI: fetch fonts + dong fonts/ vao zip portable (canh exe).
- Tauri: main() dat FOFREEXIT_FONTS_PATH (fonts/ canh exe, dev thi goc
workspace); command app_font_data(kind) tra base64 (whitelist 10 kind).
- UI: FontFace "ff-app-{sans|serif|mono}" dang ky theo weight/style, dung
GIUA stack: font nhung cua PDF -> Noto dong goi -> stack he thong (luoi
cuoi). Phan loai serif/sans/mono tu cssFontStack. Nap truoc cung font
nhung khi vao trang sua.
- Engine find_font_bytes (duong ghi file/reflow can font coverage): uu tien
tuyet doi fonts/ dong goi qua env — ket qua LUU ra giong nhau tren moi may
(truoc: lay Arial/Segoe cua Windows, may khach thieu font la khac ket qua).
E2E ke tiep (build 36): go ky tu ngoai subset trong o sua -> 100% glyph phai
tu font custom (nhung/dong goi), 0 glyph he thong.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -703,6 +703,31 @@ fn edit_list_objects(path: String, page: u16, password: Option<String>) -> Resul
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Bytes (base64) của font ĐÓNG GÓI theo app (Noto) — lưới đỡ WYSIWYG cho ô
|
||||
/// sửa: glyph không có trong subset nhúng (ký tự mới gõ) hay font không
|
||||
/// nhúng đều rơi về font này, KHÔNG rơi về font hệ điều hành.
|
||||
#[tauri::command]
|
||||
fn app_font_data(kind: String) -> Result<String, String> {
|
||||
let name = match kind.as_str() {
|
||||
"sans-regular" => "NotoSans-Regular.ttf",
|
||||
"sans-bold" => "NotoSans-Bold.ttf",
|
||||
"sans-italic" => "NotoSans-Italic.ttf",
|
||||
"sans-bolditalic" => "NotoSans-BoldItalic.ttf",
|
||||
"mono-regular" => "NotoSansMono-Regular.ttf",
|
||||
"mono-bold" => "NotoSansMono-Bold.ttf",
|
||||
"serif-regular" => "NotoSerif-Regular.ttf",
|
||||
"serif-bold" => "NotoSerif-Bold.ttf",
|
||||
"serif-italic" => "NotoSerif-Italic.ttf",
|
||||
"serif-bolditalic" => "NotoSerif-BoldItalic.ttf",
|
||||
_ => return Err(format!("kind font không hợp lệ: {kind}")),
|
||||
};
|
||||
let dir = std::env::var("FOFREEXIT_FONTS_PATH")
|
||||
.map_err(|_| "app không kèm bộ font (thiếu thư mục fonts/)".to_string())?;
|
||||
let bytes = std::fs::read(std::path::Path::new(&dir).join(name))
|
||||
.map_err(|e| format!("đọc font {name}: {e}"))?;
|
||||
Ok(base64::engine::general_purpose::STANDARD.encode(&bytes))
|
||||
}
|
||||
|
||||
/// 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]
|
||||
@@ -1408,6 +1433,22 @@ fn pick_image(app: tauri::AppHandle) -> Option<String> {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Bộ font ĐÓNG GÓI theo app (Noto): fonts/ cạnh exe (bản portable) hoặc ở
|
||||
// gốc workspace (dev). Engine (find_font_bytes) + command app_font_data
|
||||
// đọc qua env — hiển thị/ghi file giống nhau trên mọi máy khách.
|
||||
let exe_fonts = std::env::current_exe()
|
||||
.ok()
|
||||
.and_then(|p| p.parent().map(|d| d.join("fonts")));
|
||||
let fonts_dir = match exe_fonts {
|
||||
Some(d) if d.join("NotoSans-Regular.ttf").exists() => Some(d),
|
||||
_ => {
|
||||
let w = workspace_root().join("fonts");
|
||||
if w.join("NotoSans-Regular.ttf").exists() { Some(w) } else { None }
|
||||
}
|
||||
};
|
||||
if let Some(d) = fonts_dir {
|
||||
std::env::set_var("FOFREEXIT_FONTS_PATH", &d);
|
||||
}
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
@@ -1435,6 +1476,7 @@ fn main() {
|
||||
preview_header_footer,
|
||||
edit_list_objects,
|
||||
edit_font_data,
|
||||
app_font_data,
|
||||
edit_apply,
|
||||
edit_apply_to_temp,
|
||||
edit_preview,
|
||||
|
||||
+53
-6
@@ -2340,10 +2340,12 @@ async function loadEditPage() {
|
||||
img.src = url;
|
||||
img.style.width = stageW + "px";
|
||||
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.
|
||||
// Nạp trước font nhúng của mọi run text (dedup theo tên font) + font Noto
|
||||
// đóng gói cùng phân loại — đến lúc đúp mở ô sửa mọi face đã sẵn.
|
||||
for (const o of objs) {
|
||||
if (o.kind === "text") ensureDocFont(o, state.editBase, state.editPage);
|
||||
if (o.kind !== "text") continue;
|
||||
ensureDocFont(o, state.editBase, state.editPage);
|
||||
ensureAppFont(fontClassOf(o.fontFamily), o.fontBold, o.fontItalic);
|
||||
}
|
||||
buildEditOverlay();
|
||||
$("pageInput").value = state.editPage + 1;
|
||||
@@ -2634,12 +2636,57 @@ function ensureDocFont(run, path, page) {
|
||||
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.
|
||||
// ---- Tầng đỡ: bộ font ĐÓNG GÓI theo app (Noto, phủ tiếng Việt) ----
|
||||
// Glyph không có trong subset nhúng (ký tự MỚI gõ vào), font không nhúng hay
|
||||
// không vá được → rơi xuống font Noto đóng gói — GIỐNG NHAU trên mọi máy,
|
||||
// không rơi về font hệ điều hành "gần giống".
|
||||
const appFonts = new Map(); // kind -> true | false | Promise (bền theo app)
|
||||
|
||||
function appFontKind(cls, bold, italic) {
|
||||
if (cls === "mono") return "mono-" + (bold ? "bold" : "regular"); // mono không có italic
|
||||
const style = bold && italic ? "bolditalic" : bold ? "bold" : italic ? "italic" : "regular";
|
||||
return cls + "-" + style;
|
||||
}
|
||||
|
||||
// Phân loại serif/sans/mono theo stack CSS đã suy cho family của run.
|
||||
function fontClassOf(family) {
|
||||
const stack = cssFontStack(family).toLowerCase();
|
||||
if (stack.endsWith("monospace")) return "mono";
|
||||
if (/[^-]serif$/.test(stack)) return "serif";
|
||||
return "sans";
|
||||
}
|
||||
|
||||
function ensureAppFont(cls, bold, italic) {
|
||||
const kind = appFontKind(cls, !!bold, !!italic);
|
||||
if (appFonts.has(kind)) return;
|
||||
const p = (async () => {
|
||||
try {
|
||||
const b64 = await invoke("app_font_data", { kind });
|
||||
const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
|
||||
const face = new FontFace("ff-app-" + cls, bytes.buffer, {
|
||||
weight: bold ? "700" : "400",
|
||||
style: cls !== "mono" && italic ? "italic" : "normal",
|
||||
});
|
||||
await face.load();
|
||||
document.fonts.add(face);
|
||||
appFonts.set(kind, true);
|
||||
return true;
|
||||
} catch (_) {
|
||||
appFonts.set(kind, false); // bản build không kèm fonts/ → stack cũ
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
appFonts.set(kind, p);
|
||||
}
|
||||
|
||||
// Stack font cho 1 run trong ô sửa: font NHÚNG của tài liệu đứng đầu → font
|
||||
// Noto đóng gói theo app → stack hệ thống (lưới cuối cùng).
|
||||
function editFontStack(run, fallbackFamily) {
|
||||
const base = cssFontStack(fallbackFamily);
|
||||
const mid = '"ff-app-' + fontClassOf(fallbackFamily) + '", ';
|
||||
const fam = run && run.fontName ? docFonts.get(run.fontName) : null;
|
||||
return typeof fam === "string" && fam ? '"' + fam + '", ' + base : base;
|
||||
const head = typeof fam === "string" && fam ? '"' + fam + '", ' : "";
|
||||
return head + mid + base;
|
||||
}
|
||||
|
||||
function cssFontStack(family) {
|
||||
|
||||
Reference in New Issue
Block a user