@courtifyai/docx-render 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +168 -0
- package/debug-comments.cjs +19 -0
- package/index.html +312 -0
- package/package.json +36 -0
- package/src/comments/comments-parser.ts +159 -0
- package/src/comments/index.ts +6 -0
- package/src/font-table/font-loader.ts +379 -0
- package/src/font-table/font-parser.ts +258 -0
- package/src/font-table/index.ts +22 -0
- package/src/index.ts +137 -0
- package/src/parser/document-parser.ts +1606 -0
- package/src/parser/index.ts +3 -0
- package/src/parser/xml-parser.ts +152 -0
- package/src/renderer/document-renderer.ts +2163 -0
- package/src/renderer/index.ts +1 -0
- package/src/styles/index.css +692 -0
- package/src/theme/index.ts +8 -0
- package/src/theme/theme-parser.ts +172 -0
- package/src/theme/theme-utils.ts +148 -0
- package/src/types/index.ts +847 -0
- package/tsconfig.json +27 -0
- package/vite.config.ts +26 -0
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 嵌入字体加载器
|
|
3
|
+
* 从 DOCX 文件中加载嵌入字体,支持字体解密和 CSS @font-face 注入
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import JSZip from 'jszip'
|
|
7
|
+
import {
|
|
8
|
+
IFontTable,
|
|
9
|
+
IFontDeclaration,
|
|
10
|
+
IEmbedFontRef,
|
|
11
|
+
ILoadedEmbedFont,
|
|
12
|
+
IRelationship,
|
|
13
|
+
TEmbedFontType,
|
|
14
|
+
RELATIONSHIP_TYPES,
|
|
15
|
+
} from '../types'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 嵌入字体加载器配置
|
|
19
|
+
*/
|
|
20
|
+
export interface IFontLoaderOptions {
|
|
21
|
+
/** 是否自动注入 CSS @font-face */
|
|
22
|
+
injectStyles?: boolean
|
|
23
|
+
/** 自定义样式容器(默认为 document.head) */
|
|
24
|
+
styleContainer?: HTMLElement
|
|
25
|
+
/** 字体加载超时时间(毫秒) */
|
|
26
|
+
timeout?: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const DEFAULT_OPTIONS: IFontLoaderOptions = {
|
|
30
|
+
injectStyles: true,
|
|
31
|
+
timeout: 10000,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 加载嵌入字体
|
|
36
|
+
* @param zip DOCX 文件的 JSZip 实例
|
|
37
|
+
* @param fontTable 字体表
|
|
38
|
+
* @param fontRels fontTable.xml 的关系文件内容(可选)
|
|
39
|
+
* @param options 配置选项
|
|
40
|
+
* @returns 已加载的嵌入字体数组
|
|
41
|
+
*/
|
|
42
|
+
export async function loadEmbeddedFonts(
|
|
43
|
+
zip: JSZip,
|
|
44
|
+
fontTable: IFontTable,
|
|
45
|
+
fontRels: IRelationship[],
|
|
46
|
+
options: IFontLoaderOptions = {}
|
|
47
|
+
): Promise<ILoadedEmbedFont[]> {
|
|
48
|
+
const opts = { ...DEFAULT_OPTIONS, ...options }
|
|
49
|
+
const loadedFonts: ILoadedEmbedFont[] = []
|
|
50
|
+
|
|
51
|
+
// 遍历所有有嵌入字体的字体声明
|
|
52
|
+
for (const font of fontTable.fonts) {
|
|
53
|
+
if (font.embedFontRefs.length === 0) continue
|
|
54
|
+
|
|
55
|
+
for (const embedRef of font.embedFontRefs) {
|
|
56
|
+
try {
|
|
57
|
+
const loadedFont = await loadSingleEmbeddedFont(
|
|
58
|
+
zip,
|
|
59
|
+
font,
|
|
60
|
+
embedRef,
|
|
61
|
+
fontRels,
|
|
62
|
+
opts.timeout || DEFAULT_OPTIONS.timeout!
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if (loadedFont) {
|
|
66
|
+
loadedFonts.push(loadedFont)
|
|
67
|
+
}
|
|
68
|
+
} catch (e) {
|
|
69
|
+
console.warn(`加载嵌入字体失败: ${font.name} (${embedRef.type})`, e)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 注入 CSS @font-face
|
|
75
|
+
if (opts.injectStyles && loadedFonts.length > 0) {
|
|
76
|
+
injectFontFaceStyles(loadedFonts, opts.styleContainer)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return loadedFonts
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 加载单个嵌入字体
|
|
84
|
+
*/
|
|
85
|
+
async function loadSingleEmbeddedFont(
|
|
86
|
+
zip: JSZip,
|
|
87
|
+
font: IFontDeclaration,
|
|
88
|
+
embedRef: IEmbedFontRef,
|
|
89
|
+
fontRels: IRelationship[],
|
|
90
|
+
timeout: number
|
|
91
|
+
): Promise<ILoadedEmbedFont | null> {
|
|
92
|
+
// 根据关系 ID 查找字体文件路径
|
|
93
|
+
const rel = fontRels.find(r => r.id === embedRef.id)
|
|
94
|
+
if (!rel) {
|
|
95
|
+
console.warn(`找不到嵌入字体关系: ${embedRef.id}`)
|
|
96
|
+
return null
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 构建字体文件路径
|
|
100
|
+
const fontPath = `word/${rel.target}`
|
|
101
|
+
const fontFile = zip.file(fontPath)
|
|
102
|
+
|
|
103
|
+
if (!fontFile) {
|
|
104
|
+
console.warn(`找不到嵌入字体文件: ${fontPath}`)
|
|
105
|
+
return null
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 读取字体文件
|
|
109
|
+
const fontData = await Promise.race([
|
|
110
|
+
fontFile.async('arraybuffer'),
|
|
111
|
+
new Promise<never>((_, reject) =>
|
|
112
|
+
setTimeout(() => reject(new Error('字体加载超时')), timeout)
|
|
113
|
+
),
|
|
114
|
+
])
|
|
115
|
+
|
|
116
|
+
// 如果有密钥,需要解密字体
|
|
117
|
+
let decryptedData = fontData
|
|
118
|
+
if (embedRef.key) {
|
|
119
|
+
decryptedData = decryptEmbeddedFont(fontData, embedRef.key)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 检测字体格式
|
|
123
|
+
const format = detectFontFormat(new Uint8Array(decryptedData))
|
|
124
|
+
|
|
125
|
+
// 转换为 Data URL
|
|
126
|
+
const mimeType = getFontMimeType(format)
|
|
127
|
+
const blob = new Blob([decryptedData], { type: mimeType })
|
|
128
|
+
const dataUrl = await blobToDataUrl(blob)
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
fontName: font.name,
|
|
132
|
+
type: embedRef.type,
|
|
133
|
+
dataUrl,
|
|
134
|
+
format,
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 解密嵌入字体
|
|
140
|
+
* OOXML 嵌入字体使用 XOR 加密,密钥为 32 位 GUID
|
|
141
|
+
* @param data 加密的字体数据
|
|
142
|
+
* @param keyString 密钥字符串(格式:{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx})
|
|
143
|
+
* @returns 解密后的字体数据
|
|
144
|
+
*/
|
|
145
|
+
function decryptEmbeddedFont(data: ArrayBuffer, keyString: string): ArrayBuffer {
|
|
146
|
+
// 解析密钥 GUID(移除花括号和连字符)
|
|
147
|
+
const keyHex = keyString.replace(/[{}-]/g, '')
|
|
148
|
+
if (keyHex.length !== 32) {
|
|
149
|
+
console.warn('无效的字体密钥格式:', keyString)
|
|
150
|
+
return data
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 将 GUID 转换为 16 字节密钥数组
|
|
154
|
+
// 注意:GUID 的字节顺序需要特殊处理
|
|
155
|
+
const key = new Uint8Array(16)
|
|
156
|
+
|
|
157
|
+
// 前三组需要反转字节顺序(小端序)
|
|
158
|
+
// 第一组:8 个字符 = 4 字节,反转
|
|
159
|
+
for (let i = 0; i < 4; i++) {
|
|
160
|
+
key[3 - i] = parseInt(keyHex.substr(i * 2, 2), 16)
|
|
161
|
+
}
|
|
162
|
+
// 第二组:4 个字符 = 2 字节,反转
|
|
163
|
+
for (let i = 0; i < 2; i++) {
|
|
164
|
+
key[5 - i] = parseInt(keyHex.substr(8 + i * 2, 2), 16)
|
|
165
|
+
}
|
|
166
|
+
// 第三组:4 个字符 = 2 字节,反转
|
|
167
|
+
for (let i = 0; i < 2; i++) {
|
|
168
|
+
key[7 - i] = parseInt(keyHex.substr(12 + i * 2, 2), 16)
|
|
169
|
+
}
|
|
170
|
+
// 后两组保持原顺序
|
|
171
|
+
for (let i = 0; i < 8; i++) {
|
|
172
|
+
key[8 + i] = parseInt(keyHex.substr(16 + i * 2, 2), 16)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// XOR 解密:每 16 字节与密钥进行 XOR
|
|
176
|
+
const input = new Uint8Array(data)
|
|
177
|
+
const output = new Uint8Array(input.length)
|
|
178
|
+
|
|
179
|
+
// 只解密前 32 字节(OOXML 规范)
|
|
180
|
+
const decryptLength = Math.min(32, input.length)
|
|
181
|
+
|
|
182
|
+
for (let i = 0; i < decryptLength; i++) {
|
|
183
|
+
output[i] = input[i] ^ key[i % 16]
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 剩余部分保持不变
|
|
187
|
+
for (let i = decryptLength; i < input.length; i++) {
|
|
188
|
+
output[i] = input[i]
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return output.buffer
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* 检测字体格式
|
|
196
|
+
*/
|
|
197
|
+
function detectFontFormat(data: Uint8Array): 'opentype' | 'truetype' | 'embedded-opentype' {
|
|
198
|
+
if (data.length < 4) {
|
|
199
|
+
return 'truetype'
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// OpenType (CFF) 签名: 'OTTO'
|
|
203
|
+
if (data[0] === 0x4F && data[1] === 0x54 && data[2] === 0x54 && data[3] === 0x4F) {
|
|
204
|
+
return 'opentype'
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// TrueType 签名: 0x00010000 或 'true'
|
|
208
|
+
if ((data[0] === 0x00 && data[1] === 0x01 && data[2] === 0x00 && data[3] === 0x00) ||
|
|
209
|
+
(data[0] === 0x74 && data[1] === 0x72 && data[2] === 0x75 && data[3] === 0x65)) {
|
|
210
|
+
return 'truetype'
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// EOT 签名
|
|
214
|
+
if (data[0] === 0x00 && data[1] === 0x00 && data[2] === 0x01) {
|
|
215
|
+
return 'embedded-opentype'
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// 默认假设为 TrueType
|
|
219
|
+
return 'truetype'
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* 获取字体 MIME 类型
|
|
224
|
+
*/
|
|
225
|
+
function getFontMimeType(format: string): string {
|
|
226
|
+
switch (format) {
|
|
227
|
+
case 'opentype':
|
|
228
|
+
return 'font/otf'
|
|
229
|
+
case 'truetype':
|
|
230
|
+
return 'font/ttf'
|
|
231
|
+
case 'embedded-opentype':
|
|
232
|
+
return 'application/vnd.ms-fontobject'
|
|
233
|
+
default:
|
|
234
|
+
return 'font/ttf'
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Blob 转 Data URL
|
|
240
|
+
*/
|
|
241
|
+
function blobToDataUrl(blob: Blob): Promise<string> {
|
|
242
|
+
return new Promise((resolve, reject) => {
|
|
243
|
+
const reader = new FileReader()
|
|
244
|
+
reader.onloadend = () => resolve(reader.result as string)
|
|
245
|
+
reader.onerror = reject
|
|
246
|
+
reader.readAsDataURL(blob)
|
|
247
|
+
})
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* 注入 CSS @font-face 样式
|
|
252
|
+
*/
|
|
253
|
+
function injectFontFaceStyles(
|
|
254
|
+
fonts: ILoadedEmbedFont[],
|
|
255
|
+
container?: HTMLElement
|
|
256
|
+
): void {
|
|
257
|
+
const styleEl = document.createElement('style')
|
|
258
|
+
styleEl.setAttribute('data-docx-fonts', 'true')
|
|
259
|
+
|
|
260
|
+
const cssRules = fonts.map(font => buildFontFaceRule(font)).join('\n')
|
|
261
|
+
styleEl.textContent = cssRules
|
|
262
|
+
|
|
263
|
+
const target = container || document.head
|
|
264
|
+
|
|
265
|
+
// 移除旧的字体样式
|
|
266
|
+
const oldStyle = target.querySelector('style[data-docx-fonts]')
|
|
267
|
+
if (oldStyle) {
|
|
268
|
+
oldStyle.remove()
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
target.appendChild(styleEl)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* 构建单个 @font-face 规则
|
|
276
|
+
*/
|
|
277
|
+
function buildFontFaceRule(font: ILoadedEmbedFont): string {
|
|
278
|
+
const fontWeight = getFontWeight(font.type)
|
|
279
|
+
const fontStyle = getFontStyle(font.type)
|
|
280
|
+
const format = getFontFormatString(font.format)
|
|
281
|
+
|
|
282
|
+
return `@font-face {
|
|
283
|
+
font-family: "${escapeFontName(font.fontName)}";
|
|
284
|
+
src: url("${font.dataUrl}") format("${format}");
|
|
285
|
+
font-weight: ${fontWeight};
|
|
286
|
+
font-style: ${fontStyle};
|
|
287
|
+
font-display: swap;
|
|
288
|
+
}`
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* 获取字体粗细
|
|
293
|
+
*/
|
|
294
|
+
function getFontWeight(type: TEmbedFontType): string {
|
|
295
|
+
switch (type) {
|
|
296
|
+
case 'bold':
|
|
297
|
+
case 'boldItalic':
|
|
298
|
+
return '700'
|
|
299
|
+
default:
|
|
300
|
+
return '400'
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* 获取字体样式
|
|
306
|
+
*/
|
|
307
|
+
function getFontStyle(type: TEmbedFontType): string {
|
|
308
|
+
switch (type) {
|
|
309
|
+
case 'italic':
|
|
310
|
+
case 'boldItalic':
|
|
311
|
+
return 'italic'
|
|
312
|
+
default:
|
|
313
|
+
return 'normal'
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* 获取 CSS font format 字符串
|
|
319
|
+
*/
|
|
320
|
+
function getFontFormatString(format: string): string {
|
|
321
|
+
switch (format) {
|
|
322
|
+
case 'opentype':
|
|
323
|
+
return 'opentype'
|
|
324
|
+
case 'truetype':
|
|
325
|
+
return 'truetype'
|
|
326
|
+
case 'embedded-opentype':
|
|
327
|
+
return 'embedded-opentype'
|
|
328
|
+
default:
|
|
329
|
+
return 'truetype'
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* 转义字体名称中的特殊字符
|
|
335
|
+
*/
|
|
336
|
+
function escapeFontName(name: string): string {
|
|
337
|
+
return name.replace(/"/g, '\\"')
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* 清理已注入的字体样式
|
|
342
|
+
* @param container 样式容器(默认为 document.head)
|
|
343
|
+
*/
|
|
344
|
+
export function cleanupFontStyles(container?: HTMLElement): void {
|
|
345
|
+
const target = container || document.head
|
|
346
|
+
const oldStyle = target.querySelector('style[data-docx-fonts]')
|
|
347
|
+
if (oldStyle) {
|
|
348
|
+
oldStyle.remove()
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* 解析 fontTable.xml.rels 文件获取字体关系
|
|
354
|
+
*/
|
|
355
|
+
export function parseFontRelationships(xmlContent: string): IRelationship[] {
|
|
356
|
+
const parser = new DOMParser()
|
|
357
|
+
const doc = parser.parseFromString(xmlContent, 'application/xml')
|
|
358
|
+
const root = doc.documentElement
|
|
359
|
+
|
|
360
|
+
const relationships: IRelationship[] = []
|
|
361
|
+
const relElements = root.getElementsByTagName('Relationship')
|
|
362
|
+
|
|
363
|
+
for (let i = 0; i < relElements.length; i++) {
|
|
364
|
+
const el = relElements[i]
|
|
365
|
+
const type = el.getAttribute('Type') || ''
|
|
366
|
+
|
|
367
|
+
// 只处理字体类型的关系
|
|
368
|
+
if (type === RELATIONSHIP_TYPES.FONT) {
|
|
369
|
+
relationships.push({
|
|
370
|
+
id: el.getAttribute('Id') || '',
|
|
371
|
+
type,
|
|
372
|
+
target: el.getAttribute('Target') || '',
|
|
373
|
+
targetMode: el.getAttribute('TargetMode') || undefined,
|
|
374
|
+
})
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return relationships
|
|
379
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 字体表解析器
|
|
3
|
+
* 解析 word/fontTable.xml,提取字体声明、嵌入字体引用和字体替换映射
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { xmlParser, parseXmlString } from '../parser/xml-parser'
|
|
7
|
+
import {
|
|
8
|
+
IFontTable,
|
|
9
|
+
IFontDeclaration,
|
|
10
|
+
IEmbedFontRef,
|
|
11
|
+
IFontSignature,
|
|
12
|
+
TEmbedFontType,
|
|
13
|
+
} from '../types'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 嵌入字体元素名到类型的映射
|
|
17
|
+
*/
|
|
18
|
+
const EMBED_FONT_TYPE_MAP: Record<string, TEmbedFontType> = {
|
|
19
|
+
embedRegular: 'regular',
|
|
20
|
+
embedBold: 'bold',
|
|
21
|
+
embedItalic: 'italic',
|
|
22
|
+
embedBoldItalic: 'boldItalic',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 解析字体表 XML
|
|
27
|
+
* @param xmlContent fontTable.xml 的内容
|
|
28
|
+
* @returns 字体表对象
|
|
29
|
+
*/
|
|
30
|
+
export function parseFontTable(xmlContent: string): IFontTable {
|
|
31
|
+
const doc = parseXmlString(xmlContent)
|
|
32
|
+
const root = doc.documentElement
|
|
33
|
+
|
|
34
|
+
const fonts = parseFonts(root)
|
|
35
|
+
const fontMap = new Map(fonts.map(f => [f.name, f]))
|
|
36
|
+
const substitutionMap = buildSubstitutionMap(fonts)
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
fonts,
|
|
40
|
+
fontMap,
|
|
41
|
+
substitutionMap,
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 解析所有字体声明
|
|
47
|
+
*/
|
|
48
|
+
function parseFonts(root: Element): IFontDeclaration[] {
|
|
49
|
+
return xmlParser.elements(root, 'font').map(el => parseFont(el))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 解析单个字体声明
|
|
54
|
+
*/
|
|
55
|
+
function parseFont(elem: Element): IFontDeclaration {
|
|
56
|
+
const result: IFontDeclaration = {
|
|
57
|
+
name: xmlParser.attr(elem, 'name') || '',
|
|
58
|
+
embedFontRefs: [],
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for (const el of xmlParser.elements(elem)) {
|
|
62
|
+
switch (el.localName) {
|
|
63
|
+
case 'family':
|
|
64
|
+
result.family = xmlParser.attr(el, 'val')
|
|
65
|
+
break
|
|
66
|
+
|
|
67
|
+
case 'altName':
|
|
68
|
+
result.altName = xmlParser.attr(el, 'val')
|
|
69
|
+
break
|
|
70
|
+
|
|
71
|
+
case 'charset':
|
|
72
|
+
result.charset = xmlParser.attr(el, 'val')
|
|
73
|
+
break
|
|
74
|
+
|
|
75
|
+
case 'panose1':
|
|
76
|
+
result.panose1 = xmlParser.attr(el, 'val')
|
|
77
|
+
break
|
|
78
|
+
|
|
79
|
+
case 'sig':
|
|
80
|
+
result.sig = parseFontSignature(el)
|
|
81
|
+
break
|
|
82
|
+
|
|
83
|
+
case 'embedRegular':
|
|
84
|
+
case 'embedBold':
|
|
85
|
+
case 'embedItalic':
|
|
86
|
+
case 'embedBoldItalic':
|
|
87
|
+
const embedRef = parseEmbedFontRef(el)
|
|
88
|
+
if (embedRef) {
|
|
89
|
+
result.embedFontRefs.push(embedRef)
|
|
90
|
+
}
|
|
91
|
+
break
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return result
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 解析字体签名
|
|
100
|
+
*/
|
|
101
|
+
function parseFontSignature(elem: Element): IFontSignature {
|
|
102
|
+
return {
|
|
103
|
+
usb0: xmlParser.attr(elem, 'usb0'),
|
|
104
|
+
usb1: xmlParser.attr(elem, 'usb1'),
|
|
105
|
+
usb2: xmlParser.attr(elem, 'usb2'),
|
|
106
|
+
usb3: xmlParser.attr(elem, 'usb3'),
|
|
107
|
+
csb0: xmlParser.attr(elem, 'csb0'),
|
|
108
|
+
csb1: xmlParser.attr(elem, 'csb1'),
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 解析嵌入字体引用
|
|
114
|
+
*/
|
|
115
|
+
function parseEmbedFontRef(elem: Element): IEmbedFontRef | null {
|
|
116
|
+
const id = xmlParser.attr(elem, 'id')
|
|
117
|
+
if (!id) return null
|
|
118
|
+
|
|
119
|
+
const type = EMBED_FONT_TYPE_MAP[elem.localName]
|
|
120
|
+
if (!type) return null
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
id,
|
|
124
|
+
key: xmlParser.attr(elem, 'fontKey'),
|
|
125
|
+
subsetted: xmlParser.boolAttr(elem, 'subsetted'),
|
|
126
|
+
type,
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 构建字体替换映射
|
|
132
|
+
* 从字体声明中提取 altName,建立原字体名到替代字体名的映射
|
|
133
|
+
*/
|
|
134
|
+
function buildSubstitutionMap(fonts: IFontDeclaration[]): Map<string, string> {
|
|
135
|
+
const map = new Map<string, string>()
|
|
136
|
+
|
|
137
|
+
for (const font of fonts) {
|
|
138
|
+
if (font.altName) {
|
|
139
|
+
map.set(font.name, font.altName)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return map
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* 获取字体的替代字体名
|
|
148
|
+
* @param fontTable 字体表
|
|
149
|
+
* @param fontName 原字体名
|
|
150
|
+
* @returns 替代字体名,如果没有替代则返回原字体名
|
|
151
|
+
*/
|
|
152
|
+
export function getSubstituteFontName(fontTable: IFontTable, fontName: string): string {
|
|
153
|
+
return fontTable.substitutionMap.get(fontName) || fontName
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* 获取字体声明
|
|
158
|
+
* @param fontTable 字体表
|
|
159
|
+
* @param fontName 字体名
|
|
160
|
+
* @returns 字体声明,如果不存在则返回 undefined
|
|
161
|
+
*/
|
|
162
|
+
export function getFontDeclaration(fontTable: IFontTable, fontName: string): IFontDeclaration | undefined {
|
|
163
|
+
return fontTable.fontMap.get(fontName)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* 检查字体是否有嵌入字体
|
|
168
|
+
* @param fontTable 字体表
|
|
169
|
+
* @param fontName 字体名
|
|
170
|
+
* @returns 是否有嵌入字体
|
|
171
|
+
*/
|
|
172
|
+
export function hasEmbeddedFont(fontTable: IFontTable, fontName: string): boolean {
|
|
173
|
+
const font = fontTable.fontMap.get(fontName)
|
|
174
|
+
return font ? font.embedFontRefs.length > 0 : false
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* 获取字体的嵌入字体引用
|
|
179
|
+
* @param fontTable 字体表
|
|
180
|
+
* @param fontName 字体名
|
|
181
|
+
* @param type 字体类型(可选,不指定则返回所有类型)
|
|
182
|
+
* @returns 嵌入字体引用数组
|
|
183
|
+
*/
|
|
184
|
+
export function getEmbedFontRefs(
|
|
185
|
+
fontTable: IFontTable,
|
|
186
|
+
fontName: string,
|
|
187
|
+
type?: TEmbedFontType
|
|
188
|
+
): IEmbedFontRef[] {
|
|
189
|
+
const font = fontTable.fontMap.get(fontName)
|
|
190
|
+
if (!font) return []
|
|
191
|
+
|
|
192
|
+
if (type) {
|
|
193
|
+
return font.embedFontRefs.filter(ref => ref.type === type)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return font.embedFontRefs
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* 生成 CSS font-family 字符串
|
|
201
|
+
* 根据字体声明生成包含替代字体的 font-family
|
|
202
|
+
* @param fontTable 字体表
|
|
203
|
+
* @param fontName 字体名
|
|
204
|
+
* @returns CSS font-family 字符串
|
|
205
|
+
*/
|
|
206
|
+
export function buildFontFamily(fontTable: IFontTable, fontName: string): string {
|
|
207
|
+
const fonts: string[] = []
|
|
208
|
+
const font = fontTable.fontMap.get(fontName)
|
|
209
|
+
|
|
210
|
+
// 添加原字体名
|
|
211
|
+
fonts.push(quoteFontName(fontName))
|
|
212
|
+
|
|
213
|
+
// 添加替代字体
|
|
214
|
+
if (font?.altName) {
|
|
215
|
+
fonts.push(quoteFontName(font.altName))
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// 根据字体家族添加通用字体
|
|
219
|
+
if (font?.family) {
|
|
220
|
+
const genericFont = getGenericFontFamily(font.family)
|
|
221
|
+
if (genericFont) {
|
|
222
|
+
fonts.push(genericFont)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return fonts.join(', ')
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* 为字体名添加引号(如果需要)
|
|
231
|
+
*/
|
|
232
|
+
function quoteFontName(name: string): string {
|
|
233
|
+
// 如果字体名包含空格或特殊字符,需要加引号
|
|
234
|
+
if (/[\s,'"()]/.test(name)) {
|
|
235
|
+
return `"${name.replace(/"/g, '\\"')}"`
|
|
236
|
+
}
|
|
237
|
+
return name
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* 根据 OOXML 字体家族获取 CSS 通用字体家族
|
|
242
|
+
*/
|
|
243
|
+
function getGenericFontFamily(family: string): string | null {
|
|
244
|
+
switch (family.toLowerCase()) {
|
|
245
|
+
case 'roman':
|
|
246
|
+
return 'serif'
|
|
247
|
+
case 'swiss':
|
|
248
|
+
return 'sans-serif'
|
|
249
|
+
case 'modern':
|
|
250
|
+
return 'monospace'
|
|
251
|
+
case 'script':
|
|
252
|
+
return 'cursive'
|
|
253
|
+
case 'decorative':
|
|
254
|
+
return 'fantasy'
|
|
255
|
+
default:
|
|
256
|
+
return null
|
|
257
|
+
}
|
|
258
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 字体表模块
|
|
3
|
+
* 提供字体表解析、嵌入字体加载和字体替换映射功能
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// 导出解析器
|
|
7
|
+
export {
|
|
8
|
+
parseFontTable,
|
|
9
|
+
getSubstituteFontName,
|
|
10
|
+
getFontDeclaration,
|
|
11
|
+
hasEmbeddedFont,
|
|
12
|
+
getEmbedFontRefs,
|
|
13
|
+
buildFontFamily,
|
|
14
|
+
} from './font-parser'
|
|
15
|
+
|
|
16
|
+
// 导出加载器
|
|
17
|
+
export {
|
|
18
|
+
loadEmbeddedFonts,
|
|
19
|
+
cleanupFontStyles,
|
|
20
|
+
parseFontRelationships,
|
|
21
|
+
type IFontLoaderOptions,
|
|
22
|
+
} from './font-loader'
|