@file-viewer/renderer-ofd 2.1.24 → 2.1.26

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": "2.1.24",
3
+ "version": "2.1.26",
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,7 +53,7 @@
53
53
  "LICENSE"
54
54
  ],
55
55
  "dependencies": {
56
- "@file-viewer/core": "2.1.24",
56
+ "@file-viewer/core": "2.1.26",
57
57
  "jszip": "^3.10.1"
58
58
  },
59
59
  "devDependencies": {
@@ -64,6 +64,8 @@
64
64
  "scripts": {
65
65
  "build": "tsc -b tsconfig.json",
66
66
  "type-check": "tsc -b tsconfig.json",
67
- "verify:github-94": "node scripts/verify-github-94.mjs"
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"
68
70
  }
69
71
  }
@@ -60,7 +60,7 @@ export const renderOfd = function (screenWidth, ofd) {
60
60
  const pageId = Object.keys(page)[0];
61
61
  let pageDiv = document.createElement('div');
62
62
  pageDiv.id = pageId;
63
- pageDiv.setAttribute('style', `margin-bottom: 20px;position: relative;width:${box.w}px;height:${box.h}px;background: white;`)
63
+ pageDiv.setAttribute('style', `margin-bottom: 20px;position: relative;width:${box.w}px;height:${box.h}px;background: white;overflow:hidden;`)
64
64
  renderPage(pageDiv, page, ofd.tpls, ofd.fontResObj, ofd.drawParamResObj, ofd.multiMediaResObj);
65
65
  divArray.push(pageDiv);
66
66
  }
@@ -77,7 +77,7 @@ export const renderOfdByScale = function (ofd) {
77
77
  const pageId = Object.keys(page)[0];
78
78
  let pageDiv = document.createElement('div');
79
79
  pageDiv.id = pageId;
80
- pageDiv.setAttribute('style', `margin-bottom: 20px;position: relative;width:${box.w}px;height:${box.h}px;background: white;`)
80
+ pageDiv.setAttribute('style', `margin-bottom: 20px;position: relative;width:${box.w}px;height:${box.h}px;background: white;overflow:hidden;`)
81
81
  renderPage(pageDiv, page, ofd.tpls, ofd.fontResObj, ofd.drawParamResObj, ofd.multiMediaResObj);
82
82
  divArray.push(pageDiv);
83
83
  }
@@ -43,7 +43,7 @@ const appendChildValue = function (target, key, value) {
43
43
  target[key] = value;
44
44
  }
45
45
 
