diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index f14d94b..6bfcfd6 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -40,6 +40,8 @@ jobs: - name: Fetch QPDF (Windows x64) shell: pwsh run: powershell -ExecutionPolicy Bypass -File scripts/fetch-qpdf.ps1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Bộ font Noto đóng gói theo app (phủ tiếng Việt) — hiển thị/ghi file # giống nhau trên mọi máy khách, không phụ thuộc font hệ điều hành. diff --git a/app/src-tauri/src/main.rs b/app/src-tauri/src/main.rs index a9e0c3e..2aaa653 100644 --- a/app/src-tauri/src/main.rs +++ b/app/src-tauri/src/main.rs @@ -775,6 +775,10 @@ fn app_font_data(kind: String) -> Result { "serif-bold" => "NotoSerif-Bold.ttf", "serif-italic" => "NotoSerif-Italic.ttf", "serif-bolditalic" => "NotoSerif-BoldItalic.ttf", + // Gói CJK tuỳ chọn (scripts/fetch-fonts-cjk.ps1) — không có file thì + // trả lỗi, UI tự rơi về font CJK hệ thống. + "cjk-regular" => "NotoSansCJK-Regular.ttc", + "cjk-bold" => "NotoSansCJK-Bold.ttc", _ => return Err(format!("kind font không hợp lệ: {kind}")), }; let dir = std::env::var("FOFREEXIT_FONTS_PATH") diff --git a/app/src/main.js b/app/src/main.js index 74d9a2e..e32d4f3 100644 --- a/app/src/main.js +++ b/app/src/main.js @@ -2359,6 +2359,7 @@ async function loadEditPage() { if (o.kind !== "text") continue; ensureDocFont(o, state.editBase, state.editPage); ensureAppFont(fontClassOf(o.fontFamily), o.fontBold, o.fontItalic); + if (hasCjk(o.text)) ensureAppFont("cjk", o.fontBold, false); } buildEditOverlay(); $("pageInput").value = state.editPage + 1; @@ -2656,11 +2657,17 @@ function ensureDocFont(run, path, page) { 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 + // mono/cjk không có biến thể italic riêng — trình duyệt tự nghiêng. + if (cls === "mono" || cls === "cjk") return cls + "-" + (bold ? "bold" : "regular"); const style = bold && italic ? "bolditalic" : bold ? "bold" : italic ? "italic" : "regular"; return cls + "-" + style; } +// Text có ký tự CJK (Hoa/Nhật/Hàn)? — nạp thêm gói CJK tuỳ chọn nếu app kèm. +function hasCjk(text) { + return /[぀-ヿ㐀-䶿一-鿿가-힯]/.test(text || ""); +} + // Phân loại serif/sans/mono theo stack CSS đã suy cho family của run. function fontClassOf(family) { const stack = cssFontStack(family).toLowerCase(); @@ -2693,10 +2700,11 @@ function ensureAppFont(cls, bold, italic) { } // 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). +// Noto đóng gói theo app (+CJK nếu chữ CJK) → stack hệ thống (lưới cuối). function editFontStack(run, fallbackFamily) { const base = cssFontStack(fallbackFamily); - const mid = '"ff-app-' + fontClassOf(fallbackFamily) + '", '; + let mid = '"ff-app-' + fontClassOf(fallbackFamily) + '", '; + if (run && hasCjk(run.text)) mid += '"ff-app-cjk", '; const fam = run && run.fontName ? docFonts.get(run.fontName) : null; const head = typeof fam === "string" && fam ? '"' + fam + '", ' : ""; return head + mid + base; diff --git a/crates/ff-engine/src/tounicode.rs b/crates/ff-engine/src/tounicode.rs index 77c6c1c..7e662aa 100644 --- a/crates/ff-engine/src/tounicode.rs +++ b/crates/ff-engine/src/tounicode.rs @@ -10,7 +10,8 @@ use lopdf::{Dictionary, Document as LoDoc, Object}; use crate::formsurgery::{deref, stream_bytes}; -/// Thông tin giải mã của 1 font trong PDF — đủ để fontfix dựng cmap Unicode. +/// Thông tin giải mã của 1 font trong PDF — đủ để fontfix dựng cmap Unicode +/// và (với CFF trần) cả hmtx/OS-2 khi bọc thành OTF. pub(crate) struct FontMapping { /// Mã trong content stream → Unicode (từ /ToUnicode). pub code_to_uni: HashMap, @@ -19,6 +20,14 @@ pub(crate) struct FontMapping { /// từ stream /CIDToGIDMap (2 byte big-endian mỗi CID). pub is_cid: bool, pub cid_to_gid: Option>, + /// Mã → bề rộng advance (đơn vị text space ×1000): /Widths (font đơn + /// giản) hoặc /W (font CID). + pub widths: HashMap, + /// /DW của font CID (mặc định 1000) — bề rộng cho mã không có trong /W. + pub default_width: f32, + /// /Ascent /Descent từ FontDescriptor (0 = không rõ). + pub ascent: f32, + pub descent: f32, } /// Mapping của font có BaseFont khớp `font_name` (so phần sau prefix subset diff --git a/scripts/fetch-fonts-cjk.ps1 b/scripts/fetch-fonts-cjk.ps1 new file mode 100644 index 0000000..038a589 --- /dev/null +++ b/scripts/fetch-fonts-cjk.ps1 @@ -0,0 +1,28 @@ +# TUY CHON: tai bo font Noto Sans CJK (Hoa gian the/phon the + Nhat + Han) +# vao fonts/ - phuc vu tai lieu CJK khong nhung font. KHONG chay trong CI +# mac dinh (moi file ~16-20MB lam zip phinh to); nguoi dung/dev can thi chay: +# powershell -ExecutionPolicy Bypass -File scripts/fetch-fonts-cjk.ps1 +# App tu nhan dien file trong fonts/ luc khoi dong - khong can cau hinh. + +$ErrorActionPreference = "Stop" +$root = Split-Path -Parent $PSScriptRoot +$fontsDir = Join-Path $root "fonts" +New-Item -ItemType Directory -Force -Path $fontsDir | Out-Null + +# Ban OTC gop 4 ngon ngu (SC/TC/JP/KR) tu repo phat hanh chinh thuc. +$files = @( + @{ name = "NotoSansCJK-Regular.ttc"; url = "https://github.com/notofonts/noto-cjk/raw/main/Sans/OTC/NotoSansCJK-Regular.ttc" }, + @{ name = "NotoSansCJK-Bold.ttc"; url = "https://github.com/notofonts/noto-cjk/raw/main/Sans/OTC/NotoSansCJK-Bold.ttc" } +) + +foreach ($f in $files) { + $dest = Join-Path $fontsDir $f.name + if ((Test-Path $dest) -and ((Get-Item $dest).Length -gt 1000000)) { + Write-Output "Da co: $($f.name)" + continue + } + Write-Output "Tai $($f.name) (~20MB)..." + Invoke-WebRequest -Uri $f.url -OutFile $dest -UseBasicParsing + if ((Get-Item $dest).Length -lt 1000000) { throw "File $($f.name) qua nho - tai hong?" } +} +Write-Output "Xong. De dong goi kem ban portable: copy fonts/ (gom ca file CJK) canh FoFreeXit.exe." diff --git a/scripts/fetch-qpdf.ps1 b/scripts/fetch-qpdf.ps1 index 522b5c1..40edba9 100644 --- a/scripts/fetch-qpdf.ps1 +++ b/scripts/fetch-qpdf.ps1 @@ -9,12 +9,23 @@ $qpdfDir = Join-Path $root "qpdf" $tmp = Join-Path $env:TEMP "qpdf-win-msvc64.zip" Write-Output "Tra cuu release moi nhat cua qpdf/qpdf..." -$release = Invoke-RestMethod -Uri "https://api.github.com/repos/qpdf/qpdf/releases/latest" -UseBasicParsing -$asset = $release.assets | Where-Object { $_.name -like "*msvc64.zip" } | Select-Object -First 1 -if (-not $asset) { throw "Khong tim thay asset msvc64.zip trong release $($release.tag_name)" } - -Write-Output "Tai $($asset.name) (tu release $($release.tag_name))..." -Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $tmp -UseBasicParsing +# Dung GITHUB_TOKEN khi co (CI) - goi API khong xac thuc tu runner hay dinh +# rate limit lam fail build oan. API loi thi roi ve ban pin cung. +$headers = @{} +if ($env:GITHUB_TOKEN) { $headers["Authorization"] = "Bearer $($env:GITHUB_TOKEN)" } +$dlUrl = $null +try { + $release = Invoke-RestMethod -Uri "https://api.github.com/repos/qpdf/qpdf/releases/latest" -Headers $headers -UseBasicParsing + $asset = $release.assets | Where-Object { $_.name -like "*msvc64.zip" } | Select-Object -First 1 + if ($asset) { $dlUrl = $asset.browser_download_url; Write-Output "Tai $($asset.name) (release $($release.tag_name))..." } +} catch { + Write-Output "API loi ($($_.Exception.Message)) - dung ban pin." +} +if (-not $dlUrl) { + $dlUrl = "https://github.com/qpdf/qpdf/releases/download/v12.2.0/qpdf-12.2.0-msvc64.zip" + Write-Output "Tai ban pin: $dlUrl" +} +Invoke-WebRequest -Uri $dlUrl -OutFile $tmp -UseBasicParsing $extractDir = Join-Path $env:TEMP "qpdf-extract" if (Test-Path $extractDir) { Remove-Item -Recurse -Force $extractDir }