@file-viewer/renderer-ofd 2.1.23 → 2.1.25

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.en.md CHANGED
@@ -35,6 +35,7 @@ const options = {
35
35
  ## Features
36
36
 
37
37
  - Preview `.ofd` documents in the browser.
38
+ - Preview electronic seal appearance (SES seal images and nested OFD seals) using `StampAnnot` boundaries; preview does not perform cryptographic verification.
38
39
  - Lazy-load the OFD parser and renderer at the format boundary.
39
40
  - Ship the vendor files inside the npm package for offline and intranet deployments.
40
41
  - Support unified zoom, print, HTML export, and lifecycle context integration.
package/README.md CHANGED
@@ -35,6 +35,7 @@ const options = {
35
35
  ## 能力
36
36
 
37
37
  - 支持 `.ofd` 国产版式文件。
38
+ - 支持电子签章外观预览(SES 印章图片与嵌套 OFD 印章),按 `StampAnnot` 边界叠加显示;预览不做国密验签。
38
39
  - 按需动态导入 OFD 解析/渲染代码,不污染首屏。
39
40
  - vendor 文件随 npm 包发布,支持企业内网和纯离线部署。
40
41
  - 支持统一缩放、打印、HTML 导出和生命周期上下文。
package/dist/ofd.js CHANGED
@@ -10,10 +10,12 @@ const ofdStyle = `
10
10
  .ofd-state[hidden]{display:none!important}
11
11
  .ofd-state.error{color:#b42318}
12
12
  .ofd-page-frame{position:relative;display:block;margin:0 auto 20px;overflow:visible}
13
- .ofd-page{display:block;margin-left:auto!important;margin-right:auto!important;box-shadow:0 10px 26px rgba(15,23,42,.12);transition:transform .16s ease}
14
- .file-viewer[data-viewer-theme='dark'] .ofd-viewer{background:#172033;color:#e5eef8}
15
- .file-viewer[data-viewer-theme='dark'] .ofd-state{background:rgba(15,23,42,.9);color:#cbd5e1}
16
- @media (prefers-color-scheme:dark){.file-viewer[data-viewer-theme='system'] .ofd-viewer{background:#172033;color:#e5eef8}.file-viewer[data-viewer-theme='system'] .ofd-state{background:rgba(15,23,42,.9);color:#cbd5e1}}
13
+ .ofd-page{display:block;margin-left:auto!important;margin-right:auto!important;background:#fff!important;color:#111827!important;color-scheme:only light;forced-color-adjust:none;isolation:isolate;box-shadow:0 10px 26px rgba(15,23,42,.12);transition:transform .16s ease}
14
+ .ofd-page,.ofd-page *{color-scheme:only light;forced-color-adjust:none}
15
+ .ofd-page svg,.ofd-page canvas,.ofd-page img{filter:none!important;mix-blend-mode:normal!important}
16
+ [data-viewer-theme='dark'] .ofd-viewer{background:#172033;color:#e5eef8}
17
+ [data-viewer-theme='dark'] .ofd-state{background:rgba(15,23,42,.9);color:#cbd5e1}
18
+ @media (prefers-color-scheme:dark){[data-viewer-theme='system'] .ofd-viewer{background:#172033;color:#e5eef8}[data-viewer-theme='system'] .ofd-state{background:rgba(15,23,42,.9);color:#cbd5e1}}
17
19
  @media print{.ofd-viewer{background:#fff!important}.ofd-stage{padding:0!important;overflow:visible!important}.ofd-page-frame{break-after:page;page-break-after:always;margin:0 auto!important}.ofd-page-frame:last-child{break-after:auto;page-break-after:auto}.ofd-page{box-shadow:none!important;transition:none!important}}
18
20
  `;
19
21
  const loadOfdModule = (() => {
@@ -73,6 +75,10 @@ const appendPages = (documentRef, target, pages) => {
73
75
  const frame = documentRef.createElement('div');
74
76
  frame.className = 'ofd-page-frame';
75
77
  page.classList.add('ofd-page');
78
+ page.dataset.viewerThemeBoundary = 'light';
79
+ page.style.backgroundColor = '#fff';
80
+ page.style.color = '#111827';
81
+ page.style.colorScheme = 'only light';
76
82
  frame.appendChild(page);
77
83
  fragment.appendChild(frame);
78
84
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@file-viewer/renderer-ofd",
3
- "version": "2.1.23",
3
+ "version": "2.1.25",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Standalone OFD renderer plugin for Flyfish File Viewer powered by DLTech21/ofd.js.",
@@ -53,15 +53,19 @@
53
53
  "LICENSE"
54
54
  ],
55
55
  "dependencies": {
56
- "@file-viewer/core": "2.1.23",
56
+ "@file-viewer/core": "2.1.25",
57
57
  "jszip": "^3.10.1"
58
58
  },
59
59
  "devDependencies": {
60
+ "linkedom": "^0.18.12",
60
61
  "typescript": "^6.0.3"
61
62
  },
62
63
  "license": "Apache-2.0",
63
64
  "scripts": {
64
65
  "build": "tsc -b tsconfig.json",
65
- "type-check": "tsc -b tsconfig.json"
66
+ "type-check": "tsc -b tsconfig.json",
67
+ "verify:github-94": "node scripts/verify-github-94.mjs",
68
+ "verify:pageblock-render": "node scripts/verify-pageblock-render.mjs",
69
+ "verify:render-fidelity": "node scripts/verify-render-fidelity.mjs"
66
70
  }
67
71
  }
@@ -21,6 +21,8 @@
21
21
  import JsZip from "jszip";
22
22
  import {parseStBox, getExtensionByPath, replaceFirstSlash} from "./ofd_util.js";
23
23
  import {Jbig2Image} from '../jbig2/jbig2.js';
24
+ import {pipeline} from "./pipeline.js";
25
+ import {parseSesSignature} from "./ses_signature_parser.js";
24
26
 
25
27
  const ensureBrowserGlobal = () => {
26
28
  // DLTech21/ofd.js 的一部分遗留逻辑读取 Node 风格 global。
@@ -41,7 +43,7 @@ const appendChildValue = function (target, key, value) {
41
43
  target[key] = value;
42
44
  }
43
45
 
44
- const parseXmlElement = function (element, options) {
46
+ const parseXmlElement = function (element, options, order) {
45
47
  const attrPrefix = options.attributeNamePrefix ?? '@_';
46
48
  const result = {};
47
49
  if (options.ignoreAttributes !== true) {
@@ -53,7 +55,16 @@ const parseXmlElement = function (element, options) {
53
55
  const textParts = [];
54
56
  for (const child of Array.from(element.childNodes || [])) {
55
57
  if (child.nodeType === 1) {
56
- appendChildValue(result, child.nodeName, parseXmlElement(child, options));
58
+ // 渲染层按 pfIndex 做绘制顺序(CSS z-index),这里用前序遍历给每个元素编号,
59
+ // 还原 Image/Path/Text/PageBlock 在原始文档里交错出现的先后关系。
60
+ // 若不记录,解析结果会把同名标签分组成数组,渲染时只能按“类型”整批绘制
61
+ // (所有图片一起、所有路径一起……),导致本该压在图片下方的装饰路径盖住图片。
62
+ const pfIndex = order ? order.next++ : undefined;
63
+ const parsedChild = parseXmlElement(child, options, order);
64
+ if (parsedChild && typeof parsedChild === 'object' && pfIndex !== undefined) {
65
+ parsedChild.pfIndex = pfIndex;
66
+ }
67
+ appendChildValue(result, child.nodeName, parsedChild);
57
68
  continue;
58
69
  }
59
70
  if (child.nodeType === 3 || child.nodeType === 4) {
@@ -92,7 +103,7 @@ const parseXmlToJson = function (xmlData, options = {}) {
92
103
  return {};
93
104
  }
94
105
  return {
95
- [root.nodeName]: parseXmlElement(root, options)
106
+ [root.nodeName]: parseXmlElement(root, options, { next: 0 })
96
107
  };
97
108
  }
98
109
 
@@ -257,6 +268,30 @@ export const parseSingleDoc = async function ([zip, array]) {
257
268
  return docs;
258
269
  }
259
270
 
271
+ const uint8ArrayToBase64 = function (bytes) {
272
+ const chunkSize = 0x8000;
273
+ let binary = '';
274
+ for (let i = 0; i < bytes.length; i += chunkSize) {
275
+ const chunk = bytes.subarray(i, i + chunkSize);
276
+ binary += String.fromCharCode.apply(null, chunk);
277
+ }
278
+ return btoa(binary);
279
+ }
280
+
281
+ const sealImageMime = function (type) {
282
+ const normalized = String(type || '').toLowerCase();
283
+ if (normalized === 'jpg' || normalized === 'jpeg') {
284
+ return 'image/jpeg';
285
+ }
286
+ if (normalized === 'gif') {
287
+ return 'image/gif';
288
+ }
289
+ if (normalized === 'bmp') {
290
+ return 'image/bmp';
291
+ }
292
+ return 'image/png';
293
+ }
294
+
260
295
  export const doGetDocRoot = async function (zip, docbody) {
261
296
  let docRoot = docbody['ofd:DocRoot'];
262
297
  docRoot = normalizeZipPath(replaceFirstSlash(docRoot));
@@ -267,18 +302,33 @@ export const doGetDocRoot = async function (zip, docbody) {
267
302
  for (const stamp of stampAnnot) {
268
303
  if (stamp.sealObj && Object.keys(stamp.sealObj).length > 0) {
269
304
  if (stamp.sealObj.type === 'ofd') {
270
- const stampObjs = await getSealDocumentObj(stamp);
271
- for (let stampObj of stampObjs) {
272
- stamp.stampAnnot.boundary = parseStBox(stamp.stampAnnot['@_Boundary']);
273
- //console.log(stamp.stampAnnot.boundary)
274
- stamp.stampAnnot.pageRef = stamp.stampAnnot['@_PageRef'];
275
- if (!stampAnnotArray[stamp.stampAnnot['@_PageRef']]) {
276
- stampAnnotArray[stamp.stampAnnot['@_PageRef']] = [];
305
+ try {
306
+ const stampObjs = await getSealDocumentObj(stamp);
307
+ let annotArray = [];
308
+ annotArray = annotArray.concat(stamp.stampAnnot);
309
+ for (const annot of annotArray) {
310
+ if (!annot) {
311
+ continue;
312
+ }
313
+ annot.boundary = parseStBox(annot['@_Boundary']);
314
+ annot.pageRef = annot['@_PageRef'];
315
+ for (let stampObj of stampObjs) {
316
+ if (!stampAnnotArray[annot['@_PageRef']]) {
317
+ stampAnnotArray[annot['@_PageRef']] = [];
318
+ }
319
+ stampAnnotArray[annot['@_PageRef']].push({
320
+ type: 'ofd',
321
+ obj: stampObj,
322
+ stamp: {...stamp, stampAnnot: annot},
323
+ });
324
+ }
277
325
  }
278
- stampAnnotArray[stamp.stampAnnot['@_PageRef']].push({type: 'ofd', obj: stampObj, stamp});
326
+ } catch (e) {
327
+ console.warn('[ofd] nested OFD seal parse failed', e);
279
328
  }
280
- } else if (stamp.sealObj.type === 'png') {
281
- let img = 'data:image/png;base64,' + btoa(String.fromCharCode.apply(null, stamp.sealObj.ofdArray));
329
+ } else if (stamp.sealObj.ofdArray?.length) {
330
+ const mime = sealImageMime(stamp.sealObj.type);
331
+ let img = `data:${mime};base64,` + uint8ArrayToBase64(stamp.sealObj.ofdArray);
282
332
  let stampArray = [];
283
333
  stampArray = stampArray.concat(stamp.stampAnnot);
284
334
  for (const annot of stampArray) {
@@ -523,16 +573,93 @@ const parsePage = async function (zip, obj, doc) {
523
573
  }
524
574
 
525
575
  const getSignature = async function (zip, signatures, doc) {
526
- // 签章解析依赖 ASN.1、国密算法和证书校验链路。在线预览先跳过签章校验,
527
- // 以免个别签章解析失败阻断正文页渲染;业务如需验签可在服务端独立处理。
528
- void zip;
529
- void signatures;
530
- void doc;
531
- return [];
576
+ // 预览只解析签章外观(SES 印章图片 / 嵌套 OFD),不做国密/证书验签,
577
+ // 单个签章失败不影响正文页渲染。
578
+ let stampAnnot = [];
579
+ if (!signatures) {
580
+ return stampAnnot;
581
+ }
582
+ try {
583
+ let signaturesPath = normalizeZipPath(replaceFirstSlash(signatures));
584
+ if (!startsWithZipRoot(signaturesPath, doc)) {
585
+ signaturesPath = joinZipPath(doc, signaturesPath);
586
+ }
587
+ if (!getZipEntry(zip, signaturesPath)) {
588
+ return stampAnnot;
589
+ }
590
+ let data = await getJsonFromXmlContent(zip, signaturesPath);
591
+ let signature = data['json']['ofd:Signatures']['ofd:Signature'];
592
+ let signatureArray = [];
593
+ signatureArray = signatureArray.concat(signature);
594
+ for (const sign of signatureArray) {
595
+ if (!sign) {
596
+ continue;
597
+ }
598
+ try {
599
+ let signatureLoc = normalizeZipPath(replaceFirstSlash(sign['@_BaseLoc']));
600
+ let signatureID = sign['@_ID'];
601
+ if (signatureLoc && signatureLoc.indexOf('Signs') === -1 && !startsWithZipRoot(signatureLoc, doc)) {
602
+ signatureLoc = joinZipPath('Signs', signatureLoc);
603
+ }
604
+ if (!startsWithZipRoot(signatureLoc, doc)) {
605
+ signatureLoc = joinZipPath(doc, signatureLoc);
606
+ }
607
+ const parsed = await getSignatureData(zip, signatureLoc, signatureID);
608
+ if (parsed) {
609
+ stampAnnot.push(parsed);
610
+ }
611
+ } catch (e) {
612
+ console.warn('[ofd] signature entry parse failed', sign?.['@_ID'], e);
613
+ }
614
+ }
615
+ } catch (e) {
616
+ console.warn('[ofd] signatures.xml parse failed', e);
617
+ }
618
+ return stampAnnot;
619
+ }
620
+
621
+ const getSignatureData = async function (zip, signature, signatureID) {
622
+ const data = await getJsonFromXmlContent(zip, signature);
623
+ let signedValue = data['json']['ofd:Signature']['ofd:SignedValue'];
624
+ if (signedValue == null) {
625
+ return null;
626
+ }
627
+ signedValue = normalizeZipPath(replaceFirstSlash(String(signedValue)));
628
+ if (!getZipEntry(zip, signedValue)) {
629
+ signedValue = joinZipPath(signature.substring(0, signature.lastIndexOf('/')), signedValue);
630
+ }
631
+ if (!getZipEntry(zip, signedValue)) {
632
+ console.warn('[ofd] SignedValue not found', signedValue);
633
+ return null;
634
+ }
635
+ let sealObj = await parseSesSignature(zip, signedValue, getZipEntry);
636
+ if (!sealObj || !Object.keys(sealObj).length) {
637
+ return null;
638
+ }
639
+ const signedInfoNode = data['json']['ofd:Signature']['ofd:SignedInfo'] || {};
640
+ return {
641
+ 'stampAnnot': signedInfoNode['ofd:StampAnnot'],
642
+ 'sealObj': sealObj,
643
+ 'signedInfo': {
644
+ 'signatureID': signatureID,
645
+ 'VerifyRet': sealObj.verifyRet,
646
+ 'Provider': signedInfoNode['ofd:Provider'],
647
+ 'SignatureMethod': signedInfoNode['ofd:SignatureMethod'],
648
+ 'SignatureDateTime': signedInfoNode['ofd:SignatureDateTime'],
649
+ },
650
+ };
532
651
  }
533
652
 
534
- const getSealDocumentObj = function () {
535
- return Promise.resolve([]);
653
+ const getSealDocumentObj = function (stampAnnot) {
654
+ return new Promise((resolve, reject) => {
655
+ pipeline.call(this, async () => await unzipOfd(stampAnnot.sealObj.ofdArray), getDocRoots, parseSingleDoc)
656
+ .then(res => {
657
+ resolve(res)
658
+ })
659
+ .catch(res => {
660
+ reject(res);
661
+ });
662
+ });
536
663
  }
537
664
 
538
665
  const getJsonFromXmlContent = async function (zip, xmlName) {
@@ -125,6 +125,43 @@ export const calPageBoxScale = function (document, page) {
125
125
  return box;
126
126
  }
127
127
 
128
+ // OFD 的 PathObject/TextObject/Layer 通常通过 DrawParam 引用共享颜色/线宽,
129
+ // 而不是在对象上直接写 ofd:FillColor / ofd:StrokeColor;DrawParam 之间还能通过
130
+ // Relative 相互继承。三处(Layer、PathObject、TextObject)都需要同样的解析规则,
131
+ // 否则引用 DrawParam 取色的对象会因为拿不到颜色而被当成透明,完全不可见。
132
+ const resolveDrawParamStyle = function (drawParamResObj, drawParamId, base) {
133
+ let fillColor = base?.fillColor ?? null;
134
+ let strokeColor = base?.strokeColor ?? null;
135
+ let lineWith = base?.lineWith;
136
+ if (!drawParamId || !drawParamResObj || !drawParamResObj[drawParamId]) {
137
+ return { fillColor, strokeColor, lineWith };
138
+ }
139
+ let drawParam = drawParamId;
140
+ if (drawParamResObj[drawParam]['relative']) {
141
+ drawParam = drawParamResObj[drawParam]['relative'];
142
+ if (drawParamResObj[drawParam] && drawParamResObj[drawParam]['FillColor']) {
143
+ fillColor = parseColor(drawParamResObj[drawParam]['FillColor']);
144
+ }
145
+ if (drawParamResObj[drawParam] && drawParamResObj[drawParam]['StrokeColor']) {
146
+ strokeColor = parseColor(drawParamResObj[drawParam]['StrokeColor']);
147
+ }
148
+ if (drawParamResObj[drawParam] && drawParamResObj[drawParam]['LineWidth']) {
149
+ lineWith = converterDpi(drawParamResObj[drawParam]['LineWidth']);
150
+ }
151
+ drawParam = drawParamId;
152
+ }
153
+ if (drawParamResObj[drawParam]['FillColor']) {
154
+ fillColor = parseColor(drawParamResObj[drawParam]['FillColor']);
155
+ }
156
+ if (drawParamResObj[drawParam]['StrokeColor']) {
157
+ strokeColor = parseColor(drawParamResObj[drawParam]['StrokeColor']);
158
+ }
159
+ if (drawParamResObj[drawParam]['LineWidth']) {
160
+ lineWith = converterDpi(drawParamResObj[drawParam]['LineWidth']);
161
+ }
162
+ return { fillColor, strokeColor, lineWith };
163
+ }
164
+
128
165
  // 根据模板来渲染层节点
129
166
  const renderLayerFromTemplate = function (tpls, template, pageDiv, fontResObj, drawParamResObj, multiMediaResObj) {
130
167
  let array = [];
@@ -208,9 +245,9 @@ const renderSealPage = function (pageDiv, pages, tpls, isStampAnnot, stampAnnot,
208
245
  let divBoundary = converterBox(stampAnnotBoundary);
209
246
  let div = document.createElement('div');
210
247
  div.setAttribute("name","seal_img_div");
211
- div.setAttribute('style', `cursor: pointer; position:relative; left: ${divBoundary.x}px; top: ${divBoundary.y}px; width: ${divBoundary.w}px; height: ${divBoundary.h}px`)
212
- div.setAttribute('data-ses-signature', `${JSON.stringify(SES_Signature)}`);
213
- div.setAttribute('data-signed-info', `${JSON.stringify(signedInfo)}`);
248
+ div.setAttribute('style', `cursor: pointer; overflow: hidden; position:absolute; left: ${divBoundary.x}px; top: ${divBoundary.y}px; width: ${divBoundary.w}px; height: ${divBoundary.h}px`)
249
+ div.setAttribute('data-ses-signature', `${JSON.stringify(SES_Signature || {})}`);
250
+ div.setAttribute('data-signed-info', `${JSON.stringify(signedInfo || {})}`);
214
251
  const template = page[pageId]['json']['ofd:Template'];
215
252
  if (template) {
216
253
  const layers = tpls[template['@_TemplateID']]['json']['ofd:Content']['ofd:Layer'];
@@ -235,33 +272,14 @@ const renderSealPage = function (pageDiv, pages, tpls, isStampAnnot, stampAnnot,
235
272
  }
236
273
 
237
274
  const renderLayer = function (pageDiv, fontResObj, drawParamResObj, multiMediaResObj, layer, isStampAnnot) {
238
- let fillColor = null;
239
- let strokeColor = null;
240
- let lineWith = converterDpi(0.353);
241
- let drawParam = layer?.['@_DrawParam'];
242
- if (drawParam && Object.keys(drawParamResObj).length > 0 && drawParamResObj[drawParam]) {
243
- if (drawParamResObj[drawParam]['relative']) {
244
- drawParam = drawParamResObj[drawParam]['relative'];
245
- if (drawParamResObj[drawParam]['FillColor']) {
246
- fillColor = parseColor(drawParamResObj[drawParam]['FillColor']);
247
- }
248
- if (drawParamResObj[drawParam]['StrokeColor']) {
249
- strokeColor = parseColor(drawParamResObj[drawParam]['StrokeColor']);
250
- }
251
- if (drawParamResObj[drawParam]['LineWidth']) {
252
- lineWith = converterDpi(drawParamResObj[drawParam]['LineWidth']);
253
- }
254
- }
255
- if (drawParamResObj[drawParam]['FillColor']) {
256
- fillColor = parseColor(drawParamResObj[drawParam]['FillColor']);
257
- }
258
- if (drawParamResObj[drawParam]['StrokeColor']) {
259
- strokeColor = parseColor(drawParamResObj[drawParam]['StrokeColor']);
260
- }
261
- if (drawParamResObj[drawParam]['LineWidth']) {
262
- lineWith = converterDpi(drawParamResObj[drawParam]['LineWidth']);
263
- }
264
- }
275
+ const layerStyle = resolveDrawParamStyle(drawParamResObj, layer?.['@_DrawParam'], {
276
+ fillColor: null,
277
+ strokeColor: null,
278
+ lineWith: converterDpi(0.353),
279
+ });
280
+ let fillColor = layerStyle.fillColor;
281
+ let strokeColor = layerStyle.strokeColor;
282
+ let lineWith = layerStyle.lineWith;
265
283
  const imageObjects = layer?.['ofd:ImageObject'];
266
284
  let imageObjectArray = [];
267
285
  imageObjectArray = imageObjectArray.concat(imageObjects);
@@ -285,15 +303,55 @@ const renderLayer = function (pageDiv, fontResObj, drawParamResObj, multiMediaRe
285
303
  textObjectArray = textObjectArray.concat(textObjects);
286
304
  for (const textObject of textObjectArray) {
287
305
  if (textObject) {
288
- let svg = renderTextObject(fontResObj, textObject, fillColor, strokeColor);
306
+ let svg = renderTextObject(drawParamResObj, fontResObj, textObject, fillColor, strokeColor);
289
307
  pageDiv.appendChild(svg);
290
308
  }
291
309
  }
310
+ const pageBlocks = layer?.['ofd:PageBlock'];
311
+ let pageBlockArray = [];
312
+ pageBlockArray = pageBlockArray.concat(pageBlocks);
313
+ for (const pageBlock of pageBlockArray) {
314
+ if (pageBlock) {
315
+ // 部分 OFD(如 OFD R&W 导出的版式)把正文对象包在 PageBlock 里,需递归渲染。
316
+ renderLayer(pageDiv, fontResObj, drawParamResObj, multiMediaResObj, pageBlock, isStampAnnot);
317
+ }
318
+ }
319
+ }
320
+
321
+ // 部分 OFD 导出工具(如 OFD R&W)会让同一图层内所有图像共享同一个整页 Boundary,
322
+ // 图像的真实位置和尺寸完全由 CTM 决定;此时必须优先按 CTM 计算矩形,否则所有图像会被拉伸铺满整页并互相重叠。
323
+ const parseImageCtm = function (ctm) {
324
+ if (!ctm) {
325
+ return null;
326
+ }
327
+ const values = String(ctm).trim().split(/\s+/).map(Number);
328
+ return values.length === 6 && values.every(value => Number.isFinite(value)) ? values : null;
329
+ }
330
+
331
+ const resolveImageGeometry = function (imageObject) {
332
+ const boundary = converterBox(parseStBox(imageObject['@_Boundary']));
333
+ const ctmValues = parseImageCtm(imageObject['@_CTM']);
334
+ if (!ctmValues) {
335
+ return { boundary, matrix: null };
336
+ }
337
+ const [a, b, c, d, e, f] = ctmValues;
338
+ const isAxisAligned = Math.abs(b) < 1e-6 && Math.abs(c) < 1e-6;
339
+ if (isAxisAligned) {
340
+ // CTM 把单位正方形映射到页面坐标系,a/d 为宽高、e/f 为左上角,单位与 Boundary 一致(mm)。
341
+ const ctmBoundary = converterBox({
342
+ x: Math.min(e, e + a),
343
+ y: Math.min(f, f + d),
344
+ w: Math.abs(a),
345
+ h: Math.abs(d),
346
+ });
347
+ return { boundary: ctmBoundary, matrix: null };
348
+ }
349
+ // 存在旋转/斜切时,退化为整页 Boundary 无法给出正确外框,改用矩阵直接变换单位方块。
350
+ return { boundary, matrix: ctmValues };
292
351
  }
293
352
 
294
353
  export const renderImageObject = function (pageWidth, pageHeight, multiMediaResObj, imageObject){
295
- let boundary = parseStBox(imageObject['@_Boundary']);
296
- boundary = converterBox(boundary);
354
+ const { boundary, matrix } = resolveImageGeometry(imageObject);
297
355
  const resId = imageObject['@_ResourceID'];
298
356
  const imageResource = multiMediaResObj ? multiMediaResObj[resId] : null;
299
357
  if (!imageResource || typeof imageResource !== 'object') {
@@ -304,11 +362,24 @@ export const renderImageObject = function (pageWidth, pageHeight, multiMediaResO
304
362
  const width = imageResource.width;
305
363
  const height = imageResource.height;
306
364
  return renderImageOnCanvas(img, width, height, boundary, imageObject['pfIndex']);
365
+ } else if (matrix) {
366
+ return renderImageWithMatrix(imageResource.img, matrix, imageObject['pfIndex']);
307
367
  } else {
308
368
  return renderImageOnDiv(pageWidth, pageHeight, imageResource.img, boundary, false, false, null, null, imageObject['pfIndex']);
309
369
  }
310
370
  }
311
371
 
372
+ const renderImageWithMatrix = function (imgSrc, ctmValues, oid) {
373
+ const [a, b, c, d, e, f] = ctmValues;
374
+ const unit = converterDpi(1);
375
+ let img = document.createElement('img');
376
+ img.src = imgSrc;
377
+ img.setAttribute('width', `${unit}`);
378
+ img.setAttribute('height', `${unit}`);
379
+ img.setAttribute('style', `position:absolute;left:0;top:0;transform-origin:0 0;transform:matrix(${a}, ${b}, ${c}, ${d}, ${converterDpi(e)}, ${converterDpi(f)});z-index:${oid}`);
380
+ return img;
381
+ }
382
+
312
383
  const renderMissingImageObject = function (boundary, oid){
313
384
  let div = document.createElement('div');
314
385
  div.setAttribute('style', `overflow: hidden; position: absolute; left: ${boundary.x}px; top: ${boundary.y}px; width: ${boundary.w}px; height: ${boundary.h}px;z-index: ${oid}`)
@@ -339,8 +410,8 @@ export const renderImageOnDiv = function (pageWidth, pageHeight, imgSrc, boundar
339
410
  if(isStampAnnot)
340
411
  {
341
412
  div.setAttribute("name","seal_img_div");
342
- div.setAttribute('data-ses-signature', `${JSON.stringify(SES_Signature)}`);
343
- div.setAttribute('data-signed-info', `${JSON.stringify(signedInfo)}`);
413
+ div.setAttribute('data-ses-signature', `${JSON.stringify(SES_Signature || {})}`);
414
+ div.setAttribute('data-signed-info', `${JSON.stringify(signedInfo || {})}`);
344
415
  }
345
416
  let img = document.createElement('img');
346
417
  img.src = imgSrc;
@@ -360,7 +431,7 @@ export const renderImageOnDiv = function (pageWidth, pageHeight, imgSrc, boundar
360
431
  return div;
361
432
  }
362
433
 
363
- export const renderTextObject = function (fontResObj, textObject, defaultFillColor, defaultStrokeColor) {
434
+ export const renderTextObject = function (drawParamResObj, fontResObj, textObject, defaultFillColor, defaultStrokeColor) {
364
435
  let defaultFillOpacity = 1;
365
436
  let boundary = parseStBox(textObject['@_Boundary']);
366
437
  boundary = converterBox(boundary);
@@ -374,6 +445,12 @@ export const renderTextObject = function (fontResObj, textObject, defaultFillCol
374
445
  const textCodePointList = calTextPoint(array);
375
446
  let svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
376
447
  svg.setAttribute('version', '1.1');
448
+ const objectStyle = resolveDrawParamStyle(drawParamResObj, textObject['@_DrawParam'], {
449
+ fillColor: defaultFillColor,
450
+ strokeColor: defaultStrokeColor,
451
+ });
452
+ defaultFillColor = objectStyle.fillColor;
453
+ defaultStrokeColor = objectStyle.strokeColor;
377
454
  const fillColor = textObject['ofd:FillColor'];
378
455
  if (fillColor) {
379
456
  defaultFillColor = parseColor(fillColor['@_Value']);
@@ -387,7 +464,7 @@ export const renderTextObject = function (fontResObj, textObject, defaultFillCol
387
464
  let text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
388
465
  text.setAttribute('x', textCodePoint.x);
389
466
  text.setAttribute('y', textCodePoint.y);
390
- text.innerHTML = textCodePoint.text;
467
+ text.textContent = textCodePoint.text;
391
468
  if (ctm) {
392
469
  const ctms = parseCtm(ctm);
393
470
  text.setAttribute('transform', `matrix(${ctms[0]} ${ctms[1]} ${ctms[2]} ${ctms[3]} ${converterDpi(ctms[4])} ${converterDpi(ctms[5])})`)
@@ -396,10 +473,10 @@ export const renderTextObject = function (fontResObj, textObject, defaultFillCol
396
473
  text.setAttribute('transform', `matrix(${hScale}, 0, 0, 1, ${(1-hScale)*textCodePoint.x}, 0)`)
397
474
  // text.setAttribute('transform-origin', `${textCodePoint.x}`);
398
475
  }
399
- text.setAttribute('fill', defaultStrokeColor);
400
- text.setAttribute('fill', defaultFillColor);
476
+ const textFillColor = defaultFillColor || defaultStrokeColor || 'rgb(0, 0, 0)';
477
+ text.setAttribute('fill', textFillColor);
401
478
  text.setAttribute('fill-opacity', defaultFillOpacity);
402
- text.setAttribute('style', `font-weight: ${weight};font-size:${size}px;font-family: ${getFontFamily(fontResObj[font])};`)
479
+ text.setAttribute('style', `font-weight: ${weight};font-size:${size}px;font-family: ${getFontFamily(fontResObj[font])};color:${textFillColor};`)
403
480
  svg.appendChild(text);
404
481
  }
405
482
 
@@ -412,6 +489,83 @@ export const renderTextObject = function (fontResObj, textObject, defaultFillCol
412
489
  return svg;
413
490
  }
414
491
 
492
+ const buildSvgPathData = function (points) {
493
+ let d = '';
494
+ for (const point of points) {
495
+ if (point.type === 'M') {
496
+ d += `M${point.x} ${point.y} `;
497
+ } else if (point.type === 'L') {
498
+ d += `L${point.x} ${point.y} `;
499
+ } else if (point.type === 'Q') {
500
+ d += `Q${point.x1} ${point.y1} ${point.x2} ${point.y2} `;
501
+ } else if (point.type === 'B') {
502
+ d += `C${point.x1} ${point.y1} ${point.x2} ${point.y2} ${point.x3} ${point.y3} `;
503
+ } else if (point.type === 'C') {
504
+ d += `Z`;
505
+ }
506
+ }
507
+ return d;
508
+ }
509
+
510
+ // OFD 的装饰性 PathObject(如色带、圆角背景)经常共用一个近乎整页的 Boundary,
511
+ // 真正可见的形状由 Clips 裁剪出来;不处理 Clips 会让这些路径以整块矩形铺满并
512
+ // 盖住下层图片/文字。Clips 下可以有多个 Clip(依次相交),每个 Clip 下可以有
513
+ // 多个 Area(在该 Clip 内取并集),语义上等价于 SVG 里逐层嵌套 clip-path。
514
+ const appendClipAreaPath = function (clipPathEl, area) {
515
+ const clipShape = area?.['ofd:Path'];
516
+ const abbreviatedData = clipShape?.['ofd:AbbreviatedData'];
517
+ if (!clipShape || !abbreviatedData) {
518
+ return;
519
+ }
520
+ const clipPoints = calPathPoint(convertPathAbbreviatedDatatoPoint(abbreviatedData));
521
+ const clipPathShape = document.createElementNS('http://www.w3.org/2000/svg', 'path');
522
+ clipPathShape.setAttribute('d', buildSvgPathData(clipPoints));
523
+ const clipCtm = clipShape['@_CTM'];
524
+ if (clipCtm) {
525
+ const ctms = parseCtm(clipCtm);
526
+ clipPathShape.setAttribute('transform', `matrix(${ctms[0]} ${ctms[1]} ${ctms[2]} ${ctms[3]} ${converterDpi(ctms[4])} ${converterDpi(ctms[5])})`);
527
+ }
528
+ clipPathEl.appendChild(clipPathShape);
529
+ }
530
+
531
+ const applyClips = function (svgRoot, contentEl, clips) {
532
+ const clipList = clips?.['ofd:Clip'];
533
+ if (!clipList) {
534
+ return contentEl;
535
+ }
536
+ let clipArray = [];
537
+ clipArray = clipArray.concat(clipList);
538
+ let defs = null;
539
+ let current = contentEl;
540
+ let clipIndex = 0;
541
+ for (const clip of clipArray) {
542
+ const areas = clip?.['ofd:Area'];
543
+ if (!clip || !areas) {
544
+ continue;
545
+ }
546
+ if (!defs) {
547
+ defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
548
+ svgRoot.appendChild(defs);
549
+ }
550
+ const clipPathEl = document.createElementNS('http://www.w3.org/2000/svg', 'clipPath');
551
+ const clipId = `ofd-clip-${clipIndex++}-${Math.random().toString(36).slice(2, 8)}`;
552
+ clipPathEl.setAttribute('id', clipId);
553
+ let areaArray = [];
554
+ areaArray = areaArray.concat(areas);
555
+ for (const area of areaArray) {
556
+ if (area) {
557
+ appendClipAreaPath(clipPathEl, area);
558
+ }
559
+ }
560
+ defs.appendChild(clipPathEl);
561
+ const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
562
+ group.setAttribute('clip-path', `url(#${clipId})`);
563
+ group.appendChild(current);
564
+ current = group;
565
+ }
566
+ return current;
567
+ }
568
+
415
569
  export const renderPathObject = function (drawParamResObj, pathObject, defaultFillColor, defaultStrokeColor, defaultLineWith, isStampAnnot) {
416
570
  let boundary = parseStBox(pathObject['@_Boundary']);
417
571
  boundary = converterBox(boundary);
@@ -427,11 +581,20 @@ export const renderPathObject = function (drawParamResObj, pathObject, defaultFi
427
581
  }
428
582
  const drawParam = pathObject['@_DrawParam'];
429
583
  if (drawParam) {
430
- lineWidth = drawParamResObj[drawParam].LineWidth;
584
+ lineWidth = drawParamResObj[drawParam]?.LineWidth;
431
585
  if (lineWidth) {
432
586
  defaultLineWith = converterDpi(lineWidth);
433
587
  }
434
588
  }
589
+ // PathObject 自身没有内联 ofd:FillColor/StrokeColor 时,颜色通常来自其引用的 DrawParam;
590
+ // 先按 DrawParam 解析出默认色,再让内联颜色(若存在)覆盖,否则文字/图形轮廓会因为
591
+ // 拿不到颜色而按 fill:none 处理,变成不可见。
592
+ const objectStyle = resolveDrawParamStyle(drawParamResObj, drawParam, {
593
+ fillColor: defaultFillColor,
594
+ strokeColor: defaultStrokeColor,
595
+ });
596
+ defaultFillColor = objectStyle.fillColor;
597
+ defaultStrokeColor = objectStyle.strokeColor;
435
598
  if (ctm) {
436
599
  const ctms = parseCtm(ctm);
437
600
  path.setAttribute('transform', `matrix(${ctms[0]} ${ctms[1]} ${ctms[2]} ${ctms[3]} ${converterDpi(ctms[4])} ${converterDpi(ctms[5])})`)
@@ -460,20 +623,8 @@ export const renderPathObject = function (drawParamResObj, pathObject, defaultFi
460
623
  fillStyle = `fill:${isStampAnnot ? 'none' : defaultFillColor ? defaultFillColor : 'none'};`;
461
624
  }
462
625
  path.setAttribute('style', `${strokeStyle};${fillStyle}`)
463
- let d = '';
464
- for (const point of points) {
465
- if (point.type === 'M') {
466
- d += `M${point.x} ${point.y} `;
467
- } else if (point.type === 'L') {
468
- d += `L${point.x} ${point.y} `;
469
- } else if (point.type === 'B') {
470
- d += `C${point.x1} ${point.y1} ${point.x2} ${point.y2} ${point.x3} ${point.y3} `;
471
- } else if (point.type === 'C') {
472
- d += `Z`;
473
- }
474
- }
475
- path.setAttribute('d', d);
476
- svg.appendChild(path);
626
+ path.setAttribute('d', buildSvgPathData(points));
627
+ svg.appendChild(applyClips(svg, path, pathObject['ofd:Clips']));
477
628
  let width = isStampAnnot ? boundary.w : Math.ceil(boundary.w);
478
629
  let height = isStampAnnot ? boundary.h : Math.ceil(boundary.h);
479
630
  let left = boundary.x;