From 6a6972ae48d633bb0a62fabd9710085cd04f91e4 Mon Sep 17 00:00:00 2001 From: SkyJourney Date: Mon, 3 Aug 2026 12:07:12 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=86=85=E7=BD=AEDOCX=E5=8F=8C=E4=B8=AD?= =?UTF-8?q?=E6=96=87=E5=AD=97=E4=BD=93=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/desktop/electron-builder.config.cjs | 5 + apps/desktop/src/main.ts | 13 ++- apps/desktop/tests/release-build.test.ts | 28 +++--- deploy/Dockerfile | 1 + deploy/verify-font-pack-compose.mjs | 57 ++++++----- docs/PROGRESS.md | 11 ++- font-packs/README.md | 90 +++++------------- .../mdtp-sans-sc/1.0.0/MdTPSansSC-Bold.ttf | 3 + .../mdtp-sans-sc/1.0.0/MdTPSansSC-Bold.woff2 | 3 + .../mdtp-sans-sc/1.0.0/MdTPSansSC-Regular.ttf | 3 + .../1.0.0/MdTPSansSC-Regular.woff2 | 3 + font-packs/bundle.json | 17 ++++ .../recipes/mdtp-sans-sc/OFL-NotoSansSC.txt | 94 +++++++++++++++++++ font-packs/recipes/mdtp-sans-sc/SOURCE.md | 29 ++++++ font-packs/recipes/mdtp-sans-sc/recipe.json | 57 +++++++++++ font-packs/scripts/build-noto-static.py | 85 +++++++++++++++++ package.json | 10 +- scripts/build-desktop-with-font-pack.mjs | 45 +-------- scripts/build-font-packs.mjs | 75 +++++++++++++++ scripts/stage-release.mjs | 4 +- themes/gov-red-standard/theme.css | 10 +- themes/gov-red-standard/theme.json | 12 --- themes/red-briefing/theme.css | 8 +- themes/red-briefing/theme.json | 25 +++++ 24 files changed, 508 insertions(+), 180 deletions(-) create mode 100644 font-packs/assets/mdtp-sans-sc/1.0.0/MdTPSansSC-Bold.ttf create mode 100644 font-packs/assets/mdtp-sans-sc/1.0.0/MdTPSansSC-Bold.woff2 create mode 100644 font-packs/assets/mdtp-sans-sc/1.0.0/MdTPSansSC-Regular.ttf create mode 100644 font-packs/assets/mdtp-sans-sc/1.0.0/MdTPSansSC-Regular.woff2 create mode 100644 font-packs/bundle.json create mode 100644 font-packs/recipes/mdtp-sans-sc/OFL-NotoSansSC.txt create mode 100644 font-packs/recipes/mdtp-sans-sc/SOURCE.md create mode 100644 font-packs/recipes/mdtp-sans-sc/recipe.json create mode 100644 font-packs/scripts/build-noto-static.py create mode 100644 scripts/build-font-packs.mjs diff --git a/apps/desktop/electron-builder.config.cjs b/apps/desktop/electron-builder.config.cjs index 3b1af75..2443f47 100644 --- a/apps/desktop/electron-builder.config.cjs +++ b/apps/desktop/electron-builder.config.cjs @@ -41,6 +41,11 @@ module.exports = { to: "samples", filter: ["**/*"] }, + { + from: "../../output/font-packs/root", + to: "font-packs", + filter: ["**/*"] + }, { from: `.runtime/pandoc/${pandocRuntime.version}/windows-x86_64`, to: `pandoc/${pandocRuntime.version}/windows-x86_64`, diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 503e8ca..cbe3265 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -130,7 +130,7 @@ function getDesktopThemeDirectory() { : path.resolve(currentDirectory, "../../..", ".local", "themes"); } -function getDesktopFontPackDirectory() { +function getDesktopUserFontPackDirectory() { if (!app.isPackaged) { return path.resolve( currentDirectory, @@ -147,6 +147,15 @@ function getDesktopFontPackDirectory() { function createDesktopApplicationService() { const projectRoot = path.resolve(currentDirectory, "../../.."); + const fontPackRoots = app.isPackaged + ? [ + path.join(process.resourcesPath, "font-packs"), + getDesktopUserFontPackDirectory() + ] + : [ + path.join(projectRoot, "output", "font-packs", "root"), + getDesktopUserFontPackDirectory() + ]; return createApplicationService({ bundledRoot: app.isPackaged ? path.join(process.resourcesPath, "themes") @@ -155,7 +164,7 @@ function createDesktopApplicationService() { createAssetUrl: (themeId, assetPath) => `${APP_SCHEME}://${THEME_HOST}/${encodeURIComponent(themeId)}/${encodeThemeAssetPath(assetPath)}`, fontPacks: { - roots: [getDesktopFontPackDirectory()], + roots: fontPackRoots, appVersion: app.getVersion(), createAssetUrl: ( packId, diff --git a/apps/desktop/tests/release-build.test.ts b/apps/desktop/tests/release-build.test.ts index 843a320..e01c191 100644 --- a/apps/desktop/tests/release-build.test.ts +++ b/apps/desktop/tests/release-build.test.ts @@ -72,7 +72,7 @@ describe("桌面发行构建链", () => { "npm run prepare:pandoc" ); expect(rootManifest.scripts?.["desktop:package"]).toBe( - "npm run package -w @md-to-pdf/desktop" + "npm run build:font-pack && npm run package -w @md-to-pdf/desktop" ); }); @@ -180,32 +180,28 @@ describe("桌面发行构建链", () => { ); }); - it("仅在组合构建时接入经过哈希校验的同目录字体包", () => { + it("在安装版与便携版中强制内置完整字体包集合", () => { const builderConfig = readFileSync( `${desktopRoot}/electron-builder.config.cjs`, "utf8" ); - const installerInclude = readFileSync( - `${desktopRoot}/build/installer.nsh`, + const desktopMain = readFileSync( + `${desktopRoot}/src/main.ts`, "utf8" ); - const companionBuild = readFileSync( + const bundledBuild = readFileSync( `${projectRoot}/scripts/build-desktop-with-font-pack.mjs`, "utf8" ); - expect(builderConfig).toContain("MD_TO_PDF_NSIS_INCLUDE"); - expect(installerInclude).toContain("FONT_PACK_COMPANION_SHA256"); - expect(installerInclude).toContain("StdUtils.HashFile"); - expect(installerInclude).toContain("/WITHFONTPACK="); - expect(companionBuild).toContain("createDesktopCompanionInclude"); - expect(companionBuild).toContain("MD_TO_PDF_NSIS_INCLUDE"); - expect(companionBuild).toContain( - "MorphDoc-FontPack-mdtp-serif-sc-1.0.0-x86_64-Setup.exe" - ); - expect(companionBuild).toContain( - "MorphDoc-FontPack-mdtp-serif-sc-1.0.0.zip" + expect(builderConfig).toContain('../../output/font-packs/root'); + expect(builderConfig).toContain('to: "font-packs"'); + expect(desktopMain).toContain( + 'path.join(process.resourcesPath, "font-packs")' ); + expect(desktopMain).toContain("getDesktopUserFontPackDirectory()"); + expect(bundledBuild).not.toContain("createDesktopCompanionInclude"); + expect(bundledBuild).toContain("已生成内置字体包"); }); it("将 DOCX Lua Filter 复制到桌面主进程资源目录", () => { diff --git a/deploy/Dockerfile b/deploy/Dockerfile index 90a3d18..f294684 100644 --- a/deploy/Dockerfile +++ b/deploy/Dockerfile @@ -166,6 +166,7 @@ ENV HOST=127.0.0.1 \ FONT_PACK_ROOT=/app/.local/font-packs RUN test -f /app/.local/font-packs/mdtp-serif-sc/1.0.0/font-pack.json +RUN test -f /app/.local/font-packs/mdtp-sans-sc/1.0.0/font-pack.json USER pwuser diff --git a/deploy/verify-font-pack-compose.mjs b/deploy/verify-font-pack-compose.mjs index dd0b75e..e3cf7da 100644 --- a/deploy/verify-font-pack-compose.mjs +++ b/deploy/verify-font-pack-compose.mjs @@ -15,12 +15,10 @@ const image = process.env.MD_TO_PDF_IMAGE?.trim() || "md-to-pdf:local"; const fontPackRoot = path.resolve( path.join(repositoryRoot, "output", "font-packs", "root") ); -const manifestPath = path.join( - fontPackRoot, - "mdtp-serif-sc", - "1.0.0", - "font-pack.json" -); +const expectedFontPacks = [ + { id: "mdtp-serif-sc", version: "1.0.0" }, + { id: "mdtp-sans-sc", version: "1.0.0" } +]; function assert(condition, message) { if (!condition) { @@ -126,7 +124,14 @@ assert( fontPackStatus?.isDirectory(), `未找到有效字体包根目录:${fontPackRoot}。请先运行 npm run build:font-pack。` ); -const localManifest = await readFile(manifestPath); +const localManifests = await Promise.all( + expectedFontPacks.map(async (pack) => ({ + ...pack, + content: await readFile( + path.join(fontPackRoot, pack.id, pack.version, "font-pack.json") + ) + })) +); const buildEnvironment = { ...process.env, MD_TO_PDF_IMAGE: image @@ -137,22 +142,26 @@ run( { env: buildEnvironment } ); run("docker", ["image", "inspect", image], { capture: true }); -const embeddedManifestHash = run( - "docker", - [ - "run", - "--rm", - "--entrypoint", - "sha256sum", - image, - "/app/.local/font-packs/mdtp-serif-sc/1.0.0/font-pack.json" - ], - { capture: true } -).split(/\s+/u)[0]; -assert( - embeddedManifestHash === sha256(localManifest), - "镜像内字体包清单与本次受校验构建产物不一致" -); +const embeddedManifestHashes = {}; +for (const manifest of localManifests) { + const embeddedHash = run( + "docker", + [ + "run", + "--rm", + "--entrypoint", + "sha256sum", + image, + `/app/.local/font-packs/${manifest.id}/${manifest.version}/font-pack.json` + ], + { capture: true } + ).split(/\s+/u)[0]; + assert( + embeddedHash === sha256(manifest.content), + `镜像内字体包清单不一致:${manifest.id}@${manifest.version}` + ); + embeddedManifestHashes[`${manifest.id}@${manifest.version}`] = embeddedHash; +} const port = await freePort(); const projectName = `morphdoc-font-pack-${randomUUID().slice(0, 8)}`; @@ -237,7 +246,7 @@ try { `${JSON.stringify( { image, - embeddedManifestSha256: embeddedManifestHash, + embeddedManifestSha256: embeddedManifestHashes, runtimeFontPackMount: false, fontPackAvailable: true, pandocVersion: capability.detectedVersion, diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 4e417b8..444efd6 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -1502,7 +1502,16 @@ ECharts 第二阶段浏览器与 PDF 验证结果: 总计 76 个阻断问题;`formal-feasibility--landscape--binding` 验证 A4 横向、非对称装订边距和独立封面整页 strict 门禁,封面因墨迹与边缘 IoU 不达标被阻断;全仓库测试、类型检查、生产构建和差异检查通过; -- 阶段 D:修复主题封面、块元素样式和字体解析、嵌入、回退; +- 阶段 D:进行中。字体门禁已扩展到 PDF 实际字体解析、DOCX 声明与嵌入 + 数量,以及 Word/WPS 是否真正采用 Office 嵌入别名。字体资产第一批已 + 新增 `mdtp-sans-sc@1.0.0` 静态 TrueType/WOFF2,与现有 + `mdtp-serif-sc@1.0.0` 组成受 `font-packs/bundle.json` 约束的确定性 + 双字体包集合;Desktop 安装版和便携版改为直接内置,Docker 同步执行 + 双清单哈希验证,不再依赖可选伴随安装器。`red-briefing` 与 + `gov-red-standard` 已收敛为每主题三个中文字体面,真实 Chromium、Word、 + WPS 输出均实际采用 MdTP 宋体/黑体,DOCX 各包含三个字体部件且字体门禁 + 失败为 0。下一步将同一映射规则扩展到其余 12 套主题,再修复封面和正文 + 语义块视觉差异; - 阶段 E:完成源码服务、Docker Web API、Desktop 安装版发布后复验; - 阶段 F:完成 `v0.6.1` 版本、发行说明、发布产物、发布提交和标签。 diff --git a/font-packs/README.md b/font-packs/README.md index 20a382d..349f122 100644 --- a/font-packs/README.md +++ b/font-packs/README.md @@ -1,9 +1,9 @@ -# 可选字体包构建 +# 内置字体包集合构建 本目录保存字体包配方、许可证、来源说明和正式冻结字体资产。字体二进制 位于固定的 `font-packs/assets/`,统一由 Git LFS 管理,普通 Git 对象只保存 -LFS 指针。普通 Desktop 主安装包不包含字体;组合 Desktop 发行把字体作为 -同目录可选附件,Docker 发行构建则把本次受校验的字体包写入镜像层。 +LFS 指针。Desktop 安装版、便携版和 Docker 镜像都直接包含本次受校验的 +完整字体包集合,不再依赖用户另行安装字体附件。 ## 源目录 @@ -16,10 +16,18 @@ font-packs/assets/mdtp-serif-sc/1.0.0/ ├── MdTPSerifSC-Regular.woff2 ├── MdTPSerifSC-Bold.ttf └── MdTPSerifSC-Bold.woff2 + +font-packs/assets/mdtp-sans-sc/1.0.0/ +├── MdTPSansSC-Regular.ttf +├── MdTPSansSC-Regular.woff2 +├── MdTPSansSC-Bold.ttf +└── MdTPSansSC-Bold.woff2 ``` 该目录中的 `ttf/otf/woff/woff2` 由 `.gitattributes` 强制使用 Git LFS。 -构建器不会联网下载、转换或自动替换字体。每个输入文件都必须与配方声明的 +构建器不会联网下载或自动替换字体。派生字体通过 +`font-packs/scripts/build-noto-static.py` 从来源说明固定的 Noto CJK +可变字体生成;每个发行输入文件都必须与配方声明的 字节数和 SHA-256 完全一致;TTF 还必须是无需运行时规范化的原生静态 TrueType,并具有可安装或可编辑的文档嵌入权限。 @@ -30,17 +38,13 @@ npm run build:font-pack npm run verify:font-pack ``` -也可以显式指定源目录: - -```powershell -npm run build:font-pack -- --source-root D:/font-pack-sources/mdtp-serif-sc/1.0.0 -``` - 克隆仓库后应先执行 `git lfs pull`。缺少 LFS 内容时,字体构建会因文件大小 或 SHA-256 不匹配而拒绝继续,不允许使用系统字体或相近字体静默替代。 -默认运行时字体包根目录为 `output/font-packs/root`,构建报告写入 -`output/font-packs/build-report.json`,两者均被 Git 忽略。输出布局为: +受版本控制的 `font-packs/bundle.json` 是正式字体包集合清单。构建命令会 +逐包构建、验证,并拒绝 `output/font-packs/root` 中出现清单外字体包, +避免本地残留进入发行产物。各包报告写入 +`output/font-packs/-build-report.json`。输出布局为: ```text root///font-pack.json @@ -53,34 +57,12 @@ root///font-pack.json `Mdpdf Fandol Song` 的 400/700 字重,不匹配 `SimSun` 等通用别名,避免 错误替换仿宋、黑体、楷体或普通外部主题字体。 -## Windows 独立安装器 +`mdtp-sans-sc@1.0.0` 提供 `MdTP Sans SC`、`FandolHei`、 +`Microsoft YaHei`、`PingFang SC`、`Noto Sans CJK SC` 和 +`Source Han Sans SC` 的 400/700 字重映射。主题仍需明确声明所需字体面, +注册器不会对未声明字体进行静默替换。 -发布人员可以使用 electron-builder 锁定并校验的 NSIS 工具链构建独立 -字体包安装器,不需要额外安装系统 NSIS: - -```powershell -npm run build:font-pack:windows -``` - -默认产物为: - -```text -output/font-packs/windows/ -└── MorphDoc-FontPack-mdtp-serif-sc-1.0.0-x86_64-Setup.exe -``` - -安装器使用当前用户权限,将整个 `mdtp-serif-sc` ID 安装到: - -```text -%LOCALAPPDATA%/md-to-pdf/font-packs/mdtp-serif-sc/1.0.0/ -``` - -它不向 Windows 系统字体目录注册字体。同一版本重装保持幂等,升级时先把 -该 ID 的现有目录移入备份,再原子提升新版本;失败会回滚。独立卸载项只 -删除安装器拥有的 `mdtp-serif-sc` ID,不影响其他字体包 ID。安装器支持 -`/S` 静默安装和卸载。 - -## Desktop 伴随发行 +## Desktop 内置发行 组合构建命令为: @@ -88,32 +70,10 @@ output/font-packs/windows/ npm run desktop:make:with-font-pack ``` -该命令把主安装器、字体包安装器和字体包 ZIP 作为同目录文件发行。主安装器 -只编译字体包安装器的文件名与 SHA-256,不嵌入字体二进制;仅当同目录文件 -存在且哈希完全一致时,交互安装向导才显示默认勾选的字体包选项。字体包 -失败不回滚已安装的主程序。 - -主安装器静默运行时默认不安装伴随字体包;只有显式传入以下参数才会安装: - -```powershell -MorphDoc--x86_64-Setup.exe /S /WITHFONTPACK=1 -``` - -普通 `npm run make -w @md-to-pdf/desktop` 不启用伴随逻辑,避免本地残留的 -生成文件影响常规桌面构建。 - -## ZIP 发行附件 - -独立生成可手动部署的确定性 ZIP: - -```powershell -npm run build:font-pack:archive -``` - -产物为 `output/font-packs/release/MorphDoc-FontPack-mdtp-serif-sc-1.0.0.zip`。 -归档顶层已经包含 `mdtp-serif-sc/1.0.0/`,用户可直接解压到 -`%LOCALAPPDATA%/md-to-pdf/font-packs`。ZIP 内保留清单、许可证、来源说明 -以及 Web/DOCX 两类字体资产;相同输入会生成逐字节一致的附件。 +该命令先重建完整字体包集合,再构建主安装器和便携 ZIP。electron-builder +把 `output/font-packs/root` 复制到 `resources/font-packs`;Desktop 生产 +运行时优先读取该只读内置根目录,再读取用户目录中的额外字体包。内置包 +因此不会被缺失、旧版或同 ID 用户包替换。 ## Docker 内置发行 diff --git a/font-packs/assets/mdtp-sans-sc/1.0.0/MdTPSansSC-Bold.ttf b/font-packs/assets/mdtp-sans-sc/1.0.0/MdTPSansSC-Bold.ttf new file mode 100644 index 0000000..b59362f --- /dev/null +++ b/font-packs/assets/mdtp-sans-sc/1.0.0/MdTPSansSC-Bold.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:85fcfab7284fd72dfbbdb1a579230a9034dc6f9fc351b40cecd34d46125eebd6 +size 10586172 diff --git a/font-packs/assets/mdtp-sans-sc/1.0.0/MdTPSansSC-Bold.woff2 b/font-packs/assets/mdtp-sans-sc/1.0.0/MdTPSansSC-Bold.woff2 new file mode 100644 index 0000000..06fd8ec --- /dev/null +++ b/font-packs/assets/mdtp-sans-sc/1.0.0/MdTPSansSC-Bold.woff2 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:729e7b445a1db47e588597eaacecebb892ef4de331379ff2439fb0be95aa808a +size 4314832 diff --git a/font-packs/assets/mdtp-sans-sc/1.0.0/MdTPSansSC-Regular.ttf b/font-packs/assets/mdtp-sans-sc/1.0.0/MdTPSansSC-Regular.ttf new file mode 100644 index 0000000..0ae186c --- /dev/null +++ b/font-packs/assets/mdtp-sans-sc/1.0.0/MdTPSansSC-Regular.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a1d7e9c407d664c6d13ab9c01c48eca6775d77fd6daa3448c41ff7f1a3f316e1 +size 10596656 diff --git a/font-packs/assets/mdtp-sans-sc/1.0.0/MdTPSansSC-Regular.woff2 b/font-packs/assets/mdtp-sans-sc/1.0.0/MdTPSansSC-Regular.woff2 new file mode 100644 index 0000000..6ed67de --- /dev/null +++ b/font-packs/assets/mdtp-sans-sc/1.0.0/MdTPSansSC-Regular.woff2 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:45f4ea0072861106b028f975b05752c13b46541dc84799e2746a657593c0c62d +size 4192788 diff --git a/font-packs/bundle.json b/font-packs/bundle.json new file mode 100644 index 0000000..b00beb3 --- /dev/null +++ b/font-packs/bundle.json @@ -0,0 +1,17 @@ +{ + "schemaVersion": 1, + "packs": [ + { + "id": "mdtp-serif-sc", + "version": "1.0.0", + "recipe": "font-packs/recipes/mdtp-serif-sc/recipe.json", + "sourceRoot": "font-packs/assets/mdtp-serif-sc/1.0.0" + }, + { + "id": "mdtp-sans-sc", + "version": "1.0.0", + "recipe": "font-packs/recipes/mdtp-sans-sc/recipe.json", + "sourceRoot": "font-packs/assets/mdtp-sans-sc/1.0.0" + } + ] +} diff --git a/font-packs/recipes/mdtp-sans-sc/OFL-NotoSansSC.txt b/font-packs/recipes/mdtp-sans-sc/OFL-NotoSansSC.txt new file mode 100644 index 0000000..94d1bf7 --- /dev/null +++ b/font-packs/recipes/mdtp-sans-sc/OFL-NotoSansSC.txt @@ -0,0 +1,94 @@ +Copyright 2012 Google Inc. All Rights Reserved. + +This Font Software is licensed under the SIL Open Font License, +Version 1.1. + +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font +creation efforts of academic and linguistic communities, and to +provide a free and open framework in which fonts may be shared and +improved in partnership with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply to +any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software +components as distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, +deleting, or substituting -- in part or in whole -- any of the +components of the Original Version, by changing formats or by porting +the Font Software to a new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, +modify, redistribute, and sell modified and unmodified copies of the +Font Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, in +Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the +corresponding Copyright Holder. This restriction only applies to the +primary font name as presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created using +the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/font-packs/recipes/mdtp-sans-sc/SOURCE.md b/font-packs/recipes/mdtp-sans-sc/SOURCE.md new file mode 100644 index 0000000..ee95ffb --- /dev/null +++ b/font-packs/recipes/mdtp-sans-sc/SOURCE.md @@ -0,0 +1,29 @@ +# MdTP Sans SC 字体来源说明 + +本字体包的 Regular 400 与 Bold 700 字形面派生自 Noto Sans SC 可变字体。 + +- 上游项目:Noto CJK +- 上游仓库: +- Google Fonts 源提交:`985fa52c81c1d6692ccdd82bc3656e8fb932fd89` +- 上游源文件:`google-fonts/NotoSansSC[wght].ttf` +- 上游源 SHA-256:`b1736bb633392cfc59b50d97c353d234111bd1721c9c83eaf643eaab9392e7f9` +- 许可证:SIL Open Font License 1.1 + +发行字形面使用 fontTools 4.60.1 从 `wght` 轴固定生成,并将主字体家族改名 +为 `MdTP Sans SC`、PostScript 前缀改名为 `MdTPSansSC`。改名用于明确 +标识派生版本,并避免派生字体占用上游字体名称。字形轮廓与字符覆盖未做 +删减;`OS/2.fsType` 为 0,允许安装和文档嵌入。 + +可复现生成命令: + +```powershell +python font-packs/scripts/build-noto-static.py ` + --source .local/font-pack-upstream/NotoSansSC-wght.ttf ` + --output font-packs/assets/mdtp-sans-sc/1.0.0 ` + --family "MdTP Sans SC" ` + --postscript-family MdTPSansSC +``` + +字体包构建器不从网络下载字体。发布人员必须先核对固定上游文件的 +SHA-256,再生成配方声明的四个冻结资源;构建器会继续校验文件大小、 +SHA-256、格式、字体内部家族、字重与嵌入权限。 diff --git a/font-packs/recipes/mdtp-sans-sc/recipe.json b/font-packs/recipes/mdtp-sans-sc/recipe.json new file mode 100644 index 0000000..72512f1 --- /dev/null +++ b/font-packs/recipes/mdtp-sans-sc/recipe.json @@ -0,0 +1,57 @@ +{ + "recipeVersion": 1, + "manifestVersion": 1, + "id": "mdtp-sans-sc", + "version": "1.0.0", + "name": "MdTP 中文黑体兼容包", + "description": "为现代文档与商务主题提供经过 Word/WPS 验证的原生静态中文黑体 Regular 与 Bold 字形面。", + "internalFamily": "MdTP Sans SC", + "license": "OFL-1.1", + "licenseSource": "OFL-NotoSansSC.txt", + "licensePath": "OFL-NotoSansSC.txt", + "licenseBytes": 4350, + "licenseSha256": "5e0da210fb04058a8c0087985d2d456b931c2579811a49655721d3cf0c36b6d6", + "noticeSource": "SOURCE.md", + "noticePath": "SOURCE.md", + "compatibility": { + "minimumAppVersion": "0.6.0" + }, + "faces": [ + { + "id": "sans-regular", + "targets": ["MdTP Sans SC", "FandolHei", "Microsoft YaHei", "PingFang SC", "Noto Sans CJK SC", "Source Han Sans SC"], + "weight": 400, + "style": "normal", + "web": { + "source": "MdTPSansSC-Regular.woff2", + "path": "fonts/MdTPSansSC-Regular.woff2", + "bytes": 4192788, + "sha256": "45f4ea0072861106b028f975b05752c13b46541dc84799e2746a657593c0c62d" + }, + "docx": { + "source": "MdTPSansSC-Regular.ttf", + "path": "fonts/MdTPSansSC-Regular.ttf", + "bytes": 10596656, + "sha256": "a1d7e9c407d664c6d13ab9c01c48eca6775d77fd6daa3448c41ff7f1a3f316e1" + } + }, + { + "id": "sans-bold", + "targets": ["MdTP Sans SC", "FandolHei", "Microsoft YaHei", "PingFang SC", "Noto Sans CJK SC", "Source Han Sans SC"], + "weight": 700, + "style": "normal", + "web": { + "source": "MdTPSansSC-Bold.woff2", + "path": "fonts/MdTPSansSC-Bold.woff2", + "bytes": 4314832, + "sha256": "729e7b445a1db47e588597eaacecebb892ef4de331379ff2439fb0be95aa808a" + }, + "docx": { + "source": "MdTPSansSC-Bold.ttf", + "path": "fonts/MdTPSansSC-Bold.ttf", + "bytes": 10586172, + "sha256": "85fcfab7284fd72dfbbdb1a579230a9034dc6f9fc351b40cecd34d46125eebd6" + } + } + ] +} diff --git a/font-packs/scripts/build-noto-static.py b/font-packs/scripts/build-noto-static.py new file mode 100644 index 0000000..5490c91 --- /dev/null +++ b/font-packs/scripts/build-noto-static.py @@ -0,0 +1,85 @@ +"""从固定的 Noto CJK 可变字体生成墨呈静态字体面。""" + +from __future__ import annotations + +import argparse +from pathlib import Path + +from fontTools.ttLib import TTFont +from fontTools.varLib.instancer import instantiateVariableFont + + +def set_name(font: TTFont, name_id: int, value: str) -> None: + name_table = font["name"] + for platform_id, encoding_id, language_id in ( + (3, 1, 0x0409), + (1, 0, 0), + ): + name_table.setName( + value, + name_id, + platform_id, + encoding_id, + language_id, + ) + + +def build_face( + source: Path, + output_root: Path, + family: str, + postscript_family: str, + weight: int, + style: str, +) -> None: + font = TTFont(source, recalcTimestamp=False) + instantiateVariableFont(font, {"wght": weight}, inplace=True, optimize=True) + + set_name(font, 1, family) + set_name(font, 2, style) + set_name(font, 4, f"{family} {style}") + set_name(font, 6, f"{postscript_family}-{style}") + set_name(font, 16, family) + set_name(font, 17, style) + set_name(font, 25, postscript_family) + font["OS/2"].usWeightClass = weight + font["OS/2"].fsType = 0 + + output_root.mkdir(parents=True, exist_ok=True) + ttf_path = output_root / f"{postscript_family}-{style}.ttf" + font.flavor = None + font.save(ttf_path, reorderTables=False) + + woff2_path = output_root / f"{postscript_family}-{style}.woff2" + font.flavor = "woff2" + font.save(woff2_path, reorderTables=False) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--source", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--family", required=True) + parser.add_argument("--postscript-family", required=True) + arguments = parser.parse_args() + + build_face( + arguments.source, + arguments.output, + arguments.family, + arguments.postscript_family, + 400, + "Regular", + ) + build_face( + arguments.source, + arguments.output, + arguments.family, + arguments.postscript_family, + 700, + "Bold", + ) + + +if __name__ == "__main__": + main() diff --git a/package.json b/package.json index 61c7743..e09a1ab 100644 --- a/package.json +++ b/package.json @@ -25,13 +25,11 @@ "dev:web": "npm run dev -w @md-to-pdf/web", "dev:desktop": "npm run dev -w @md-to-pdf/desktop", "desktop:dev": "npm run build -w @md-to-pdf/markdown-echarts && npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/semantic-document && npm run build -w @md-to-pdf/font-pack-registry && npm run build -w @md-to-pdf/docx-engine && npm run build -w @md-to-pdf/renderer && npm run build -w @md-to-pdf/application && npm run build -w @md-to-pdf/preview-engine && concurrently -k -n web,desktop \"npm:dev:web\" \"npm:dev:desktop\"", - "desktop:package": "npm run package -w @md-to-pdf/desktop", - "build:font-pack": "npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/docx-theme-engine && npm run build -w @md-to-pdf/font-pack-registry && npm run build -w @md-to-pdf/docx-engine && npm run build -w @md-to-pdf/font-pack-builder && node packages/font-pack-builder/dist/cli.js build", - "verify:font-pack": "npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/docx-theme-engine && npm run build -w @md-to-pdf/font-pack-registry && npm run build -w @md-to-pdf/docx-engine && npm run build -w @md-to-pdf/font-pack-builder && node packages/font-pack-builder/dist/cli.js verify", + "desktop:package": "npm run build:font-pack && npm run package -w @md-to-pdf/desktop", + "build:font-pack": "npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/docx-theme-engine && npm run build -w @md-to-pdf/font-pack-registry && npm run build -w @md-to-pdf/docx-engine && npm run build -w @md-to-pdf/font-pack-builder && node scripts/build-font-packs.mjs", + "verify:font-pack": "npm run build:font-pack", "verify:docker-font-pack": "npm run verify:font-pack && npm run build:web-runtime && npm run build -w @md-to-pdf/server && node deploy/verify-font-pack-compose.mjs", - "build:font-pack:windows": "npm run verify:font-pack && node packages/font-pack-builder/dist/cli.js windows-installer", - "build:font-pack:archive": "npm run verify:font-pack && node packages/font-pack-builder/dist/cli.js archive", - "desktop:make:with-font-pack": "npm run build:font-pack:windows && npm run build:font-pack:archive && node scripts/build-desktop-with-font-pack.mjs", + "desktop:make:with-font-pack": "npm run build:font-pack && node scripts/build-desktop-with-font-pack.mjs", "release:stage": "node scripts/stage-release.mjs", "test:release-stage": "node --test scripts/stage-release.test.mjs", "test:docx-layout-visual-matrix-cases": "node --test scripts/docx-layout-visual-matrix-cases.test.mjs", diff --git a/scripts/build-desktop-with-font-pack.mjs b/scripts/build-desktop-with-font-pack.mjs index bb79474..e08b1cf 100644 --- a/scripts/build-desktop-with-font-pack.mjs +++ b/scripts/build-desktop-with-font-pack.mjs @@ -1,45 +1,13 @@ -import { copyFile, mkdir, readFile } from "node:fs/promises"; +import { readFile } from "node:fs/promises"; import { spawn } from "node:child_process"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; -import { createDesktopCompanionInclude } from "../packages/font-pack-builder/dist/index.js"; const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const desktopRoot = join(repositoryRoot, "apps", "desktop"); const desktopManifest = JSON.parse( await readFile(join(desktopRoot, "package.json"), "utf8") ); -const fontInstallerName = - "MorphDoc-FontPack-mdtp-serif-sc-1.0.0-x86_64-Setup.exe"; -const fontArchiveName = "MorphDoc-FontPack-mdtp-serif-sc-1.0.0.zip"; -const fontInstallerPath = join( - repositoryRoot, - "output", - "font-packs", - "windows", - fontInstallerName -); -const generatedInclude = join( - repositoryRoot, - "output", - "font-packs", - "desktop-companion.nsh" -); -const fontArchivePath = join( - repositoryRoot, - "output", - "font-packs", - "release", - fontArchiveName -); - -await createDesktopCompanionInclude({ - installerPath: fontInstallerPath, - baseIncludePath: join(desktopRoot, "build", "installer.nsh"), - outputPath: generatedInclude, - displayName: "MdTP 中文宋体兼容包 1.0.0" -}); - const npmCli = process.env.npm_execpath || join(dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js"); @@ -49,10 +17,7 @@ await new Promise((resolvePromise, reject) => { [npmCli, "run", "make", "-w", "@md-to-pdf/desktop"], { cwd: repositoryRoot, - env: { - ...process.env, - MD_TO_PDF_NSIS_INCLUDE: generatedInclude - }, + env: process.env, stdio: "inherit", windowsHide: true } @@ -71,10 +36,6 @@ await new Promise((resolvePromise, reject) => { }); }); -const desktopOutput = join(desktopRoot, "out", `v${desktopManifest.version}`); -await mkdir(desktopOutput, { recursive: true }); -await copyFile(fontInstallerPath, join(desktopOutput, fontInstallerName)); -await copyFile(fontArchivePath, join(desktopOutput, fontArchiveName)); process.stdout.write( - `主安装器、字体包伴随安装器与 ZIP 附件已生成:${desktopOutput}\n` + `已生成内置字体包的主安装器与便携 ZIP:${join(desktopRoot, "out", `v${desktopManifest.version}`)}\n` ); diff --git a/scripts/build-font-packs.mjs b/scripts/build-font-packs.mjs new file mode 100644 index 0000000..e3789cd --- /dev/null +++ b/scripts/build-font-packs.mjs @@ -0,0 +1,75 @@ +import { readFile } from "node:fs/promises"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { + buildFontPack, + verifyFontPack +} from "../packages/font-pack-builder/dist/index.js"; +import { discoverFontPacks } from "../packages/font-pack-registry/dist/index.js"; + +const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const bundlePath = join(repositoryRoot, "font-packs", "bundle.json"); +const bundle = JSON.parse(await readFile(bundlePath, "utf8")); +const outputRoot = join(repositoryRoot, "output", "font-packs", "root"); +const appVersion = process.env.npm_package_version || "0.6.0"; + +if ( + bundle.schemaVersion !== 1 || + !Array.isArray(bundle.packs) || + bundle.packs.length === 0 +) { + throw new Error("字体包集合清单无效"); +} + +const identities = new Set(); +const reports = []; +for (const pack of bundle.packs) { + const identity = `${pack.id}@${pack.version}`; + if (identities.has(identity)) { + throw new Error(`字体包集合存在重复项:${identity}`); + } + identities.add(identity); + const reportPath = join( + repositoryRoot, + "output", + "font-packs", + `${pack.id}-build-report.json` + ); + const built = await buildFontPack({ + recipePath: resolve(repositoryRoot, pack.recipe), + sourceRoot: resolve(repositoryRoot, pack.sourceRoot), + outputRoot, + reportPath + }); + const verified = await verifyFontPack({ + root: outputRoot, + appVersion, + packId: pack.id, + packVersion: pack.version + }); + reports.push({ identity, built, verified }); +} + +const registry = await discoverFontPacks({ + roots: [outputRoot], + appVersion +}); +const errors = registry.diagnostics.filter((item) => item.severity === "error"); +if (errors.length > 0) { + throw new Error( + `字体包集合注册失败:${errors.map((item) => `${item.code} ${item.message}`).join(";")}` + ); +} +const actualIdentities = registry.packs + .map((pack) => `${pack.id}@${pack.version}`) + .sort(); +const expectedIdentities = [...identities].sort(); +if (JSON.stringify(actualIdentities) !== JSON.stringify(expectedIdentities)) { + throw new Error( + `字体包输出根目录包含清单外内容:期望 ${expectedIdentities.join(", ")},实际 ${actualIdentities.join(", ")}` + ); +} + +process.stdout.write( + `${JSON.stringify({ appVersion, outputRoot, registryFingerprint: registry.fingerprint, reports }, null, 2)}\n` +); diff --git a/scripts/stage-release.mjs b/scripts/stage-release.mjs index b4382e6..738722a 100644 --- a/scripts/stage-release.mjs +++ b/scripts/stage-release.mjs @@ -184,9 +184,7 @@ export async function stageRelease(options = {}) { ); const desktopNames = [ `MorphDoc-${version}-x86_64-Setup.exe`, - `MorphDoc-${version}-x86_64.zip`, - "MorphDoc-FontPack-mdtp-serif-sc-1.0.0-x86_64-Setup.exe", - "MorphDoc-FontPack-mdtp-serif-sc-1.0.0.zip" + `MorphDoc-${version}-x86_64.zip` ]; const releaseNotesSource = join( repositoryRoot, diff --git a/themes/gov-red-standard/theme.css b/themes/gov-red-standard/theme.css index 44fea3d..30e10ec 100644 --- a/themes/gov-red-standard/theme.css +++ b/themes/gov-red-standard/theme.css @@ -8,7 +8,7 @@ #write { color: var(--official-ink); font-family: - "FandolFang", "FangSong", "STFangsong", "Noto Serif CJK SC", + "FandolSong", "FangSong", "STFangsong", "Noto Serif CJK SC", "Source Han Serif SC", "SimSun", serif; font-size: 21.333px; line-height: 1.75; @@ -64,13 +64,13 @@ #write .doc-signatory > span { font-family: - "FandolFang", "FangSong", "STFangsong", + "FandolSong", "FangSong", "STFangsong", "Noto Serif CJK SC", serif; } #write .doc-signatory { font-family: - "FandolKai", "KaiTi", "STKaiti", + "FandolSong", "KaiTi", "STKaiti", "Noto Serif CJK SC", serif; } @@ -110,7 +110,7 @@ #write h5, #write h6 { font-family: - "FandolKai", "KaiTi", "STKaiti", + "FandolSong", "KaiTi", "STKaiti", "Noto Serif CJK SC", serif; font-weight: 400; } @@ -222,7 +222,7 @@ border-bottom: 0.35mm solid var(--official-rule); break-inside: avoid; font-family: - "FandolFang", "FangSong", "STFangsong", + "FandolSong", "FangSong", "STFangsong", "Noto Serif CJK SC", serif; font-size: 0.875em; line-height: 1.7; diff --git a/themes/gov-red-standard/theme.json b/themes/gov-red-standard/theme.json index 86a3459..25e670e 100644 --- a/themes/gov-red-standard/theme.json +++ b/themes/gov-red-standard/theme.json @@ -77,23 +77,11 @@ "weight": 700, "license": "GPL-3.0-or-later WITH Font-exception-2.0" }, - { - "family": "FandolFang", - "aliases": ["Mdpdf Fandol Fang", "FangSong", "STFangsong"], - "source": "theme-shared:official-fonts/FandolFang-Regular.woff2", - "license": "GPL-3.0-or-later WITH Font-exception-2.0" - }, { "family": "FandolHei", "aliases": ["Mdpdf Fandol Hei", "SimHei", "Noto Sans CJK SC", "Source Han Sans SC", "Microsoft YaHei"], "source": "theme-shared:official-fonts/FandolHei-Regular.woff2", "license": "GPL-3.0-or-later WITH Font-exception-2.0" - }, - { - "family": "FandolKai", - "aliases": ["Mdpdf Fandol Kai", "KaiTi", "STKaiti"], - "source": "theme-shared:official-fonts/FandolKai-Regular.woff2", - "license": "GPL-3.0-or-later WITH Font-exception-2.0" } ] }, diff --git a/themes/red-briefing/theme.css b/themes/red-briefing/theme.css index ceb7bf1..84f573f 100644 --- a/themes/red-briefing/theme.css +++ b/themes/red-briefing/theme.css @@ -8,7 +8,7 @@ #write { color: var(--briefing-ink); font-family: - "Noto Serif CJK SC", "Source Han Serif SC", + "FandolSong", "Noto Serif CJK SC", "Source Han Serif SC", "Microsoft YaHei", "SimSun", serif; font-size: 18px; line-height: 1.8; @@ -27,7 +27,7 @@ border-bottom: 1mm solid var(--briefing-red); color: var(--briefing-red); font-family: - "STZhongsong", "Noto Serif CJK SC", "Source Han Serif SC", serif; + "FandolSong", "STZhongsong", "Noto Serif CJK SC", "Source Han Serif SC", serif; font-size: 2.35em; font-weight: 700; letter-spacing: 0.18em; @@ -55,7 +55,7 @@ #write .doc-title { margin: 0 0 8mm; font-family: - "STZhongsong", "Noto Serif CJK SC", "Source Han Serif SC", serif; + "FandolSong", "STZhongsong", "Noto Serif CJK SC", "Source Han Serif SC", serif; font-size: 1.65em; font-weight: 700; line-height: 1.4; @@ -68,7 +68,7 @@ #write h4 { margin: 1.35em 0 0.5em; font-family: - "Noto Sans CJK SC", "Source Han Sans SC", "Microsoft YaHei", sans-serif; + "MdTP Sans SC", "Noto Sans CJK SC", "Source Han Sans SC", "Microsoft YaHei", sans-serif; font-weight: 700; line-height: 1.5; } diff --git a/themes/red-briefing/theme.json b/themes/red-briefing/theme.json index 58e2c46..add8276 100644 --- a/themes/red-briefing/theme.json +++ b/themes/red-briefing/theme.json @@ -32,5 +32,30 @@ "docxStyle": { "preset": "official" }, + "docxFonts": { + "faces": [ + { + "family": "FandolSong", + "aliases": ["Mdpdf Fandol Song", "STZhongsong", "Noto Serif CJK SC", "Source Han Serif SC", "SimSun"], + "source": "theme-shared:official-fonts/FandolSong-Regular.woff2", + "weight": 400, + "license": "GPL-3.0-or-later WITH Font-exception-2.0" + }, + { + "family": "FandolSong", + "aliases": ["Mdpdf Fandol Song", "STZhongsong", "Noto Serif CJK SC", "Source Han Serif SC", "SimSun"], + "source": "theme-shared:official-fonts/FandolSong-Bold.woff2", + "weight": 700, + "license": "GPL-3.0-or-later WITH Font-exception-2.0" + }, + { + "family": "MdTP Sans SC", + "aliases": ["FandolHei", "Noto Sans CJK SC", "Source Han Sans SC", "Microsoft YaHei"], + "source": "theme-shared:official-fonts/FandolHei-Regular.woff2", + "weight": 700, + "license": "OFL-1.1" + } + ] + }, "bundled": true }