@postermywall/fabricjs-2 7.1.0-pmw-69 → 7.4.0-pmw-71

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.
Files changed (60) hide show
  1. package/.vitest-attachments/b895abca787d674e9796cc7d12648978cac129d5.png +0 -0
  2. package/.vitest-attachments/babbd13e5ebd8c87d4f680401cd695e6d9726cc4.png +0 -0
  3. package/.vitest-attachments/cf8464e8a89ae803d8259877182f41cb2ace5cbb.png +0 -0
  4. package/.vitest-attachments/d76689b557e9557f6d3c9a7f0763e94eb8eceb76.png +0 -0
  5. package/CHANGELOG.md +2 -0
  6. package/dist/index.js +92 -24
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.min.js +26 -26
  9. package/dist/index.min.js.map +1 -1
  10. package/dist/index.min.mjs +26 -26
  11. package/dist/index.min.mjs.map +1 -1
  12. package/dist/index.mjs +92 -24
  13. package/dist/index.mjs.map +1 -1
  14. package/dist/index.node.cjs +90 -23
  15. package/dist/index.node.cjs.map +1 -1
  16. package/dist/index.node.mjs +90 -23
  17. package/dist/index.node.mjs.map +1 -1
  18. package/dist/package.min.mjs +1 -1
  19. package/dist/package.mjs +1 -1
  20. package/dist/src/parser/normalizeValue.d.ts.map +1 -1
  21. package/dist/src/parser/normalizeValue.min.mjs +1 -1
  22. package/dist/src/parser/normalizeValue.min.mjs.map +1 -1
  23. package/dist/src/parser/normalizeValue.mjs +4 -2
  24. package/dist/src/parser/normalizeValue.mjs.map +1 -1
  25. package/dist/src/shapes/Text/Text.d.ts +26 -2
  26. package/dist/src/shapes/Text/Text.d.ts.map +1 -1
  27. package/dist/src/shapes/Text/Text.min.mjs +2 -2
  28. package/dist/src/shapes/Text/Text.min.mjs.map +1 -1
  29. package/dist/src/shapes/Text/Text.mjs +51 -20
  30. package/dist/src/shapes/Text/Text.mjs.map +1 -1
  31. package/dist/src/shapes/Text/TextSVGExportMixin.d.ts +12 -0
  32. package/dist/src/shapes/Text/TextSVGExportMixin.d.ts.map +1 -1
  33. package/dist/src/shapes/Text/TextSVGExportMixin.min.mjs +2 -2
  34. package/dist/src/shapes/Text/TextSVGExportMixin.min.mjs.map +1 -1
  35. package/dist/src/shapes/Text/TextSVGExportMixin.mjs +37 -1
  36. package/dist/src/shapes/Text/TextSVGExportMixin.mjs.map +1 -1
  37. package/dist-extensions/src/parser/normalizeValue.d.ts.map +1 -1
  38. package/dist-extensions/src/shapes/Text/Text.d.ts +26 -2
  39. package/dist-extensions/src/shapes/Text/Text.d.ts.map +1 -1
  40. package/dist-extensions/src/shapes/Text/TextSVGExportMixin.d.ts +12 -0
  41. package/dist-extensions/src/shapes/Text/TextSVGExportMixin.d.ts.map +1 -1
  42. package/package.json +1 -1
  43. package/src/__screenshots__/__tmp_real_svg.spec.ts/real-Office-exported-SVG-from-order-42675801-parses--measures-and-renders-its-clipped-text-without-throwing-1.png +0 -0
  44. package/src/parser/loadSVGFromString.test.ts +26 -0
  45. package/src/parser/normalizeValue.ts +6 -0
  46. package/src/shapes/Text/Text.ts +94 -38
  47. package/src/shapes/Text/TextSVGExportMixin.ts +62 -1
  48. package/src/shapes/Text/__screenshots__/TextSVGExportMixin.spec.ts/TextSvgExport-toSVG-toSVG-1.png +0 -0
  49. package/src/shapes/Text/__screenshots__/TextSVGExportMixin.spec.ts/TextSvgExport-toSVG-toSVG-justified-1.png +0 -0
  50. package/src/shapes/Text/__screenshots__/TextSVGExportMixin.spec.ts/TextSvgExport-toSVG-toSVG-with-deltaY-1.png +0 -0
  51. package/src/shapes/Text/__screenshots__/TextSVGExportMixin.spec.ts/TextSvgExport-toSVG-toSVG-with-font-1.png +0 -0
  52. package/src/shapes/Text/__screenshots__/TextSVGExportMixin.spec.ts/TextSvgExport-toSVG-toSVG-with-font-and-origins-1.png +0 -0
  53. package/src/shapes/Text/__screenshots__/TextSVGExportMixin.spec.ts/TextSvgExport-toSVG-toSVG-with-multiple-spaces-1.png +0 -0
  54. package/src/shapes/Text/__screenshots__/TextSVGExportMixin.spec.ts/TextSvgExport-toSVG-toSVG-with-text-as-a-clipPath-1.png +0 -0
  55. package/src/shapes/Text/glyphOverhang.spec.ts +32 -0
  56. package/src/shapes/Text/svgListMarkers.spec.ts +116 -0
  57. /package/.vitest-attachments/{0b71b97b594519eb09336f957db6701e064f1e6b.png → 1629802dbea5d4ab7edf7e6f65edc8c84e892e6e.png} +0 -0
  58. /package/.vitest-attachments/{2f7310caa5815d4e4ed35964597101936fcb397f.png → 23857cd0ba8b7c015d07ea8d7ce12e83dd88818d.png} +0 -0
  59. /package/{src/shapes/Text/__screenshots__/glyphOverhang.spec.ts/glyph-overhang-widens-a-group-cache-that-caters-for-text-children-by-the-same-reach-1.png → .vitest-attachments/23b206d0e2c2cbf41ea4d8dd1b0758f9c3447d6f.png} +0 -0
  60. /package/{src/shapes/Text/__screenshots__/glyphOverhang.spec.ts/glyph-overhang-widens-the-text-cache-canvas-by-twice-the-reach-once-it-exceeds-the-font-size-floor-1.png → .vitest-attachments/a9d6ab42d2436605fb54e5a753e00e9a55fd6d98.png} +0 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  ## [next]
