Font related improvements

This commit is contained in:
2026-08-13 12:05:47 -07:00
parent 4eda93cf24
commit acc4c117f7
19 changed files with 317 additions and 41 deletions
+7
View File
@@ -56,6 +56,13 @@ build 把同一个 build ID 注入浏览器 bundle 与 worker,发现更新后
metadata 的唯一文案来源。Vite 在构建时解析并注入它,Rust 则嵌入同一文件来验证允许保存的 locale。增加文案键时必须为每个 locale 提供值;增加语言时同时增加完整
`[locales."<code>".messages]` 段。业务命令如“点歌”是 Bilibili 输入协议,不属于 UI 翻译。
## 字体资源
Noto Serif SC、ZCOOL XiaoWei 与 LXGW
WenKai 由锁定的 Fontsource 依赖提供;漓雨手书则以固定版本的 WOFF2 文件保存在
`public/fonts/`。浏览器从 Rust 静态服务同域加载这些字体,不依赖 OBS 设备的系统字体。每套字体的 OFL-1.1 许可证均输出到
`/fonts/licenses/`。
## 格式化与构建
```bash
+30
View File
@@ -8,6 +8,9 @@
"name": "lxc-stream-overlay",
"version": "0.1.0",
"dependencies": {
"@fontsource/lxgw-wenkai": "5.3.0",
"@fontsource/noto-serif-sc": "5.3.0",
"@fontsource/zcool-xiaowei": "5.3.0",
"react": "19.1.1",
"react-dom": "19.1.1"
},
@@ -745,6 +748,33 @@
"node": ">=18"
}
},
"node_modules/@fontsource/lxgw-wenkai": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@fontsource/lxgw-wenkai/-/lxgw-wenkai-5.3.0.tgz",
"integrity": "sha512-+U5AAOXeaB7o+3G0Qm7I7Djz2Jd1hqNY+kClOZH8bTunyyGa2WxY12LZftt2AqUsxlfxwdQpd75Js1HQIt4H4g==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@fontsource/noto-serif-sc": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@fontsource/noto-serif-sc/-/noto-serif-sc-5.3.0.tgz",
"integrity": "sha512-0/zaEFkidiWldE62rTeD74x8ygUsQvejiSNtO0LQxQk3qpaHnlMZ3w4C7yH80B4KTIg8VKeFP4oSgwWMchY9+g==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@fontsource/zcool-xiaowei": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@fontsource/zcool-xiaowei/-/zcool-xiaowei-5.3.0.tgz",
"integrity": "sha512-iRuUPl9ONFbYzi6JcEshpE4VedINyzQ4hv0Z5XEt0cB0pMPiQeqc2Qvv1NwJCNzkPwG02VCdcO+cF2K8ylYn2A==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+4 -1
View File
@@ -3,13 +3,16 @@
"private": true,
"version": "0.1.0",
"scripts": {
"build": "tsc -b && vite build",
"build": "tsc -b && vite build && node scripts/check-font-assets.mjs",
"dev": "vite",
"docs:check": "node scripts/check-doc-links.mjs ../..",
"format": "prettier --ignore-path ../../.prettierignore --write . ../server-rust/README.md ../../README.md ../../docs ../../compose.yaml",
"format:check": "prettier --ignore-path ../../.prettierignore --check . ../server-rust/README.md ../../README.md ../../docs ../../compose.yaml"
},
"dependencies": {
"@fontsource/lxgw-wenkai": "5.3.0",
"@fontsource/noto-serif-sc": "5.3.0",
"@fontsource/zcool-xiaowei": "5.3.0",
"react": "19.1.1",
"react-dom": "19.1.1"
},
+19
View File
@@ -0,0 +1,19 @@
# Bundled font notice
## Liyu Shoushu
`liyu-shoushu-v0.107.woff2` is a WOFF2 conversion of `LiyuShoushu.ttf` from the official
[Liyu Shoushu v0.107 release](https://github.com/InkPantherType/LiyuShoushu/releases/tag/v0.107).
- Release tag commit: `219a0f42cb7b3f0d00be6e5bf9f8b5298a1f3060`
- Original TTF SHA-256: `16554439b7e95da1fc354ed7b2c1551e83e942c3c5afb2a2e883761c6037dc22`
- Bundled WOFF2 SHA-256: `b17f8c4fc6612a539d7a46e0eb5f57fdc732cff1d17804563b00a3f0a7eb9ab3`
- Conversion: FontTools 4.63.0 WOFF2 compression with Brotli 1.2.0; no glyph subsetting
- Coverage retained: 18,828 glyphs and 18,221 Unicode mappings
- License: SIL Open Font License 1.1; see
[`licenses/liyu-shoushu-OFL.txt`](licenses/liyu-shoushu-OFL.txt)
The original font credits Yuji Kataoka and the Yuji Project Authors for the base brush glyphs, LXGW
and the LXGW ZhenKai Project Authors for punctuation and symbols, and Yuchen Tian's
[zi2zi-JiT](https://github.com/kaonashi-tyc/zi2zi-JiT) for generated Chinese glyphs. This notice
preserves the zi2zi-JiT attribution required by the upstream font documentation.
@@ -0,0 +1,94 @@
Copyright 2026 Alexander Tseng.
Portions Copyright 2021 The Yuji Project Authors (https://github.com/Kinutafontfactory/Yuji).
Portions Copyright LXGW and LXGWZhenKai Project Authors (https://github.com/lxgw/LxgwZhenKai).
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:
https://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.
Binary file not shown.
+1
View File
@@ -66,6 +66,7 @@ function isControlShell(pathname) {
function isStaticAsset(pathname) {
return (
pathname.startsWith('/assets/') ||
pathname.startsWith('/fonts/') ||
pathname.startsWith('/pwa/') ||
pathname === '/control/manifest.webmanifest'
)
@@ -0,0 +1,67 @@
import { createHash } from 'node:crypto'
import { readdir, readFile } from 'node:fs/promises'
const dist = new URL('../dist/', import.meta.url)
async function filesBelow(directory, prefix = '') {
const entries = await readdir(directory, { withFileTypes: true })
const files = []
for (const entry of entries) {
const relative = `${prefix}${entry.name}`
if (entry.isDirectory()) {
files.push(...(await filesBelow(new URL(`${entry.name}/`, directory), `${relative}/`)))
} else {
files.push(relative)
}
}
return files
}
const files = await filesBelow(dist)
const expectedFonts = ['noto-serif-sc', 'zcool-xiaowei', 'lxgw-wenkai']
const liyuFont = 'fonts/liyu-shoushu-v0.107.woff2'
for (const font of expectedFonts) {
if (!files.some(file => file.startsWith(`assets/${font}-`) && file.endsWith('.woff2'))) {
throw new Error(`Missing bundled WOFF2 assets for ${font}`)
}
if (!files.includes(`fonts/licenses/${font}-OFL.txt`)) {
throw new Error(`Missing bundled license for ${font}`)
}
}
if (!files.includes(liyuFont)) throw new Error('Missing bundled WOFF2 asset for liyu-shoushu')
if (!files.includes('fonts/licenses/liyu-shoushu-OFL.txt')) {
throw new Error('Missing bundled license for liyu-shoushu')
}
const liyuDigest = createHash('sha256')
.update(await readFile(new URL(liyuFont, dist)))
.digest('hex')
if (liyuDigest !== 'b17f8c4fc6612a539d7a46e0eb5f57fdc732cff1d17804563b00a3f0a7eb9ab3') {
throw new Error(`Unexpected liyu-shoushu WOFF2 digest: ${liyuDigest}`)
}
const legacyWoff = files.find(file => file.endsWith('.woff'))
if (legacyWoff) throw new Error(`Unexpected legacy WOFF asset: ${legacyWoff}`)
const css = (
await Promise.all(
files.filter(file => file.endsWith('.css')).map(file => readFile(new URL(file, dist), 'utf8')),
)
).join('\n')
for (const family of ['Noto Serif SC', 'ZCOOL XiaoWei', 'LXGW WenKai', 'Liyu Shoushu']) {
const declarations = [
`font-family:${family}`,
`font-family:${JSON.stringify(family)}`,
`font-family:'${family}'`,
]
if (!declarations.some(declaration => css.includes(declaration))) {
throw new Error(`Missing @font-face declaration for ${family}`)
}
}
if (!css.includes('font-display:block') || css.includes('font-display:swap')) {
throw new Error('Bundled fonts must block local fallback while loading')
}
console.log('Verified same-origin WOFF2 component fonts and licenses.')
+7
View File
@@ -0,0 +1,7 @@
@font-face {
font-family: 'Liyu Shoushu';
font-style: normal;
font-display: block;
font-weight: 400;
src: url('/fonts/liyu-shoushu-v0.107.woff2') format('woff2');
}
+4
View File
@@ -25,6 +25,10 @@ import { SongRequestOverlay } from './songOverlay'
import { useComponentStream } from './stream'
import { I18nProvider, translate, useI18n } from './i18n'
import type { Session } from './types'
import '@fontsource/noto-serif-sc/400.css'
import '@fontsource/zcool-xiaowei/400.css'
import '@fontsource/lxgw-wenkai/500.css'
import './fonts.css'
import './style.css'
import './control.css'
import './giftEffect.css'
+2
View File
@@ -105,6 +105,8 @@
}
.gift-menu-overlay.gift-menu-theme-moonlit-water {
height: 100%;
align-items: center;
padding-block: calc(clamp(8px, 1.3vmin, 16px) + var(--moonlit-edge-space));
}
+7 -5
View File
@@ -1,7 +1,7 @@
import type { CSSProperties } from 'react'
/** Stable IDs persisted by the backend; each maps to a fixed, injection-safe stack. */
export const fontFamilyIds = ['song', 'fang-song', 'kai'] as const
/** Stable IDs persisted by the backend; each maps to a bundled, same-origin webfont. */
export const fontFamilyIds = ['song', 'fang-song', 'kai', 'liyu-shoushu'] as const
export type FontFamilyId = (typeof fontFamilyIds)[number]
@@ -9,12 +9,14 @@ export const fontFamilies: ReadonlyArray<{ id: FontFamilyId; nameKey: string }>
{ id: 'song', nameKey: 'settings.font_family.song' },
{ id: 'fang-song', nameKey: 'settings.font_family.fang_song' },
{ id: 'kai', nameKey: 'settings.font_family.kai' },
{ id: 'liyu-shoushu', nameKey: 'settings.font_family.liyu_shoushu' },
]
const fontStacks: Record<FontFamilyId, string> = {
song: '"Source Han Serif SC", "Noto Serif CJK SC", "Noto Serif SC", "Songti SC", STSong, SimSun, serif',
'fang-song': 'FangSong, STFangsong, "FZShuSong-Z01", "Noto Serif SC", "Songti SC", serif',
kai: '"Kaiti SC", STKaiti, KaiTi, "LXGW WenKai", "Noto Serif SC", serif',
song: '"Noto Serif SC", serif',
'fang-song': '"ZCOOL XiaoWei", serif',
kai: '"LXGW WenKai", serif',
'liyu-shoushu': '"Liyu Shoushu", serif',
}
export function normalizeFontFamilyId(value: unknown): FontFamilyId {
+20
View File
@@ -14,6 +14,13 @@ import { parse } from 'smol-toml'
// The waiting worker is still activated explicitly by the user in the console.
const pwaBuildId = Date.now().toString(36)
const serviceWorkerSource = readFileSync(new URL('./pwa/control-sw.js', import.meta.url), 'utf8')
const bundledFontLicenses = ['noto-serif-sc', 'zcool-xiaowei', 'lxgw-wenkai'].map(font => ({
fileName: `fonts/licenses/${font}-OFL.txt`,
source: readFileSync(
new URL(`./node_modules/@fontsource/${font}/LICENSE`, import.meta.url),
'utf8',
),
}))
const i18nResource = parse(
readFileSync(new URL('../../resources/i18n.toml', import.meta.url), 'utf8'),
) as unknown as {
@@ -51,6 +58,16 @@ const manifest = {
export default defineConfig({
plugins: [
react(),
{
name: 'same-origin-woff2-fonts',
enforce: 'pre',
transform(source, id) {
if (!id.includes('/node_modules/@fontsource/') || !id.endsWith('.css')) return
return source
.replaceAll('font-display: swap', 'font-display: block')
.replace(/,\s*url\([^)]*\.woff\) format\(['"]woff['"]\)/g, '')
},
},
{
name: 'control-pwa-assets',
transformIndexHtml(html) {
@@ -61,6 +78,9 @@ export default defineConfig({
.replaceAll('__APP_DESCRIPTION__', defaultMessages['pwa.manifest.description'])
},
generateBundle() {
for (const license of bundledFontLicenses) {
this.emitFile({ type: 'asset', ...license })
}
this.emitFile({
type: 'asset',
fileName: 'control/sw.js',
+1
View File
@@ -956,6 +956,7 @@ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "libilibili"
version = "0.1.0"
source = "git+http://gitea.home.arpa/felis/libilibili.git#3cb2a902d4d9387c5581a1827cc19248b6000ae2"
dependencies = [
"base64",
"brotli",
+14 -4
View File
@@ -7,20 +7,30 @@ edition = "2024"
axum = { version = "0.8", features = ["ws", "json"] }
async-trait = "0.1"
base64 = "0.22"
chrono = { version = "0.4", default-features = false, features = ["clock", "serde"] }
chrono = { version = "0.4", default-features = false, features = [
"clock",
"serde",
] }
chacha20poly1305 = "0.10"
deadpool-postgres = "0.14"
# Kept as a sibling checkout during development. Docker Compose supplies the
# same directory as a named BuildKit context at `/libilibili`.
libilibili = { path = "../../../libilibili" }
libilibili = { git = "http://gitea.home.arpa/felis/libilibili.git" }
rand = "0.9"
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
reqwest = { version = "0.12", default-features = false, features = [
"json",
"rustls-tls",
] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = "0.10"
subtle = "2"
tokio = { version = "1", features = ["full"] }
tokio-postgres = { version = "0.7", features = ["with-serde_json-1", "with-uuid-1", "with-chrono-0_4"] }
tokio-postgres = { version = "0.7", features = [
"with-serde_json-1",
"with-uuid-1",
"with-chrono-0_4",
] }
tokio-util = "0.7"
toml = "0.8"
totp-rs = { version = "5.7", features = ["gen_secret", "qr", "zeroize"] }
+8 -3
View File
@@ -1,8 +1,8 @@
//! Shared, validated typography settings for all built-in OBS components.
//!
//! Font IDs map to fixed frontend font stacks rather than accepting arbitrary
//! CSS. This keeps settings portable between OBS hosts and prevents untrusted
//! component settings from becoming a CSS injection boundary.
//! Font IDs map to fixed, same-origin WOFF2 assets in the frontend rather than
//! accepting arbitrary CSS. This keeps settings portable between OBS hosts and
//! prevents untrusted component settings from becoming a CSS injection boundary.
use serde::{Deserialize, Serialize};
@@ -13,6 +13,7 @@ pub enum FontFamilyId {
#[default]
FangSong,
Kai,
LiyuShoushu,
}
pub const fn default_font_brightness() -> u16 {
@@ -35,5 +36,9 @@ mod tests {
);
assert_eq!(sanitize_font_brightness(1), 70);
assert_eq!(sanitize_font_brightness(u16::MAX), 180);
assert_eq!(
serde_json::to_string(&FontFamilyId::LiyuShoushu).unwrap(),
"\"liyu-shoushu\""
);
}
}