@file-viewer/renderer-ofd 2.1.22 → 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 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.22",
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.22",
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。
@@ -71,12 +73,18 @@ const parseXmlElement = function (element, options) {
71
73
  return result;
72
74
  }
73
75
 
76
+ const normalizeXmlContent = function (xmlData) {
77
+ return String(xmlData || '')
78
+ .replace(/^\uFEFF/, '')
79
+ .replace(/^\u00EF\u00BB\u00BF/, '');
80
+ }
81
+
74
82
  const parseXmlToJson = function (xmlData, options = {}) {
75
83
  const DOMParserCtor = globalThis.DOMParser;
76
84
  if (typeof DOMParserCtor !== 'function') {
77
85
  throw new Error('OFD XML parser requires DOMParser in the current runtime');
78
86
  }
79
- const document = new DOMParserCtor().parseFromString(xmlData, 'application/xml');
87
+ const document = new DOMParserCtor().parseFromString(normalizeXmlContent(xmlData), 'application/xml');
80
88
  const parserError = document.getElementsByTagName('parsererror')[0];
81
89
  if (parserError) {
82
90
  throw new Error(parserError.textContent || 'OFD XML parse failed');
@@ -251,6 +259,30 @@ export const parseSingleDoc = async function ([zip, array]) {
251
259
  return docs;
252
260
  }
253
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
+
254
286
  export const doGetDocRoot = async function (zip, docbody) {
255
287
  let docRoot = docbody['ofd:DocRoot'];
256
288
  docRoot = normalizeZipPath(replaceFirstSlash(docRoot));
@@ -261,18 +293,33 @@ export const doGetDocRoot = async function (zip, docbody) {
261
293
  for (const stamp of stampAnnot) {
262
294
  if (stamp.sealObj && Object.keys(stamp.sealObj).length > 0) {
263
295
  if (stamp.sealObj.type === 'ofd') {
264
- const stampObjs = await getSealDocumentObj(stamp);
265
- for (let stampObj of stampObjs) {
266
- stamp.stampAnnot.boundary = parseStBox(stamp.stampAnnot['@_Boundary']);
267
- //console.log(stamp.stampAnnot.boundary)
268
- stamp.stampAnnot.pageRef = stamp.stampAnnot['@_PageRef'];
269
- if (!stampAnnotArray[stamp.stampAnnot['@_PageRef']]) {
270
- stampAnnotArray[stamp.stampAnnot['@_PageRef']] = [];
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
+ }
271
316
  }
272
- stampAnnotArray[stamp.stampAnnot['@_PageRef']].push({type: 'ofd', obj: stampObj, stamp});
317
+ } catch (e) {
318
+ console.warn('[ofd] nested OFD seal parse failed', e);
273
319
  }
274
- } else if (stamp.sealObj.type === 'png') {
275
- let img = 'data:image/png;base64,' + btoa(String.fromCharCode.apply(null, stamp.sealObj.ofdArray));
320
+ } else if (stamp.sealObj.ofdArray?.length) {
321
+ const mime = sealImageMime(stamp.sealObj.type);
322
+ let img = `data:${mime};base64,` + uint8ArrayToBase64(stamp.sealObj.ofdArray);
276
323
  let stampArray = [];
277
324
  stampArray = stampArray.concat(stamp.stampAnnot);
278
325
  for (const annot of stampArray) {
@@ -517,16 +564,93 @@ const parsePage = async function (zip, obj, doc) {
517
564
  }
518
565
 
519
566
  const getSignature = async function (zip, signatures, doc) {
520
- // 签章解析依赖 ASN.1、国密算法和证书校验链路。在线预览先跳过签章校验,
521
- // 以免个别签章解析失败阻断正文页渲染;业务如需验签可在服务端独立处理。
522
- void zip;
523
- void signatures;
524
- void doc;
525
- return [];
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;
610
+ }
611
+
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
+ };
526
642
  }
527
643
 
528
- const getSealDocumentObj = function () {
529
- return Promise.resolve([]);
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
+ });
530
654
  }
531
655
 
532
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: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)}`);
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.innerHTML = textCodePoint.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
- text.setAttribute('fill', defaultStrokeColor);
400
- text.setAttribute('fill', defaultFillColor);
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 = decodeHtml(textStr);
196
- textStr = textStr.replace(/&#x20;/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+;|&#(\d+);/g;
234
+ let REGX_HTML_DECODE = /&\w+;|&#(x?[0-9a-fA-F]+);/g;
236
235
 
237
236
  let HTML_DECODE = {
238
237
  "&lt;": "<",
@@ -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 (!isNaN($1)) {
257
- c = String.fromCharCode(($1 == 160) ? 32 : $1);
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
+ };
@@ -0,0 +1,3 @@
1
+ # @lapo/asn1js browser ESM vendoring
2
+
3
+ Vendored from @lapo/asn1js@1.2.1 for browser-safe OFD SES signature parsing.