4
4
 
5
+ - fix(text): an SVG `letter-spacing: normal` (Office exports) reads as no extra spacing instead of the raw string, and a non-finite glyph reach can no longer size a text cache canvas to 0 and crash the render with `drawImage ... width or height of 0` (pmw-71)
6
+ - feat(text): export list markers as SVG spans where the canvas draws them, write the gradient definitions of per-character fills, and read a right-to-left line's glyph reach by each glyph's own ink (pmw-70)
5
7
  - feat(text): measure each glyph's ink reach and size the text and group caches from it, so swashes and descenders of any font are not cut (pmw-69)
6
8
  - Upgrade 7.4.0 [#4](https://github.com/fabricjs/fabric.js/pull/4)
7
9
 
package/dist/index.js CHANGED
@@ -452,7 +452,7 @@
452
452
  const cache = new Cache();
453
453
  //#endregion
454
454
  //#region src/constants.ts
455
- const VERSION = "7.1.0-pmw-69";
455
+ const VERSION = "7.4.0-pmw-71";
456
456
  function noop() {}
457
457
  const halfPI = Math.PI / 2;
458
458
  const quarterPI = Math.PI / 4;
@@ -8745,8 +8745,10 @@
8745
8745
  ouputValue = parseFloat(value);
8746
8746
  if (parentAttributes && typeof parentAttributes.opacity !== "undefined") ouputValue *= parentAttributes.opacity;
8747
8747
  } else if (attr === "textAnchor") ouputValue = value === "start" ? LEFT : value === "end" ? RIGHT : CENTER;
