@file-viewer/renderer-ofd 3.0.3 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@file-viewer/renderer-ofd",
3
- "version": "3.0.3",
3
+ "version": "3.1.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Standalone OFD renderer plugin for File Viewer powered by DLTech21/ofd.js.",
@@ -54,7 +54,7 @@
54
54
  "LICENSE"
55
55
  ],
56
56
  "dependencies": {
57
- "@file-viewer/core": "3.0.3",
57
+ "@file-viewer/core": "3.1.0",
58
58
  "jszip": "3.10.2"
59
59
  },
60
60
  "devDependencies": {
@@ -73,6 +73,7 @@
73
73
  "verify:image-placement": "node scripts/verify-image-placement.mjs",
74
74
  "verify:pageblock-render": "node scripts/verify-pageblock-render.mjs",
75
75
  "verify:render-fidelity": "node scripts/verify-render-fidelity.mjs",
76
- "verify:github-177": "pnpm --filter @file-viewer/core build && pnpm exec tsc -b tsconfig.json --force && node scripts/verify-github-177.mjs"
76
+ "verify:github-177": "pnpm --filter @file-viewer/core build && pnpm exec tsc -b tsconfig.json --force && node scripts/verify-github-177.mjs",
77
+ "verify:github-266": "node scripts/verify-github-266.mjs"
77
78
  }
78
79
  }
@@ -78,7 +78,10 @@ const parseXmlElement = function (element, options, order) {
78
78
  }
79
79
  }
80
80
 
