@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 +1 -0
- package/README.md +1 -0
- package/dist/ofd.js +10 -4
- package/package.json +7 -3
- package/vendor/dltech/ofd/ofd_parser.js +148 -21
- package/vendor/dltech/ofd/ofd_render.js +206 -55
- package/vendor/dltech/ofd/ofd_util.js +36 -5
- package/vendor/dltech/ofd/ses_signature_parser.js +196 -0
- package/vendor/lapo-asn1js/README.md +3 -0
- package/vendor/lapo-asn1js/asn1.js +525 -0
- package/vendor/lapo-asn1js/base64.js +82 -0
- package/vendor/lapo-asn1js/hex.js +51 -0
- package/vendor/lapo-asn1js/int10.js +90 -0
- package/vendor/lapo-asn1js/oids.js +2455 -0
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
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
|
-
.
|
|
15
|
-
.
|
|
16
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
326
|
+
} catch (e) {
|
|
327
|
+
console.warn('[ofd] nested OFD seal parse failed', e);
|
|
279
328
|
}
|
|
280
|
-
} else if (stamp.sealObj.
|
|
281
|
-
|
|
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
|
-
//
|
|
527
|
-
//
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
400
|
-
text.setAttribute('fill',
|
|
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]
|
|
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
|
-
|
|
464
|
-
|
|
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;
|