8748
- else if (attr === "charSpacing" || attr === "textDecorationThickness") parsed = parseUnit(value, fontSize) / fontSize * 1e3;
8749
- else if (attr === "paintFirst") {
8748
+ else if (attr === "charSpacing" || attr === "textDecorationThickness") {
8749
+ parsed = parseUnit(value, fontSize) / fontSize * 1e3;
8750
+ if (attr === "charSpacing" && !Number.isFinite(parsed)) parsed = 0;
8751
+ } else if (attr === "paintFirst") {
8750
8752
  const fillIndex = value.indexOf(FILL);
8751
8753
  const strokeIndex = value.indexOf(STROKE);
8752
8754
  ouputValue = FILL;
@@ -15862,7 +15864,26 @@
15862
15864
  var TextSVGExportMixin = class extends FabricObjectSVGExportMixin {
15863
15865
  _toSVG() {
15864
15866
  const offsets = this._getSVGLeftTopOffsets(), textAndBg = this._getSVGTextAndBg(offsets.textTop, offsets.textLeft);
15865
- return this._wrapSVGTextAndBg(textAndBg);
15867
+ return [...this._getStyledCharFillerMarkup(), ...this._wrapSVGTextAndBg(textAndBg)];
15868
+ }
15869
+ /**
15870
+ * *PMW* The gradient definitions of per-character fills. The object-level fill is written by
15871
+ * _createBaseSVGMarkup, but a styled character's gradient only ever appeared as a `url(#id)`
15872
+ * reference, so the text rendered black in any SVG consumer.
15873
+ */
15874
+ _getStyledCharFillerMarkup() {
15875
+ const seen = /* @__PURE__ */ new Set(), markup = [];
15876
+ Object.values(this.styles).forEach((line) => {
15877
+ Object.values(line).forEach((declaration) => {
15878
+ var _declaration$fill;
15879
+ const fill = (_declaration$fill = declaration.fill) !== null && _declaration$fill !== void 0 ? _declaration$fill : null;
15880
+ if (isFiller(fill) && !seen.has(String(fill.id))) {
15881
+ seen.add(String(fill.id));
15882
+ markup.push(fill.toSVG(this));
15883
+ }
15884
+ });
15885
+ });
15886
+ return markup;
15866
15887
  }
15867
15888
  toSVG(reviver) {
15868
15889
  const textSvg = this._createBaseSVGMarkup(this._toSVG(), {
@@ -15918,6 +15939,7 @@
15918
15939
  lineOffset = this._getLineLeftOffset(i);
15919
15940
  if (this.direction === "rtl") lineOffset += this.width;
15920
15941
  if (this.textBackgroundColor || this.styleHas("textBackgroundColor", i)) this._setSVGTextLineBg(textBgRects, i, textLeftOffset + lineOffset, height);
15942
+ this._setSVGListMarker(textSpans, i, textLeftOffset + lineOffset, height);
15921
15943
  this._setSVGTextLineText(textSpans, i, textLeftOffset + lineOffset, height);
15922
15944
  height += this.getHeightOfLine(i);
15923
15945
  }
@@ -15942,6 +15964,21 @@
15942
15964
  }
15943
15965
  return `<tspan x="${toFixed(left, numFractionDigit)}" y="${toFixed(top, numFractionDigit)}" ${dySpan}${angleAttr}${fillStyles}>${escapeXml(char)}</tspan>`;
15944
15966
  }
15967
+ /**
15968
+ * *PMW* The list marker of a visual row as its own span, placed exactly where the canvas
15969
+ * render draws it ({@link FabricText#_getListMarkerPlacement}): the row's text start is the
15970
+ * span's left offset and the row's bottom is its top offset plus the line height.
15971
+ */
15972
+ _setSVGListMarker(textSpans, lineIndex, textLeftOffset, textTopOffset) {
15973
+ const placed = this._getListMarkerPlacement(lineIndex, textLeftOffset, textTopOffset + this.getHeightOfLineImpl(lineIndex));
15974
+ if (!placed) return;
15975
+ const styleProps = this.getSvgSpanStyles({
15976
+ fontFamily: placed.fontFamily,
15977
+ fontSize: placed.fontSize,
15978
+ fill: placed.fill
15979
+ }, true);
15980
+ textSpans.push(`<tspan x="${toFixed(placed.left, config.NUM_FRACTION_DIGITS)}" y="${toFixed(placed.baseline, config.NUM_FRACTION_DIGITS)}" style="${styleProps}">${escapeXml(placed.marker)}</tspan>`);
15981
+ }
15945
15982
  _setSVGTextLineText(textSpans, lineIndex, textLeftOffset, textTopOffset) {
15946
15983
  const lineHeight = this.getHeightOfLine(lineIndex), isJustify = this.textAlign.includes(JUSTIFY), line = this._textLines[lineIndex];
15947
15984
  let actualStyle, nextStyle, charsToRender = "", charBox, style, boxWidth = 0, timeToRender;
@@ -16090,6 +16127,12 @@
16090
16127
  /** Peak rise = (period / 2) × this factor: 6.75 text units at the reference size. */
16091
16128
  const SQUIGGLY_LINE_WAVE_RISE_FACTOR = 5 * .45;
16092
16129
  /**
16130
+ * *PMW* `Math.max` over the finite candidates only. A NaN or infinite reach (a character
16131
+ * bound poisoned by bad input) must never size a cache canvas: `canvas.width = NaN` is 0 and
16132
+ * the next `drawImage` of that canvas throws.
16133
+ */
16134
+ const maxFinite = (current, ...candidates) => candidates.reduce((max, candidate) => Number.isFinite(candidate) ? Math.max(max, candidate) : max, current);
16135
+ /**
16093
16136
  * Text class
16094
16137
  * @see {@link http://fabric5.fabricjs.com/fabric-intro-part-2#text}
16095
16138
  */
@@ -16293,7 +16336,8 @@
16293
16336
  /**
16294
16337
  * *PMW* Walks every measured grapheme: horizontally, its ink left of the line start and
16295
16338
  * right of the box edge, both after the line's alignment offset (a right-to-left line lays
16296
- * its graphemes out mirrored, so it takes no offset credit and reads conservatively);
16339
+ * its graphemes out mirrored, so it counts each glyph's reach past its own advance box
16340
+ * instead, with no alignment credit, and reads conservatively);
16297
16341
  * vertically, the first line's ink above its baseline and the last line's ink below the
16298
16342
  * box bottom. A stroke paints half its width outside the outline on every side.
16299
16343
  * @private
@@ -16307,20 +16351,20 @@
16307
16351
  let horizontal = 0, vertical = 0, lineTop = 0;
16308
16352
  for (let lineIndex = 0; lineIndex < lineCount; lineIndex++) {
16309
16353
  this.getLineWidth(lineIndex);
16310
- const bounds = this.__charBounds[lineIndex], isRtl = this.direction === "rtl", leftOffset = isRtl ? 0 : this._getLineLeftOffset(lineIndex), rightEdge = isRtl ? this.getLineWidth(lineIndex) : this.width - leftOffset, baselineFromTop = lineTop + this.getHeightOfLineImpl(lineIndex) * (1 - this._fontSizeFraction), roomBelowBaseline = this.height - baselineFromTop;
16354
+ const bounds = this.__charBounds[lineIndex], isRtl = this.direction === "rtl", leftOffset = this._getLineLeftOffset(lineIndex), rightEdge = this.width - leftOffset, baselineFromTop = lineTop + this.getHeightOfLineImpl(lineIndex) * (1 - this._fontSizeFraction), roomBelowBaseline = this.height - baselineFromTop;
16311
16355
  for (let charIndex = 0; bounds && charIndex < this._textLines[lineIndex].length; charIndex++) {
16312
16356
  const box = bounds[charIndex], ink = box.ink;
16313
16357
  if (!ink) continue;
16314
- horizontal = Math.max(horizontal, ink.left - box.left - leftOffset, box.left + ink.right - rightEdge);
16315
- if (lineIndex === 0) vertical = Math.max(vertical, ink.ascent - box.deltaY - baselineFromTop);
16316
- if (lineIndex === lineCount - 1) vertical = Math.max(vertical, ink.descent + box.deltaY - roomBelowBaseline);
16358
+ horizontal = isRtl ? maxFinite(horizontal, ink.left, ink.right - box.width) : maxFinite(horizontal, ink.left - box.left - leftOffset, box.left + ink.right - rightEdge);
16359
+ if (lineIndex === 0) vertical = maxFinite(vertical, ink.ascent - box.deltaY - baselineFromTop);
16360
+ if (lineIndex === lineCount - 1) vertical = maxFinite(vertical, ink.descent + box.deltaY - roomBelowBaseline);
16317
16361
  }
16318
16362
  lineTop += this.getHeightOfLine(lineIndex);
16319
16363
  }
16320
16364
  const strokeReach = this.stroke ? this.strokeWidth / 2 : 0;
16321
16365
  return {
16322
- horizontal: Math.max(0, horizontal + strokeReach),
16323
- vertical: Math.max(0, vertical + strokeReach)
16366
+ horizontal: maxFinite(0, horizontal + strokeReach),
16367
+ vertical: maxFinite(0, vertical + strokeReach)
16324
16368
  };
16325
16369
  }
16326
16370
  /**
@@ -16651,6 +16695,7 @@
16651
16695
  * @param {Number} lineIndex index of line to calculate
16652
16696
  * @return {Number}
16653
16697
  */
16698
+ /** *PMW* also read by the SVG export mixin, which is a separate class */
16654
16699
  getHeightOfLineImpl(lineIndex) {
16655
16700
  const lh = this.__lineHeights;
16656
16701
  if (lh[lineIndex]) return lh[lineIndex];
@@ -17056,27 +17101,50 @@
17056
17101
  * @private
17057
17102
  */
17058
17103
  _renderListMarker(ctx, visualLineIndex, lineDrawLeft, lineDrawTop) {
17059
- var _this$lineStyles$real2, _this$lineStyles$real3;
17060
- if (!this._isStartOfRealLine(visualLineIndex)) return;
17061
- const realLineIndex = this._getRealLineIndex(visualLineIndex);
17062
- const marker = (_this$lineStyles$real2 = this.lineStyles[realLineIndex]) === null || _this$lineStyles$real2 === void 0 ? void 0 : _this$lineStyles$real2.listMarker;
17063
- if (!marker) return;
17064
- const { fontSize, fontFamily, fill } = this._getListMarkerStyle(realLineIndex);
17065
- const gap = LIST_MARKER_GAP_FACTOR * fontSize;
17066
- const markerLeft = this.direction === "rtl" ? lineDrawLeft + gap : lineDrawLeft - gap - this._measureListMarkerWidth(realLineIndex);
17104
+ const placed = this._getListMarkerPlacement(visualLineIndex, lineDrawLeft, lineDrawTop);
17105
+ if (!placed) return;
17067
17106
  ctx.save();
17068
17107
  ctx.direction = "ltr";
17069
17108
  ctx.textAlign = LEFT;
17070
17109
  ctx.font = this._getFontDeclaration({
17071
- fontSize,
17072
- fontFamily
17110
+ fontSize: placed.fontSize,
17111
+ fontFamily: placed.fontFamily
17073
17112
  });
17074
- ctx.fillStyle = fill;
17075
- const unorderedMarkerRaise = ((_this$lineStyles$real3 = this.lineStyles[realLineIndex]) === null || _this$lineStyles$real3 === void 0 ? void 0 : _this$lineStyles$real3.listMarkerFontFamily) ? fontSize / UNORDERED_MARKER_RAISE_FACTOR : 0;
17076
- ctx.fillText(String(marker), markerLeft, lineDrawTop - this.getHeightOfLineImpl(visualLineIndex) * this._fontSizeFraction - unorderedMarkerRaise);
17113
+ ctx.fillStyle = placed.fill;
17114
+ ctx.fillText(placed.marker, placed.left, placed.baseline);
17077
17115
  ctx.restore();
17078
17116
  }
17079
17117
  /**
17118
+ * *PMW* Where a visual row's list marker is drawn, in text units, given the row's own draw
17119
+ * anchors (`lineDrawLeft` is the row's text start, `lineDrawTop` its bottom). Shared by the
17120
+ * canvas render and the SVG export so both put the marker in the same place. Null when the
17121
+ * row does not start a listed real line.
17122
+ *
17123
+ * LTR: right-aligned in the LEFT gutter (like the legacy bullets textbox) — the marker's
17124
+ * right edge sits a fixed gap from the text column, so ordinal periods line up and digits
17125
+ * grow left. RTL mirrors: the gutter (and the marker) sit RIGHT of the text, whose draw
17126
+ * anchor is its right edge there. An unordered glyph is raised a little above the baseline.
17127
+ * @private
17128
+ */
17129
+ _getListMarkerPlacement(visualLineIndex, lineDrawLeft, lineDrawTop) {
17130
+ var _this$lineStyles$real2, _this$lineStyles$real3;
17131
+ if (!this._isStartOfRealLine(visualLineIndex)) return null;
17132
+ const realLineIndex = this._getRealLineIndex(visualLineIndex);
17133
+ const marker = (_this$lineStyles$real2 = this.lineStyles[realLineIndex]) === null || _this$lineStyles$real2 === void 0 ? void 0 : _this$lineStyles$real2.listMarker;
17134
+ if (!marker) return null;
17135
+ const { fontSize, fontFamily, fill } = this._getListMarkerStyle(realLineIndex);
17136
+ const gap = LIST_MARKER_GAP_FACTOR * fontSize;
17137
+ const unorderedMarkerRaise = ((_this$lineStyles$real3 = this.lineStyles[realLineIndex]) === null || _this$lineStyles$real3 === void 0 ? void 0 : _this$lineStyles$real3.listMarkerFontFamily) ? fontSize / UNORDERED_MARKER_RAISE_FACTOR : 0;
17138
+ return {
17139
+ marker: String(marker),
17140
+ fontSize,
17141
+ fontFamily,
17142
+ fill,
17143
+ left: this.direction === "rtl" ? lineDrawLeft + gap : lineDrawLeft - gap - this._measureListMarkerWidth(realLineIndex),
17144
+ baseline: lineDrawTop - this.getHeightOfLineImpl(visualLineIndex) * this._fontSizeFraction - unorderedMarkerRaise
17145
+ };
17146
+ }
17147
+ /**
17080
17148
  * *PMW* Sets or clears (align undefined) one real line's alignment override.
17081
17149
  */
17082
17150
  setLineAlignment(realLineIndex, align) {