81
- const text = textParts.join('').trim();
81
+ // TextCode whitespace consumes explicitly positioned glyph slots. Trimming
82
+ // it moves right-aligned dates and also shifts their DeltaX indices.
83
+ const rawText = textParts.join('');
84
+ const text = xmlElementName(element) === 'ofd:TextCode' ? rawText : rawText.trim();
82
85
  if (text) {
83
86
  if (Object.keys(result).length === 0) {
84
87
  return text;
@@ -651,6 +654,7 @@ const getSignatureData = async function (zip, signature, signatureID) {
651
654
  'signedInfo': {
652
655
  'signatureID': signatureID,
653
656
  'VerifyRet': sealObj.verifyRet,
657
+ 'VerificationStatus': sealObj.verificationStatus || 'not-verified',
654
658
  'Provider': signedInfoNode['ofd:Provider'],
655
659
  'SignatureMethod': signedInfoNode['ofd:SignatureMethod'],
656
660
  'SignatureDateTime': signedInfoNode['ofd:SignatureDateTime'],
@@ -470,8 +470,9 @@ export const renderTextObject = function (drawParamResObj, fontResObj, textObjec
470
470
  for (const textCodePoint of textCodePointList) {
471
471
  if (textCodePoint && !isNaN(textCodePoint.x)) {
472
472
  let text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
473
- text.setAttribute('x', textCodePoint.x);
474
- text.setAttribute('y', textCodePoint.y);
473
+ text.setAttribute('x', textCodePoint.xPositions?.join(' ') || textCodePoint.x);
474
+ text.setAttribute('y', textCodePoint.yPositions?.join(' ') || textCodePoint.y);
475
+ text.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve');
475
476
  text.textContent = textCodePoint.text;
476
477
  if (ctm) {
477
478
  const ctms = parseCtm(ctm);
@@ -484,7 +485,7 @@ export const renderTextObject = function (drawParamResObj, fontResObj, textObjec
484
485
  const textFillColor = defaultFillColor || defaultStrokeColor || 'rgb(0, 0, 0)';
485
486
  text.setAttribute('fill', textFillColor);
486
487
  text.setAttribute('fill-opacity', defaultFillOpacity);
487
- text.setAttribute('style', `font-weight: ${weight};font-size:${size}px;font-family: ${getFontFamily(fontResObj[font])};color:${textFillColor};`)
488
+ text.setAttribute('style', `white-space:pre;font-weight: ${weight};font-size:${size}px;font-family: ${getFontFamily(fontResObj[font])};color:${textFillColor};`)
488
489
  svg.appendChild(text);
489
490
  }
490
491
 
@@ -142,98 +142,54 @@ export const converterDpi = function (width) {
142
142
  return millimetersToPixel(width, Scale * 25.4);
143
143
  }
144
144
 
145
- export const deltaFormatter = function (delta) {
146
- if (delta.indexOf("g") === -1) {
147
- let floatList = [];
148
- for (let f of delta.split(' ')) {
149
- floatList.push(parseFloat(f));
150
- }
151
- return floatList;
152
- } else {
153
- const array = delta.split(' ');
154
- let gFlag = false;
155
- let gProcessing = false;
156
- let gItemCount = 0;
157
- let floatList = [];
158
- for (const s of array) {
159
- if ('g' === s) {
160
- gFlag = true;
161
- } else {
162
- if (!s || s.trim().length == 0) {
163
- continue;
164
- }
165
- if (gFlag) {
166
- gItemCount = parseInt(s);
167
- gProcessing = true;
168
- gFlag = false;
169
- } else if (gProcessing) {
170
- for (let j = 0; j < gItemCount; j++) {
171
- floatList.push(parseFloat(s));
172
- }
173
- gProcessing = false;
174
- } else {
175
- floatList.push(parseFloat(s));
176
- }
177
- }
145
+ export const deltaFormatter = function (delta, maxValues = 100000) {
146
+ const tokens = String(delta ?? '').trim().split(/\s+/);
147
+ const values = [];
148
+ const limit = Math.max(0, Math.min(100000, maxValues));
149
+ for (let i = 0; i < tokens.length && values.length < limit; i++) {
150
+ let count = 1;
151
+ if (tokens[i] === 'g') {
152
+ count = Number(tokens[++i]);
153
+ i++;
154
+ if (!Number.isSafeInteger(count) || count < 0) break;
178
155
  }
179
- return floatList;
156
+ const value = Number(tokens[i]);
157
+ if (!tokens[i] || !Number.isFinite(value)) break;
158
+ count = Math.min(count, limit - values.length);
159
+ for (let j = 0; j < count; j++) values.push(value);
180
160
  }
161
+ return values;
181
162
  }
182
163
 
183
164
  export const calTextPoint = function (textCodes) {
184
- let x = 0;
185
- let y = 0;
186
- let textCodePointList = [];
187
- if (!textCodes) {
188
- return textCodePointList;
189
- }
190
- for (let textCode of textCodes) {
191
- if (!textCode) {
192
- continue
193
- }
194
- x = parseFloat(textCode['@_X']);
195
- y = parseFloat(textCode['@_Y']);
196
-
197
- if (isNaN(x)) {
198
- x = 0;
199
- }
200
- if (isNaN(y)) {
201
- y = 0;
202
- }
203
-
204
- let deltaXList = [];
205
- let deltaYList = [];
206
- if (textCode['@_DeltaX'] && textCode['@_DeltaX'].length > 0) {
207
- deltaXList = deltaFormatter(textCode['@_DeltaX']);
208
- }
209
- if (textCode['@_DeltaY'] && textCode['@_DeltaY'].length > 0) {
210
- deltaYList = deltaFormatter(textCode['@_DeltaY']);
211
- }
212
- let textStr = textCode['#text'];
213
- if (textStr) {
214
- textStr += '';
215
- textStr = decodeOfdText(textStr);
216
- for (let i = 0; i < textStr.length; i++) {
217
- if (i > 0 && deltaXList.length > 0) {
218
- x += deltaXList[(i - 1)];
219
- }
220
- if (i > 0 && deltaYList.length > 0) {
221
- y += deltaYList[(i - 1)];
222
- }
223
- let text = textStr.substring(i, i + 1);
224
- let filterPointY = textCodePointList.filter((textCodePoint) => {
225
- return textCodePoint.y == converterDpi(y)
226
- });
227
- if (filterPointY && filterPointY.length) { // Y坐标相同,无需再创建text标签
228
- filterPointY[0].text += text;
229
- } else {
230
- let textCodePoint = { 'x': converterDpi(x), 'y': converterDpi(y), 'text': text };
231
- textCodePointList.push(textCodePoint);
232
- }
165
+ const runs = [];
166
+ for (const code of textCodes || []) {
167
+ if (code == null) continue;
168
+ const value = typeof code === 'string' ? code : code['#text'];
169
+ if (value == null || value === '') continue;
170
+ const text = decodeOfdText(String(value));
171
+ const count = Array.from(text).length;
172
+ const finite = value => Number.isFinite(Number.parseFloat(value)) ? Number.parseFloat(value) : 0;
173
+ const x = finite(code['@_X']);
174
+ const y = finite(code['@_Y']);
175
+ const positions = (start, delta) => {
176
+ const list = [converterDpi(start)];
177
+ for (const offset of deltaFormatter(delta, count - 1)) {
178
+ start += offset;
179
+ list.push(converterDpi(start));
233
180
  }
234
- }
181
+ return list;
182
+ };
183
+ // One SVG text run per TextCode preserves shaping and avoids one DOM
184
+ // node per glyph. Position lists honor explicit advances (including
185
+ // spaces); omitted advances retain the browser's natural text advance.
186
+ runs.push({
187
+ x: converterDpi(x), y: converterDpi(y), text,
188
+ xPositions: positions(x, code['@_DeltaX']),
189
+ yPositions: positions(y, code['@_DeltaY']),
190
+ });
235
191
  }
236
- return textCodePointList;
192
+ return runs;
237
193
  }
238
194
 
239
195
  export const replaceFirstSlash = function (str) {
@@ -33,6 +33,38 @@ const HexApi = unwrapDefault(Hex);
33
33
  const Base64Api = unwrapDefault(Base64);
34
34
  const ASN1Api = unwrapDefault(ASN1);
35
35
 
36
+ // SignedData is not an SES seal. In particular, tax invoices use the GM/T
37
+ // ContentInfo OID and draw their visible stamp as an ordinary page resource.
38
+ const signedDataOids = new Set(['1.2.840.113549.1.7.2', '1.2.156.10197.6.1.4.2.2']);
39
+ const universal = (node, number) => node?.tag?.tagClass === 0 && node.tag.tagNumber === number;
40
+ const parseSignedData = function (node) {
41
+ if (!universal(node?.sub?.[0], 6)) return null;
42
+ const oidNode = node.sub[0];
43
+ const start = oidNode.stream.pos + oidNode.header;
44
+ const oid = oidNode.stream.parseOID(start, start + oidNode.length).split('\n', 1)[0];
45
+ if (!signedDataOids.has(oid)) throw new Error('Unsupported signature ContentInfo type');
46
+ const wrapper = node.sub[1];
47
+ const signed = wrapper?.sub?.[0];
48
+ if (!universal(node, 16) || node.sub.length !== 2 ||
49
+ wrapper?.tag?.tagClass !== 2 || wrapper.tag.tagNumber !== 0 ||
50
+ !wrapper.tag.tagConstructed || wrapper.sub.length !== 1 ||
51
+ !universal(signed, 16) || signed.sub.length < 4 ||
52
+ !universal(signed.sub[0], 2) || !universal(signed.sub[1], 17) ||
53
+ !universal(signed.sub[2], 16) || !universal(signed.sub[2]?.sub?.[0], 6) ||
54
+ !universal(signed.sub.at(-1), 17)) {
55
+ throw new Error('Malformed SignedData structure');
56
+ }
57
+ return {
58
+ type: 'signed-data',
59
+ verificationStatus: 'not-verified',
60
+ verifyRet: null,
61
+ SES_Signature: {
62
+ format: 'signed-data', contentType: oid, displayOnly: true,
63
+ verificationStatus: 'not-verified',
64
+ },
65
+ };
66
+ };
67
+
36
68
  export const parseSesSignature = async function (zip, name, getEntry) {
37
69
  const entry = typeof getEntry === 'function' ? getEntry(zip, name) : zip.files[name];
38
70
  if (!entry) {
@@ -60,7 +92,10 @@ const decodeText = function (val) {
60
92
  const decode = function (der, offset) {
61
93
  offset = offset || 0;
62
94
  try {
63
- const SES_Signature = decodeSES_Signature(der, offset);
95
+ const root = ASN1Api.decode(der, offset);
96
+ const signedData = parseSignedData(root);
97
+ if (signedData) return signedData;
98
+ const SES_Signature = decodeSES_Signature(root);
64
99
  const picture = SES_Signature?.toSign?.eseal?.esealInfo?.picture;
65
100
  if (!picture?.data?.byte?.length) {
66
101
  return {};
@@ -73,12 +108,15 @@ const decode = function (der, offset) {
73
108
  SES_Signature: {
74
109
  realVersion: SES_Signature.realVersion,
75
110
  displayOnly: true,
111
+ verificationStatus: 'not-verified',
76
112
  pictureType: type,
77
113
  pictureWidth: picture.width,
78
114
  pictureHeight: picture.height,
79
115
  sealName: SES_Signature.toSign?.eseal?.esealInfo?.property?.name,
80
116
  },
81
- verifyRet: true,
117
+ // Displaying an image is not cryptographic signature verification.
118
+ verifyRet: null,
119
+ verificationStatus: 'not-verified',
82
120
  };
83
121
  } catch (e) {
84
122
  console.warn('[ofd] decode SES signature failed', e);
@@ -122,9 +160,7 @@ const parseTimeNode = function (node, utc) {
122
160
  return decodeUTCTime(raw);
123
161
  };
124
162
 
125
- const decodeSES_Signature = function (der, offset) {
126
- offset = offset || 0;
127
- const asn1 = ASN1Api.decode(der, offset);
163
+ const decodeSES_Signature = function (asn1) {
128
164
  let SES_Signature;
129
165
  try {
130
166
  // V1