46
- const parseXmlElement = function (element, options) {
46
+ const parseXmlElement = function (element, options, order) {
47
47
  const attrPrefix = options.attributeNamePrefix ?? '@_';
48
48
  const result = {};
49
49
  if (options.ignoreAttributes !== true) {
@@ -55,7 +55,16 @@ const parseXmlElement = function (element, options) {
55
55
  const textParts = [];
56
56
  for (const child of Array.from(element.childNodes || [])) {
57
57
  if (child.nodeType === 1) {
58
- appendChildValue(result, child.nodeName, parseXmlElement(child, options));
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);
59
68
  continue;
60
69
  }
61
70
  if (child.nodeType === 3 || child.nodeType === 4) {
@@ -94,7 +103,7 @@ const parseXmlToJson = function (xmlData, options = {}) {
94
103
  return {};
95
104
  }
96
105
  return {
97
- [root.nodeName]: parseXmlElement(root, options)
106
+ [root.nodeName]: parseXmlElement(root, options, { next: 0 })
98
107
  };
99
108
  }
100
109
 
@@ -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 = [];
@@ -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
- let fillColor = null;
239
- let strokeColor = null;
240
- let lineWith = converterDpi(0.353);
241
- let drawParam = layer?.['@_DrawParam'];
242
- if (drawParam && Object.keys(drawParamResObj).length > 0 && drawParamResObj[drawParam]) {
243
- if (drawParamResObj[drawParam]['relative']) {
244
- drawParam = drawParamResObj[drawParam]['relative'];
245
- if (drawParamResObj[drawParam]['FillColor']) {
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
- let boundary = parseStBox(imageObject['@_Boundary']);
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}`)
@@ -347,20 +418,19 @@ export const renderImageOnDiv = function (pageWidth, pageHeight, imgSrc, boundar
347
418
  img.setAttribute('width', '100%');
348
419
  img.setAttribute('height', '100%');
349
420
  div.appendChild(img);
350
- const pw = parseFloat(pageWidth.replace('px', ''));
351
- const ph = parseFloat(pageHeight.replace('px', ''));
352
- const w = boundary.w > pw ? pw : boundary.w;
353
- const h = boundary.h > ph ? ph : boundary.h;
354
421
  let c = '';
355
422
  if (clip) {
356
423
  clip = converterBox(clip);
357
424
  c = `clip: rect(${clip.y}px, ${clip.w + clip.x}px, ${clip.h + clip.y}px, ${clip.x}px)`
358
425
  }
359
- div.setAttribute('style', `cursor: pointer; overflow: hidden; position: absolute; left: ${c ? boundary.x : boundary.x < 0 ? 0 : boundary.x}px; top: ${c ? boundary.y : boundary.y < 0 ? 0 : boundary.y}px; width: ${w}px; height: ${h}px; ${c};z-index: ${oid}`)
426
+ // Preserve the stamp geometry declared by OFD. Oversized or partially
427
+ // off-page seals are clipped by the page container, matching OFD readers;
428
+ // shrinking or moving the seal here changes both its position and scale.
429
+ div.setAttribute('style', `cursor: pointer; overflow: hidden; position: absolute; left: ${boundary.x}px; top: ${boundary.y}px; width: ${boundary.w}px; height: ${boundary.h}px; ${c};z-index: ${oid}`)
360
430
  return div;
361
431
  }
362
432
 
363
- export const renderTextObject = function (fontResObj, textObject, defaultFillColor, defaultStrokeColor) {
433
+ export const renderTextObject = function (drawParamResObj, fontResObj, textObject, defaultFillColor, defaultStrokeColor) {
364
434
  let defaultFillOpacity = 1;
365
435
  let boundary = parseStBox(textObject['@_Boundary']);
366
436
  boundary = converterBox(boundary);
@@ -374,6 +444,12 @@ export const renderTextObject = function (fontResObj, textObject, defaultFillCol
374
444
  const textCodePointList = calTextPoint(array);
375
445
  let svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
376
446
  svg.setAttribute('version', '1.1');
447
+ const objectStyle = resolveDrawParamStyle(drawParamResObj, textObject['@_DrawParam'], {
448
+ fillColor: defaultFillColor,
449
+ strokeColor: defaultStrokeColor,
450
+ });
451
+ defaultFillColor = objectStyle.fillColor;
452
+ defaultStrokeColor = objectStyle.strokeColor;
377
453
  const fillColor = textObject['ofd:FillColor'];
378
454
  if (fillColor) {
379
455
  defaultFillColor = parseColor(fillColor['@_Value']);
@@ -412,6 +488,83 @@ export const renderTextObject = function (fontResObj, textObject, defaultFillCol
412
488
  return svg;
413
489
  }
414
490
 
491
+ const buildSvgPathData = function (points) {
492
+ let d = '';
493
+ for (const point of points) {
494
+ if (point.type === 'M') {
495
+ d += `M${point.x} ${point.y} `;
496
+ } else if (point.type === 'L') {
497
+ d += `L${point.x} ${point.y} `;
498
+ } else if (point.type === 'Q') {
499
+ d += `Q${point.x1} ${point.y1} ${point.x2} ${point.y2} `;
500
+ } else if (point.type === 'B') {
501
+ d += `C${point.x1} ${point.y1} ${point.x2} ${point.y2} ${point.x3} ${point.y3} `;
502
+ } else if (point.type === 'C') {
503
+ d += `Z`;
504
+ }
505
+ }
506
+ return d;
507
+ }
508
+
509
+ // OFD 的装饰性 PathObject(如色带、圆角背景)经常共用一个近乎整页的 Boundary,
510
+ // 真正可见的形状由 Clips 裁剪出来;不处理 Clips 会让这些路径以整块矩形铺满并
511
+ // 盖住下层图片/文字。Clips 下可以有多个 Clip(依次相交),每个 Clip 下可以有
512
+ // 多个 Area(在该 Clip 内取并集),语义上等价于 SVG 里逐层嵌套 clip-path。
513
+ const appendClipAreaPath = function (clipPathEl, area) {
514
+ const clipShape = area?.['ofd:Path'];
515
+ const abbreviatedData = clipShape?.['ofd:AbbreviatedData'];
516
+ if (!clipShape || !abbreviatedData) {
517
+ return;
518
+ }
519
+ const clipPoints = calPathPoint(convertPathAbbreviatedDatatoPoint(abbreviatedData));
520
+ const clipPathShape = document.createElementNS('http://www.w3.org/2000/svg', 'path');
521
+ clipPathShape.setAttribute('d', buildSvgPathData(clipPoints));
522
+ const clipCtm = clipShape['@_CTM'];
523
+ if (clipCtm) {
524
+ const ctms = parseCtm(clipCtm);
525
+ clipPathShape.setAttribute('transform', `matrix(${ctms[0]} ${ctms[1]} ${ctms[2]} ${ctms[3]} ${converterDpi(ctms[4])} ${converterDpi(ctms[5])})`);
526
+ }
527
+ clipPathEl.appendChild(clipPathShape);
528
+ }
529
+
530
+ const applyClips = function (svgRoot, contentEl, clips) {
531
+ const clipList = clips?.['ofd:Clip'];
532
+ if (!clipList) {
533
+ return contentEl;
534
+ }
535
+ let clipArray = [];
536
+ clipArray = clipArray.concat(clipList);
537
+ let defs = null;
538
+ let current = contentEl;
539
+ let clipIndex = 0;
540
+ for (const clip of clipArray) {
541
+ const areas = clip?.['ofd:Area'];
542
+ if (!clip || !areas) {
543
+ continue;
544
+ }
545
+ if (!defs) {
546
+ defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
547
+ svgRoot.appendChild(defs);
548
+ }
549
+ const clipPathEl = document.createElementNS('http://www.w3.org/2000/svg', 'clipPath');
550
+ const clipId = `ofd-clip-${clipIndex++}-${Math.random().toString(36).slice(2, 8)}`;
551
+ clipPathEl.setAttribute('id', clipId);
552
+ let areaArray = [];
553
+ areaArray = areaArray.concat(areas);
554
+ for (const area of areaArray) {
555
+ if (area) {
556
+ appendClipAreaPath(clipPathEl, area);
557
+ }
558
+ }
559
+ defs.appendChild(clipPathEl);
560
+ const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
561
+ group.setAttribute('clip-path', `url(#${clipId})`);
562
+ group.appendChild(current);
563
+ current = group;
564
+ }
565
+ return current;
566
+ }
567
+
415
568
  export const renderPathObject = function (drawParamResObj, pathObject, defaultFillColor, defaultStrokeColor, defaultLineWith, isStampAnnot) {
416
569
  let boundary = parseStBox(pathObject['@_Boundary']);
417
570
  boundary = converterBox(boundary);
@@ -427,11 +580,20 @@ export const renderPathObject = function (drawParamResObj, pathObject, defaultFi
427
580
  }
428
581
  const drawParam = pathObject['@_DrawParam'];
429
582
  if (drawParam) {
430
- lineWidth = drawParamResObj[drawParam].LineWidth;
583
+ lineWidth = drawParamResObj[drawParam]?.LineWidth;
431
584
  if (lineWidth) {
432
585
  defaultLineWith = converterDpi(lineWidth);
433
586
  }
434
587
  }
588
+ // PathObject 自身没有内联 ofd:FillColor/StrokeColor 时,颜色通常来自其引用的 DrawParam;
589
+ // 先按 DrawParam 解析出默认色,再让内联颜色(若存在)覆盖,否则文字/图形轮廓会因为
590
+ // 拿不到颜色而按 fill:none 处理,变成不可见。
591
+ const objectStyle = resolveDrawParamStyle(drawParamResObj, drawParam, {
592
+ fillColor: defaultFillColor,
593
+ strokeColor: defaultStrokeColor,
594
+ });
595
+ defaultFillColor = objectStyle.fillColor;
596
+ defaultStrokeColor = objectStyle.strokeColor;
435
597
  if (ctm) {
436
598
  const ctms = parseCtm(ctm);
437
599
  path.setAttribute('transform', `matrix(${ctms[0]} ${ctms[1]} ${ctms[2]} ${ctms[3]} ${converterDpi(ctms[4])} ${converterDpi(ctms[5])})`)
@@ -460,20 +622,8 @@ export const renderPathObject = function (drawParamResObj, pathObject, defaultFi
460
622
  fillStyle = `fill:${isStampAnnot ? 'none' : defaultFillColor ? defaultFillColor : 'none'};`;
461
623
  }
462
624
  path.setAttribute('style', `${strokeStyle};${fillStyle}`)
463
- let d = '';
464
- for (const point of points) {
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);
625
+ path.setAttribute('d', buildSvgPathData(points));
626
+ svg.appendChild(applyClips(svg, path, pathObject['ofd:Clips']));
477
627
  let width = isStampAnnot ? boundary.w : Math.ceil(boundary.w);
478
628
  let height = isStampAnnot ? boundary.h : Math.ceil(boundary.h);
479
629
  let left = boundary.x;
@@ -61,6 +61,19 @@ export const convertPathAbbreviatedDatatoPoint = abbreviatedData => {
61
61
  }
62
62
  i = i + 7;
63
63
  pointList.push(point);
64
+ } else if (array[i] === 'Q') {
65
+ // Q x1 y1 x2 y2:以(x1,y1)为控制点画二次贝塞尔到(x2,y2)。
66
+ // 大量中文字形轮廓(本身是路径而非 TextObject)都靠 Q 描述笔画弧线,
67
+ // 之前完全未识别,会导致后续 token 错位、字形整体丢失。
68
+ let point = {
69
+ 'type': 'Q',
70
+ 'x1': parseFloat(array[i + 1]),
71
+ 'y1': parseFloat(array[i + 2]),
72
+ 'x2': parseFloat(array[i + 3]),
73
+ 'y2': parseFloat(array[i + 4])
74
+ }
75
+ i = i + 5;
76
+ pointList.push(point);
64
77
  } else {
65
78
  i++;
66
79
  }
@@ -90,6 +103,13 @@ export const calPathPoint = function (abbreviatedPoint) {
90
103
  'x3': converterDpi(x3), 'y3': converterDpi(y3)
91
104
  }
92
105
  pointList.push(realPoint);
106
+ } else if (point.type === 'Q') {
107
+ let realPoint = {
108
+ 'type': 'Q',
109
+ 'x1': converterDpi(point.x1), 'y1': converterDpi(point.y1),
110
+ 'x2': converterDpi(point.x2), 'y2': converterDpi(point.y2)
111
+ }
112
+ pointList.push(realPoint);
93
113
  }
94
114
  }
95
115
  return pointList;