@file-viewer/renderer-ofd 3.0.2 → 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
|
+
"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,10 +54,11 @@
|
|
|
54
54
|
"LICENSE"
|
|
55
55
|
],
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@file-viewer/core": "3.0
|
|
58
|
-
"jszip": "
|
|
57
|
+
"@file-viewer/core": "3.1.0",
|
|
58
|
+
"jszip": "3.10.2"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
|
+
"@xmldom/xmldom": "0.9.12",
|
|
61
62
|
"linkedom": "^0.18.12",
|
|
62
63
|
"typescript": "^6.0.3"
|
|
63
64
|
},
|
|
@@ -72,6 +73,7 @@
|
|
|
72
73
|
"verify:image-placement": "node scripts/verify-image-placement.mjs",
|
|
73
74
|
"verify:pageblock-render": "node scripts/verify-pageblock-render.mjs",
|
|
74
75
|
"verify:render-fidelity": "node scripts/verify-render-fidelity.mjs",
|
|
75
|
-
"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"
|
|
76
78
|
}
|
|
77
79
|
}
|
|
@@ -43,6 +43,12 @@ const appendChildValue = function (target, key, value) {
|
|
|
43
43
|
target[key] = value;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
const xmlElementName = function (element) {
|
|
47
|
+
return element.namespaceURI === 'http://www.ofdspec.org/2016'
|
|
48
|
+
? `ofd:${element.localName}`
|
|
49
|
+
: element.nodeName;
|
|
50
|
+
}
|
|
51
|
+
|
|
46
52
|
const parseXmlElement = function (element, options, order) {
|
|
47
53
|
const attrPrefix = options.attributeNamePrefix ?? '@_';
|
|
48
54
|
const result = {};
|
|
@@ -64,7 +70,7 @@ const parseXmlElement = function (element, options, order) {
|
|
|
64
70
|
if (parsedChild && typeof parsedChild === 'object' && pfIndex !== undefined) {
|
|
65
71
|
parsedChild.pfIndex = pfIndex;
|
|
66
72
|
}
|
|
67
|
-
appendChildValue(result, child
|
|
73
|
+
appendChildValue(result, xmlElementName(child), parsedChild);
|
|
68
74
|
continue;
|
|
69
75
|
}
|
|
70
76
|
if (child.nodeType === 3 || child.nodeType === 4) {
|
|
@@ -72,7 +78,10 @@ const parseXmlElement = function (element, options, order) {
|
|
|
72
78
|
}
|
|
73
79
|
}
|
|
74
80
|
|
|
75
|
-
|
|
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();
|
|
76
85
|
if (text) {
|
|
77
86
|
if (Object.keys(result).length === 0) {
|
|
78
87
|
return text;
|
|
@@ -103,7 +112,7 @@ const parseXmlToJson = function (xmlData, options = {}) {
|
|
|
103
112
|
return {};
|
|
104
113
|
}
|
|
105
114
|
return {
|
|
106
|
-
[root
|
|
115
|
+
[xmlElementName(root)]: parseXmlElement(root, options, { next: 0 })
|
|
107
116
|
};
|
|
108
117
|
}
|
|
109
118
|
|
|
@@ -159,18 +168,13 @@ const joinZipPath = function (...parts) {
|
|
|
159
168
|
}
|
|
160
169
|
|
|
161
170
|
const startsWithZipRoot = function (path, root) {
|
|
162
|
-
const normalizedPath = normalizeZipPath(path);
|
|
163
|
-
const normalizedRoot = normalizeZipPath(root);
|
|
171
|
+
const normalizedPath = normalizeZipPath(path).toLowerCase();
|
|
172
|
+
const normalizedRoot = normalizeZipPath(root).toLowerCase();
|
|
164
173
|
return normalizedPath === normalizedRoot || normalizedPath.startsWith(`${normalizedRoot}/`);
|
|
165
174
|
}
|
|
166
175
|
|
|
167
176
|
const getZipEntry = function (zip, candidates) {
|
|
168
177
|
const paths = asArray(candidates).map(normalizeZipPath).filter(Boolean);
|
|
169
|
-
for (const path of paths) {
|
|
170
|
-
if (zip.files[path]) {
|
|
171
|
-
return zip.files[path];
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
178
|
let lowerMap = zipEntryMapCache.get(zip);
|
|
175
179
|
if (!lowerMap) {
|
|
176
180
|
lowerMap = new Map();
|
|
@@ -180,7 +184,7 @@ const getZipEntry = function (zip, candidates) {
|
|
|
180
184
|
zipEntryMapCache.set(zip, lowerMap);
|
|
181
185
|
}
|
|
182
186
|
for (const path of paths) {
|
|
183
|
-
const entry = lowerMap.get(path.toLowerCase());
|
|
187
|
+
const entry = zip.files[path] || lowerMap.get(path.toLowerCase());
|
|
184
188
|
if (entry) {
|
|
185
189
|
return entry;
|
|
186
190
|
}
|
|
@@ -190,15 +194,22 @@ const getZipEntry = function (zip, candidates) {
|
|
|
190
194
|
|
|
191
195
|
const resolveDocumentPath = function (path, doc) {
|
|
192
196
|
const normalizedPath = normalizeZipPath(path);
|
|
193
|
-
if (!normalizedPath || startsWithZipRoot(normalizedPath, doc)) {
|
|
197
|
+
if (!normalizedPath || !doc || /^[\\/]/.test(String(path).trim()) || startsWithZipRoot(normalizedPath, doc)) {
|
|
194
198
|
return normalizedPath;
|
|
195
199
|
}
|
|
196
|
-
return joinZipPath(doc,
|
|
200
|
+
return joinZipPath(doc, path);
|
|
197
201
|
}
|
|
198
202
|
|
|
199
|
-
const resolveResourceCandidates = function (file, baseLoc, doc) {
|
|
203
|
+
const resolveResourceCandidates = function (file, baseLoc, doc, resourcePath) {
|
|
200
204
|
const mediaFile = normalizeZipPath(file);
|
|
201
205
|
const base = normalizeZipPath(baseLoc);
|
|
206
|
+
const rawFile = String(file || '').replace(/\\/g, '/').trim();
|
|
207
|
+
const rawBase = String(baseLoc || '').replace(/\\/g, '/').trim();
|
|
208
|
+
const resourceDirectory = normalizeZipPath(resourcePath).split('/').slice(0, -1).join('/');
|
|
209
|
+
const resourceBase = rawBase.startsWith('/') ? normalizeZipPath(rawBase) : joinZipPath(resourceDirectory, rawBase);
|
|
210
|
+
// Resolve relative components before normalizing '..'. Declared locations
|
|
211
|
+
// outrank compatibility fallbacks, including their case-insensitive matches.
|
|
212
|
+
const declaredPath = rawFile.startsWith('/') ? mediaFile : joinZipPath(resourceBase, rawFile);
|
|
202
213
|
const candidates = [mediaFile];
|
|
203
214
|
|
|
204
215
|
if (base && !startsWithZipRoot(mediaFile, base)) {
|
|
@@ -213,7 +224,7 @@ const resolveResourceCandidates = function (file, baseLoc, doc) {
|
|
|
213
224
|
}
|
|
214
225
|
}
|
|
215
226
|
|
|
216
|
-
return [...new Set(docCandidates.filter(Boolean))];
|
|
227
|
+
return [...new Set([declaredPath, ...docCandidates].filter(Boolean))];
|
|
217
228
|
}
|
|
218
229
|
|
|
219
230
|
const getImageMime = function (format) {
|
|
@@ -295,7 +306,7 @@ const sealImageMime = function (type) {
|
|
|
295
306
|
export const doGetDocRoot = async function (zip, docbody) {
|
|
296
307
|
let docRoot = docbody['ofd:DocRoot'];
|
|
297
308
|
docRoot = normalizeZipPath(replaceFirstSlash(docRoot));
|
|
298
|
-
const doc = docRoot.split('/')
|
|
309
|
+
const doc = docRoot.split('/').slice(0, -1).join('/');
|
|
299
310
|
const signatures = docbody['ofd:Signatures'];
|
|
300
311
|
const stampAnnot = await getSignature(zip, signatures, doc);
|
|
301
312
|
let stampAnnotArray = {};
|
|
@@ -422,7 +433,7 @@ export const getDocumentRes = async function ([zip, doc, Document, stampAnnot, a
|
|
|
422
433
|
const documentResObj = data['json']['ofd:Res'];
|
|
423
434
|
fontResObj = await getFont(documentResObj);
|
|
424
435
|
drawParamResObj = await getDrawParam(documentResObj);
|
|
425
|
-
multiMediaResObj = await getMultiMediaRes(zip, documentResObj, doc);
|
|
436
|
+
multiMediaResObj = await getMultiMediaRes(zip, documentResObj, doc, documentResPath);
|
|
426
437
|
}
|
|
427
438
|
}
|
|
428
439
|
return [zip, doc, Document, stampAnnot, annotationObjs, fontResObj, drawParamResObj, multiMediaResObj];
|
|
@@ -439,7 +450,7 @@ export const getPublicRes = async function ([zip, doc, Document, stampAnnot, ann
|
|
|
439
450
|
fontResObj = Object.assign(fontResObj, fontObj);
|
|
440
451
|
let drawParamObj = await getDrawParam(publicResObj);
|
|
441
452
|
drawParamResObj = Object.assign(drawParamResObj, drawParamObj);
|
|
442
|
-
let multiMediaObj = await getMultiMediaRes(zip, publicResObj, doc);
|
|
453
|
+
let multiMediaObj = await getMultiMediaRes(zip, publicResObj, doc, publicResPath);
|
|
443
454
|
multiMediaResObj = Object.assign(multiMediaResObj, multiMediaObj);
|
|
444
455
|
}
|
|
445
456
|
}
|
|
@@ -529,14 +540,14 @@ const getDrawParam = async function (res) {
|
|
|
529
540
|
return drawParamResObj;
|
|
530
541
|
}
|
|
531
542
|
|
|
532
|
-
const getMultiMediaRes = async function (zip, res, doc) {
|
|
543
|
+
const getMultiMediaRes = async function (zip, res, doc, resourcePath) {
|
|
533
544
|
const multiMedias = res['ofd:MultiMedias'];
|
|
534
545
|
let multiMediaResObj = {};
|
|
535
546
|
if (multiMedias) {
|
|
536
547
|
let array = collectGroupedItems(multiMedias, 'ofd:MultiMedia');
|
|
537
548
|
for (const item of array) {
|
|
538
549
|
if (item && item['ofd:MediaFile']) {
|
|
539
|
-
const candidates = resolveResourceCandidates(item['ofd:MediaFile'], res['@_BaseLoc'], doc);
|
|
550
|
+
const candidates = resolveResourceCandidates(item['ofd:MediaFile'], res['@_BaseLoc'], doc, resourcePath);
|
|
540
551
|
const file = getZipEntry(zip, candidates) ? candidates : null;
|
|
541
552
|
const type = item['@_Type'] ? item['@_Type'].toLowerCase() : '';
|
|
542
553
|
if (type === 'image') {
|
|
@@ -643,6 +654,7 @@ const getSignatureData = async function (zip, signature, signatureID) {
|
|
|
643
654
|
'signedInfo': {
|
|
644
655
|
'signatureID': signatureID,
|
|
645
656
|
'VerifyRet': sealObj.verifyRet,
|
|
657
|
+
'VerificationStatus': sealObj.verificationStatus || 'not-verified',
|
|
646
658
|
'Provider': signedInfoNode['ofd:Provider'],
|
|
647
659
|
'SignatureMethod': signedInfoNode['ofd:SignatureMethod'],
|
|
648
660
|
'SignatureDateTime': signedInfoNode['ofd:SignatureDateTime'],
|
|
@@ -663,44 +675,32 @@ const getSealDocumentObj = function (stampAnnot) {
|
|
|
663
675
|
}
|
|
664
676
|
|
|
665
677
|
const getJsonFromXmlContent = async function (zip, xmlName) {
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
}, function error(e) {
|
|
684
|
-
reject(e);
|
|
685
|
-
})
|
|
686
|
-
});
|
|
678
|
+
const entry = getZipEntry(zip, xmlName);
|
|
679
|
+
if (!entry) {
|
|
680
|
+
throw new Error(`OFD XML resource not found: ${normalizeZipPath(xmlName)}`);
|
|
681
|
+
}
|
|
682
|
+
const content = await entry.async('string');
|
|
683
|
+
ensureBrowserGlobal();
|
|
684
|
+
try {
|
|
685
|
+
const json = parseXmlToJson(content, {
|
|
686
|
+
attributeNamePrefix: "@_",
|
|
687
|
+
ignoreAttributes: false,
|
|
688
|
+
parseNodeValue: false,
|
|
689
|
+
trimValues: false
|
|
690
|
+
});
|
|
691
|
+
return {xml: content, json};
|
|
692
|
+
} catch (error) {
|
|
693
|
+
throw new Error(`OFD XML parse failed (${normalizeZipPath(xmlName)}): ${error.message}`);
|
|
694
|
+
}
|
|
687
695
|
}
|
|
688
696
|
|
|
689
697
|
const parseJbig2ImageFromZip = async function (zip, name) {
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
entry.async('uint8array').then(function (bytes) {
|
|
697
|
-
let jbig2 = new Jbig2Image();
|
|
698
|
-
const img = jbig2.parse(bytes);
|
|
699
|
-
resolve({img, width: jbig2.width, height: jbig2.height, format: 'gbig2'});
|
|
700
|
-
}, function error(e) {
|
|
701
|
-
reject(e);
|
|
702
|
-
})
|
|
703
|
-
});
|
|
698
|
+
const entry = getZipEntry(zip, name);
|
|
699
|
+
if (!entry) return null;
|
|
700
|
+
const bytes = await entry.async('uint8array');
|
|
701
|
+
const jbig2 = new Jbig2Image();
|
|
702
|
+
const img = jbig2.parse(bytes);
|
|
703
|
+
return {img, width: jbig2.width, height: jbig2.height, format: 'gbig2'};
|
|
704
704
|
}
|
|
705
705
|
|
|
706
706
|
const parseOtherImageFromZip = async function (zip, name, mime = 'image/png') {
|
|
@@ -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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
|
192
|
+
return runs;
|
|
237
193
|
}
|
|
238
194
|
|
|
239
195
|
export const replaceFirstSlash = function (str) {
|
|
@@ -311,6 +267,9 @@ let FONT_FAMILY = {
|
|
|
311
267
|
};
|
|
312
268
|
|
|
313
269
|
export const getFontFamily = function (font) {
|
|
270
|
+
if (typeof font !== 'string' || !font.trim()) {
|
|
271
|
+
return 'sans-serif';
|
|
272
|
+
}
|
|
314
273
|
if (FONT_FAMILY[font.toLowerCase()]) {
|
|
315
274
|
font = FONT_FAMILY[font.toLowerCase()];
|
|
316
275
|
}
|
|
@@ -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
|
|
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
|
-
|
|
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 (
|
|
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
|