@file-viewer/renderer-ofd 2.1.23 → 2.1.24
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 +5 -3
- package/vendor/dltech/ofd/ofd_parser.js +136 -18
- package/vendor/dltech/ofd/ofd_render.js +9 -9
- package/vendor/dltech/ofd/ofd_util.js +16 -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.24",
|
|
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,17 @@
|
|
|
53
53
|
"LICENSE"
|
|
54
54
|
],
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@file-viewer/core": "2.1.
|
|
56
|
+
"@file-viewer/core": "2.1.24",
|
|
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"
|
|
66
68
|
}
|
|
67
69
|
}
|
|
@@ -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。
|
|
@@ -257,6 +259,30 @@ export const parseSingleDoc = async function ([zip, array]) {
|
|
|
257
259
|
return docs;
|
|
258
260
|
}
|
|
259
261
|
|
|
262
|
+
const uint8ArrayToBase64 = function (bytes) {
|
|
263
|
+
const chunkSize = 0x8000;
|
|
264
|
+
let binary = '';
|
|
265
|
+
for (let i = 0; i < bytes.length; i += chunkSize) {
|
|
266
|
+
const chunk = bytes.subarray(i, i + chunkSize);
|
|
267
|
+
binary += String.fromCharCode.apply(null, chunk);
|
|
268
|
+
}
|
|
269
|
+
return btoa(binary);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const sealImageMime = function (type) {
|
|
273
|
+
const normalized = String(type || '').toLowerCase();
|
|
274
|
+
if (normalized === 'jpg' || normalized === 'jpeg') {
|
|
275
|
+
return 'image/jpeg';
|
|
276
|
+
}
|
|
277
|
+
if (normalized === 'gif') {
|
|
278
|
+
return 'image/gif';
|
|
279
|
+
}
|
|
280
|
+
if (normalized === 'bmp') {
|
|
281
|
+
return 'image/bmp';
|
|
282
|
+
}
|
|
283
|
+
return 'image/png';
|
|
284
|
+
}
|
|
285
|
+
|
|
260
286
|
export const doGetDocRoot = async function (zip, docbody) {
|
|
261
287
|
let docRoot = docbody['ofd:DocRoot'];
|
|
262
288
|
docRoot = normalizeZipPath(replaceFirstSlash(docRoot));
|
|
@@ -267,18 +293,33 @@ export const doGetDocRoot = async function (zip, docbody) {
|
|
|
267
293
|
for (const stamp of stampAnnot) {
|
|
268
294
|
if (stamp.sealObj && Object.keys(stamp.sealObj).length > 0) {
|
|
269
295
|
if (stamp.sealObj.type === 'ofd') {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
296
|
+
try {
|
|
297
|
+
const stampObjs = await getSealDocumentObj(stamp);
|
|
298
|
+
let annotArray = [];
|
|
299
|
+
annotArray = annotArray.concat(stamp.stampAnnot);
|
|
300
|
+
for (const annot of annotArray) {
|
|
301
|
+
if (!annot) {
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
annot.boundary = parseStBox(annot['@_Boundary']);
|
|
305
|
+
annot.pageRef = annot['@_PageRef'];
|
|
306
|
+
for (let stampObj of stampObjs) {
|
|
307
|
+
if (!stampAnnotArray[annot['@_PageRef']]) {
|
|
308
|
+
stampAnnotArray[annot['@_PageRef']] = [];
|
|
309
|
+
}
|
|
310
|
+
stampAnnotArray[annot['@_PageRef']].push({
|
|
311
|
+
type: 'ofd',
|
|
312
|
+
obj: stampObj,
|
|
313
|
+
stamp: {...stamp, stampAnnot: annot},
|
|
314
|
+
});
|
|
315
|
+
}
|
|
277
316
|
}
|
|
278
|
-
|
|
317
|
+
} catch (e) {
|
|
318
|
+
console.warn('[ofd] nested OFD seal parse failed', e);
|
|
279
319
|
}
|
|
280
|
-
} else if (stamp.sealObj.
|
|
281
|
-
|
|
320
|
+
} else if (stamp.sealObj.ofdArray?.length) {
|
|
321
|
+
const mime = sealImageMime(stamp.sealObj.type);
|
|
322
|
+
let img = `data:${mime};base64,` + uint8ArrayToBase64(stamp.sealObj.ofdArray);
|
|
282
323
|
let stampArray = [];
|
|
283
324
|
stampArray = stampArray.concat(stamp.stampAnnot);
|
|
284
325
|
for (const annot of stampArray) {
|
|
@@ -523,16 +564,93 @@ const parsePage = async function (zip, obj, doc) {
|
|
|
523
564
|
}
|
|
524
565
|
|
|
525
566
|
const getSignature = async function (zip, signatures, doc) {
|
|
526
|
-
//
|
|
527
|
-
//
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
567
|
+
// 预览只解析签章外观(SES 印章图片 / 嵌套 OFD),不做国密/证书验签,
|
|
568
|
+
// 单个签章失败不影响正文页渲染。
|
|
569
|
+
let stampAnnot = [];
|
|
570
|
+
if (!signatures) {
|
|
571
|
+
return stampAnnot;
|
|
572
|
+
}
|
|
573
|
+
try {
|
|
574
|
+
let signaturesPath = normalizeZipPath(replaceFirstSlash(signatures));
|
|
575
|
+
if (!startsWithZipRoot(signaturesPath, doc)) {
|
|
576
|
+
signaturesPath = joinZipPath(doc, signaturesPath);
|
|
577
|
+
}
|
|
578
|
+
if (!getZipEntry(zip, signaturesPath)) {
|
|
579
|
+
return stampAnnot;
|
|
580
|
+
}
|
|
581
|
+
let data = await getJsonFromXmlContent(zip, signaturesPath);
|
|
582
|
+
let signature = data['json']['ofd:Signatures']['ofd:Signature'];
|
|
583
|
+
let signatureArray = [];
|
|
584
|
+
signatureArray = signatureArray.concat(signature);
|
|
585
|
+
for (const sign of signatureArray) {
|
|
586
|
+
if (!sign) {
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
try {
|
|
590
|
+
let signatureLoc = normalizeZipPath(replaceFirstSlash(sign['@_BaseLoc']));
|
|
591
|
+
let signatureID = sign['@_ID'];
|
|
592
|
+
if (signatureLoc && signatureLoc.indexOf('Signs') === -1 && !startsWithZipRoot(signatureLoc, doc)) {
|
|
593
|
+
signatureLoc = joinZipPath('Signs', signatureLoc);
|
|
594
|
+
}
|
|
595
|
+
if (!startsWithZipRoot(signatureLoc, doc)) {
|
|
596
|
+
signatureLoc = joinZipPath(doc, signatureLoc);
|
|
597
|
+
}
|
|
598
|
+
const parsed = await getSignatureData(zip, signatureLoc, signatureID);
|
|
599
|
+
if (parsed) {
|
|
600
|
+
stampAnnot.push(parsed);
|
|
601
|
+
}
|
|
602
|
+
} catch (e) {
|
|
603
|
+
console.warn('[ofd] signature entry parse failed', sign?.['@_ID'], e);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
} catch (e) {
|
|
607
|
+
console.warn('[ofd] signatures.xml parse failed', e);
|
|
608
|
+
}
|
|
609
|
+
return stampAnnot;
|
|
532
610
|
}
|
|
533
611
|
|
|
534
|
-
const
|
|
535
|
-
|
|
612
|
+
const getSignatureData = async function (zip, signature, signatureID) {
|
|
613
|
+
const data = await getJsonFromXmlContent(zip, signature);
|
|
614
|
+
let signedValue = data['json']['ofd:Signature']['ofd:SignedValue'];
|
|
615
|
+
if (signedValue == null) {
|
|
616
|
+
return null;
|
|
617
|
+
}
|
|
618
|
+
signedValue = normalizeZipPath(replaceFirstSlash(String(signedValue)));
|
|
619
|
+
if (!getZipEntry(zip, signedValue)) {
|
|
620
|
+
signedValue = joinZipPath(signature.substring(0, signature.lastIndexOf('/')), signedValue);
|
|
621
|
+
}
|
|
622
|
+
if (!getZipEntry(zip, signedValue)) {
|
|
623
|
+
console.warn('[ofd] SignedValue not found', signedValue);
|
|
624
|
+
return null;
|
|
625
|
+
}
|
|
626
|
+
let sealObj = await parseSesSignature(zip, signedValue, getZipEntry);
|
|
627
|
+
if (!sealObj || !Object.keys(sealObj).length) {
|
|
628
|
+
return null;
|
|
629
|
+
}
|
|
630
|
+
const signedInfoNode = data['json']['ofd:Signature']['ofd:SignedInfo'] || {};
|
|
631
|
+
return {
|
|
632
|
+
'stampAnnot': signedInfoNode['ofd:StampAnnot'],
|
|
633
|
+
'sealObj': sealObj,
|
|
634
|
+
'signedInfo': {
|
|
635
|
+
'signatureID': signatureID,
|
|
636
|
+
'VerifyRet': sealObj.verifyRet,
|
|
637
|
+
'Provider': signedInfoNode['ofd:Provider'],
|
|
638
|
+
'SignatureMethod': signedInfoNode['ofd:SignatureMethod'],
|
|
639
|
+
'SignatureDateTime': signedInfoNode['ofd:SignatureDateTime'],
|
|
640
|
+
},
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const getSealDocumentObj = function (stampAnnot) {
|
|
645
|
+
return new Promise((resolve, reject) => {
|
|
646
|
+
pipeline.call(this, async () => await unzipOfd(stampAnnot.sealObj.ofdArray), getDocRoots, parseSingleDoc)
|
|
647
|
+
.then(res => {
|
|
648
|
+
resolve(res)
|
|
649
|
+
})
|
|
650
|
+
.catch(res => {
|
|
651
|
+
reject(res);
|
|
652
|
+
});
|
|
653
|
+
});
|
|
536
654
|
}
|
|
537
655
|
|
|
538
656
|
const getJsonFromXmlContent = async function (zip, xmlName) {
|
|
@@ -208,9 +208,9 @@ const renderSealPage = function (pageDiv, pages, tpls, isStampAnnot, stampAnnot,
|
|
|
208
208
|
let divBoundary = converterBox(stampAnnotBoundary);
|
|
209
209
|
let div = document.createElement('div');
|
|
210
210
|
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)}`);
|
|
211
|
+
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`)
|
|
212
|
+
div.setAttribute('data-ses-signature', `${JSON.stringify(SES_Signature || {})}`);
|
|
213
|
+
div.setAttribute('data-signed-info', `${JSON.stringify(signedInfo || {})}`);
|
|
214
214
|
const template = page[pageId]['json']['ofd:Template'];
|
|
215
215
|
if (template) {
|
|
216
216
|
const layers = tpls[template['@_TemplateID']]['json']['ofd:Content']['ofd:Layer'];
|
|
@@ -339,8 +339,8 @@ export const renderImageOnDiv = function (pageWidth, pageHeight, imgSrc, boundar
|
|
|
339
339
|
if(isStampAnnot)
|
|
340
340
|
{
|
|
341
341
|
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)}`);
|
|
342
|
+
div.setAttribute('data-ses-signature', `${JSON.stringify(SES_Signature || {})}`);
|
|
343
|
+
div.setAttribute('data-signed-info', `${JSON.stringify(signedInfo || {})}`);
|
|
344
344
|
}
|
|
345
345
|
let img = document.createElement('img');
|
|
346
346
|
img.src = imgSrc;
|
|
@@ -387,7 +387,7 @@ export const renderTextObject = function (fontResObj, textObject, defaultFillCol
|
|
|
387
387
|
let text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
|
388
388
|
text.setAttribute('x', textCodePoint.x);
|
|
389
389
|
text.setAttribute('y', textCodePoint.y);
|
|
390
|
-
text.
|
|
390
|
+
text.textContent = textCodePoint.text;
|
|
391
391
|
if (ctm) {
|
|
392
392
|
const ctms = parseCtm(ctm);
|
|
393
393
|
text.setAttribute('transform', `matrix(${ctms[0]} ${ctms[1]} ${ctms[2]} ${ctms[3]} ${converterDpi(ctms[4])} ${converterDpi(ctms[5])})`)
|
|
@@ -396,10 +396,10 @@ export const renderTextObject = function (fontResObj, textObject, defaultFillCol
|
|
|
396
396
|
text.setAttribute('transform', `matrix(${hScale}, 0, 0, 1, ${(1-hScale)*textCodePoint.x}, 0)`)
|
|
397
397
|
// text.setAttribute('transform-origin', `${textCodePoint.x}`);
|
|
398
398
|
}
|
|
399
|
-
|
|
400
|
-
text.setAttribute('fill',
|
|
399
|
+
const textFillColor = defaultFillColor || defaultStrokeColor || 'rgb(0, 0, 0)';
|
|
400
|
+
text.setAttribute('fill', textFillColor);
|
|
401
401
|
text.setAttribute('fill-opacity', defaultFillOpacity);
|
|
402
|
-
text.setAttribute('style', `font-weight: ${weight};font-size:${size}px;font-family: ${getFontFamily(fontResObj[font])};`)
|
|
402
|
+
text.setAttribute('style', `font-weight: ${weight};font-size:${size}px;font-family: ${getFontFamily(fontResObj[font])};color:${textFillColor};`)
|
|
403
403
|
svg.appendChild(text);
|
|
404
404
|
}
|
|
405
405
|
|
|
@@ -192,8 +192,7 @@ export const calTextPoint = function (textCodes) {
|
|
|
192
192
|
let textStr = textCode['#text'];
|
|
193
193
|
if (textStr) {
|
|
194
194
|
textStr += '';
|
|
195
|
-
textStr =
|
|
196
|
-
textStr = textStr.replace(/ /g, ' ');
|
|
195
|
+
textStr = decodeOfdText(textStr);
|
|
197
196
|
for (let i = 0; i < textStr.length; i++) {
|
|
198
197
|
if (i > 0 && deltaXList.length > 0) {
|
|
199
198
|
x += deltaXList[(i - 1)];
|
|
@@ -232,7 +231,7 @@ export const getExtensionByPath = function (path) {
|
|
|
232
231
|
}
|
|
233
232
|
|
|
234
233
|
|
|
235
|
-
let REGX_HTML_DECODE = /&\w+;|&#(
|
|
234
|
+
let REGX_HTML_DECODE = /&\w+;|&#(x?[0-9a-fA-F]+);/g;
|
|
236
235
|
|
|
237
236
|
let HTML_DECODE = {
|
|
238
237
|
"<": "<",
|
|
@@ -253,8 +252,9 @@ export const decodeHtml = function (s) {
|
|
|
253
252
|
var c = HTML_DECODE[$0];
|
|
254
253
|
if (c == undefined) {
|
|
255
254
|
// Maybe is Entity Number
|
|
256
|
-
if (
|
|
257
|
-
|
|
255
|
+
if ($1) {
|
|
256
|
+
const codePoint = /^x/i.test($1) ? parseInt($1.slice(1), 16) : parseInt($1, 10);
|
|
257
|
+
c = Number.isFinite(codePoint) ? String.fromCodePoint((codePoint == 160) ? 32 : codePoint) : $0;
|
|
258
258
|
} else {
|
|
259
259
|
c = $0;
|
|
260
260
|
}
|
|
@@ -263,6 +263,17 @@ export const decodeHtml = function (s) {
|
|
|
263
263
|
});
|
|
264
264
|
};
|
|
265
265
|
|
|
266
|
+
export const decodeOfdText = function (s) {
|
|
267
|
+
s = decodeHtml(s);
|
|
268
|
+
if (typeof s != "string") {
|
|
269
|
+
return s;
|
|
270
|
+
}
|
|
271
|
+
return s.replace(/\\0x([0-9a-fA-F]{4,6})/g, function (_, hex) {
|
|
272
|
+
const codePoint = parseInt(hex, 16);
|
|
273
|
+
return Number.isFinite(codePoint) ? String.fromCodePoint((codePoint == 160) ? 32 : codePoint) : _;
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
266
277
|
let FONT_FAMILY = {
|
|
267
278
|
'楷体': '楷体, KaiTi, Kai, simkai',
|
|
268
279
|
'kaiti': '楷体, KaiTi, Kai, simkai',
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ofd.js - A Javascript class for reading and rendering ofd files
|
|
3
|
+
* <https://github.com/DLTech21/ofd.js>
|
|
4
|
+
*
|
|
5
|
+
* Copyright (c) 2020. DLTech21 All Rights Reserved.
|
|
6
|
+
*
|
|
7
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
* you may not use this file except in compliance with the License.
|
|
9
|
+
* You may obtain a copy of the License at
|
|
10
|
+
*
|
|
11
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
*
|
|
13
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
* See the License for the specific language governing permissions and
|
|
17
|
+
* limitations under the License.
|
|
18
|
+
*
|
|
19
|
+
* Display-only SES signature parser for File Viewer:
|
|
20
|
+
* extracts seal picture bytes for preview, skips crypto verification
|
|
21
|
+
* so ASN.1/国密校验不会进入预览主路径。
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import Hex from '../../lapo-asn1js/hex.js';
|
|
25
|
+
import Base64 from '../../lapo-asn1js/base64.js';
|
|
26
|
+
import ASN1 from '../../lapo-asn1js/asn1.js';
|
|
27
|
+
|
|
28
|
+
const reHex = /^\s*(?:[0-9A-Fa-f][0-9A-Fa-f]\s*)+$/;
|
|
29
|
+
|
|
30
|
+
const unwrapDefault = (mod) => (mod && typeof mod === 'object' && 'default' in mod ? mod.default : mod);
|
|
31
|
+
|
|
32
|
+
const HexApi = unwrapDefault(Hex);
|
|
33
|
+
const Base64Api = unwrapDefault(Base64);
|
|
34
|
+
const ASN1Api = unwrapDefault(ASN1);
|
|
35
|
+
|
|
36
|
+
export const parseSesSignature = async function (zip, name, getEntry) {
|
|
37
|
+
const entry = typeof getEntry === 'function' ? getEntry(zip, name) : zip.files[name];
|
|
38
|
+
if (!entry) {
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const bytes = await entry.async('base64');
|
|
43
|
+
return decodeText(bytes);
|
|
44
|
+
} catch (e) {
|
|
45
|
+
console.warn('[ofd] parseSesSignature failed', name, e);
|
|
46
|
+
return {};
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const decodeText = function (val) {
|
|
51
|
+
try {
|
|
52
|
+
const der = reHex.test(val) ? HexApi.decode(val) : Base64Api.unarmor(val);
|
|
53
|
+
return decode(der);
|
|
54
|
+
} catch (e) {
|
|
55
|
+
console.warn('[ofd] decode SES signature text failed', e);
|
|
56
|
+
return {};
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const decode = function (der, offset) {
|
|
61
|
+
offset = offset || 0;
|
|
62
|
+
try {
|
|
63
|
+
const SES_Signature = decodeSES_Signature(der, offset);
|
|
64
|
+
const picture = SES_Signature?.toSign?.eseal?.esealInfo?.picture;
|
|
65
|
+
if (!picture?.data?.byte?.length) {
|
|
66
|
+
return {};
|
|
67
|
+
}
|
|
68
|
+
const type = (picture.type?.str || picture.type || '').toString().toLowerCase();
|
|
69
|
+
return {
|
|
70
|
+
ofdArray: picture.data.byte,
|
|
71
|
+
type,
|
|
72
|
+
// 预览只保留轻量元数据,避免把 DER/证书二进制塞进 DOM data-*。
|
|
73
|
+
SES_Signature: {
|
|
74
|
+
realVersion: SES_Signature.realVersion,
|
|
75
|
+
displayOnly: true,
|
|
76
|
+
pictureType: type,
|
|
77
|
+
pictureWidth: picture.width,
|
|
78
|
+
pictureHeight: picture.height,
|
|
79
|
+
sealName: SES_Signature.toSign?.eseal?.esealInfo?.property?.name,
|
|
80
|
+
},
|
|
81
|
+
verifyRet: true,
|
|
82
|
+
};
|
|
83
|
+
} catch (e) {
|
|
84
|
+
console.warn('[ofd] decode SES signature failed', e);
|
|
85
|
+
return {};
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const decodeUTCTime = function (str) {
|
|
90
|
+
str = String(str || '').replace('Unrecognized time: ', '');
|
|
91
|
+
str = str.replace('Z', '');
|
|
92
|
+
str = str.substr(0, 1) < '5' ? '20' + str : '19' + str;
|
|
93
|
+
return str;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const parseStringUTF = function (node) {
|
|
97
|
+
if (!node) {
|
|
98
|
+
return '';
|
|
99
|
+
}
|
|
100
|
+
return node.stream.parseStringUTF(node.stream.pos + node.header, node.stream.pos + node.header + node.length);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const parseInteger = function (node) {
|
|
104
|
+
if (!node) {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
return node.stream.parseInteger(node.stream.pos + node.header, node.stream.pos + node.header + node.length);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const parseOctetBytes = function (node) {
|
|
111
|
+
if (!node) {
|
|
112
|
+
return new Uint8Array(0);
|
|
113
|
+
}
|
|
114
|
+
return node.stream.enc.subarray(node.stream.pos + node.header, node.stream.pos + node.header + node.length);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const parseTimeNode = function (node, utc) {
|
|
118
|
+
if (!node) {
|
|
119
|
+
return '';
|
|
120
|
+
}
|
|
121
|
+
const raw = node.stream.parseTime(node.stream.pos + node.header, node.stream.pos + node.header + node.length, utc);
|
|
122
|
+
return decodeUTCTime(raw);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const decodeSES_Signature = function (der, offset) {
|
|
126
|
+
offset = offset || 0;
|
|
127
|
+
const asn1 = ASN1Api.decode(der, offset);
|
|
128
|
+
let SES_Signature;
|
|
129
|
+
try {
|
|
130
|
+
// V1
|
|
131
|
+
const createDate = parseTimeNode(asn1.sub[0]?.sub[1]?.sub[0]?.sub[2]?.sub[3]);
|
|
132
|
+
const validStart = parseTimeNode(asn1.sub[0]?.sub[1]?.sub[0]?.sub[2]?.sub[4]);
|
|
133
|
+
const validEnd = parseTimeNode(asn1.sub[0]?.sub[1]?.sub[0]?.sub[2]?.sub[5]);
|
|
134
|
+
const pictureNode = asn1.sub[0]?.sub[1]?.sub[0]?.sub[3];
|
|
135
|
+
SES_Signature = {
|
|
136
|
+
realVersion: 1,
|
|
137
|
+
toSign: {
|
|
138
|
+
eseal: {
|
|
139
|
+
esealInfo: {
|
|
140
|
+
property: {
|
|
141
|
+
name: parseStringUTF(asn1.sub[0]?.sub[1]?.sub[0]?.sub[2]?.sub[1]),
|
|
142
|
+
createDate,
|
|
143
|
+
validStart,
|
|
144
|
+
validEnd,
|
|
145
|
+
},
|
|
146
|
+
picture: {
|
|
147
|
+
type: parseStringUTF(pictureNode?.sub[0]),
|
|
148
|
+
data: { byte: parseOctetBytes(pictureNode?.sub[1]) },
|
|
149
|
+
width: parseInteger(pictureNode?.sub[2]),
|
|
150
|
+
height: parseInteger(pictureNode?.sub[3]),
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
} catch (e) {
|
|
157
|
+
try {
|
|
158
|
+
// V4
|
|
159
|
+
const pictureNode = asn1.sub[0]?.sub[1]?.sub[0]?.sub[3];
|
|
160
|
+
SES_Signature = {
|
|
161
|
+
realVersion: 4,
|
|
162
|
+
toSign: {
|
|
163
|
+
eseal: {
|
|
164
|
+
esealInfo: {
|
|
165
|
+
property: {
|
|
166
|
+
name: parseStringUTF(asn1.sub[0]?.sub[1]?.sub[0]?.sub[2]?.sub[1]),
|
|
167
|
+
createDate: asn1.sub[0]?.sub[1]?.sub[0]?.sub[2]?.sub[4]?.stream.parseTime(
|
|
168
|
+
asn1.sub[0].sub[1].sub[0].sub[2].sub[4].stream.pos + asn1.sub[0].sub[1].sub[0].sub[2].sub[4].header,
|
|
169
|
+
asn1.sub[0].sub[1].sub[0].sub[2].sub[4].stream.pos + asn1.sub[0].sub[1].sub[0].sub[2].sub[4].header + asn1.sub[0].sub[1].sub[0].sub[2].sub[4].length
|
|
170
|
+
),
|
|
171
|
+
validStart: asn1.sub[0]?.sub[1]?.sub[0]?.sub[2]?.sub[5]?.stream.parseTime(
|
|
172
|
+
asn1.sub[0].sub[1].sub[0].sub[2].sub[5].stream.pos + asn1.sub[0].sub[1].sub[0].sub[2].sub[5].header,
|
|
173
|
+
asn1.sub[0].sub[1].sub[0].sub[2].sub[5].stream.pos + asn1.sub[0].sub[1].sub[0].sub[2].sub[5].header + asn1.sub[0].sub[1].sub[0].sub[2].sub[5].length
|
|
174
|
+
),
|
|
175
|
+
validEnd: asn1.sub[0]?.sub[1]?.sub[0]?.sub[2]?.sub[6]?.stream.parseTime(
|
|
176
|
+
asn1.sub[0].sub[1].sub[0].sub[2].sub[6].stream.pos + asn1.sub[0].sub[1].sub[0].sub[2].sub[6].header,
|
|
177
|
+
asn1.sub[0].sub[1].sub[0].sub[2].sub[6].stream.pos + asn1.sub[0].sub[1].sub[0].sub[2].sub[6].header + asn1.sub[0].sub[1].sub[0].sub[2].sub[6].length
|
|
178
|
+
),
|
|
179
|
+
},
|
|
180
|
+
picture: {
|
|
181
|
+
type: parseStringUTF(pictureNode?.sub[0]),
|
|
182
|
+
data: { byte: parseOctetBytes(pictureNode?.sub[1]) },
|
|
183
|
+
width: parseInteger(pictureNode?.sub[2]),
|
|
184
|
+
height: parseInteger(pictureNode?.sub[3]),
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
} catch (inner) {
|
|
191
|
+
console.warn('[ofd] unsupported SES signature structure', inner);
|
|
192
|
+
SES_Signature = {};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return SES_Signature;
|
|
196
|
+
};
|