UNPKG

fabric

Version:

Object model for HTML5 canvas, and SVG-to-canvas parser. Backed by jsdom and node-canvas.

262 lines (252 loc) 10.5 kB
import { config } from '../../config.mjs'; import { escapeXml } from '../../util/lang_string.mjs'; import { colorPropToSVG, createSVGRect } from '../../util/misc/svgParsing.mjs'; import { hasStyleChanged } from '../../util/misc/textStyles.mjs'; import { toFixed } from '../../util/misc/toFixed.mjs'; import { FabricObjectSVGExportMixin } from '../Object/FabricObjectSVGExportMixin.mjs'; import { JUSTIFY } from './constants.mjs'; import { STROKE, FILL } from '../../constants.mjs'; import { createRotateMatrix } from '../../util/misc/matrix.mjs'; import { radiansToDegrees } from '../../util/misc/radiansDegreesConversion.mjs'; import { Point } from '../../Point.mjs'; import { matrixToSVG } from '../../util/misc/svgExport.mjs'; const multipleSpacesRegex = / +/g; const dblQuoteRegex = /"/g; function createSVGInlineRect(color, left, top, width, height) { return "\t\t".concat(createSVGRect(color, { left, top, width, height }), "\n"); } class TextSVGExportMixin extends FabricObjectSVGExportMixin { _toSVG() { const offsets = this._getSVGLeftTopOffsets(), textAndBg = this._getSVGTextAndBg(offsets.textTop, offsets.textLeft); return this._wrapSVGTextAndBg(textAndBg); } toSVG(reviver) { const textSvg = this._createBaseSVGMarkup(this._toSVG(), { reviver, noStyle: true, withShadow: true }), path = this.path; if (path) { return textSvg + path._createBaseSVGMarkup(path._toSVG(), { reviver, withShadow: true, additionalTransform: matrixToSVG(this.calcOwnMatrix()) }); } return textSvg; } _getSVGLeftTopOffsets() { return { textLeft: -this.width / 2, textTop: -this.height / 2, lineTop: this.getHeightOfLine(0) }; } _wrapSVGTextAndBg(_ref) { let { textBgRects, textSpans } = _ref; const noShadow = true, textDecoration = this.getSvgTextDecoration(this); return [textBgRects.join(''), '\t\t<text xml:space="preserve" ', "font-family=\"".concat(this.fontFamily.replace(dblQuoteRegex, "'"), "\" "), "font-size=\"".concat(this.fontSize, "\" "), this.fontStyle ? "font-style=\"".concat(this.fontStyle, "\" ") : '', this.fontWeight ? "font-weight=\"".concat(this.fontWeight, "\" ") : '', textDecoration ? "text-decoration=\"".concat(textDecoration, "\" ") : '', this.direction === 'rtl' ? "direction=\"".concat(this.direction, "\" ") : '', 'style="', this.getSvgStyles(noShadow), '"', this.addPaintOrder(), ' >', textSpans.join(''), '</text>\n']; } /** * @private * @param {Number} textTopOffset Text top offset * @param {Number} textLeftOffset Text left offset * @return {Object} */ _getSVGTextAndBg(textTopOffset, textLeftOffset) { const textSpans = [], textBgRects = []; let height = textTopOffset, lineOffset; // bounding-box background this.backgroundColor && textBgRects.push(...createSVGInlineRect(this.backgroundColor, -this.width / 2, -this.height / 2, this.width, this.height)); // text and text-background for (let i = 0, len = this._textLines.length; i < len; i++) { lineOffset = this._getLineLeftOffset(i); if (this.direction === 'rtl') { lineOffset += this.width; } if (this.textBackgroundColor || this.styleHas('textBackgroundColor', i)) { this._setSVGTextLineBg(textBgRects, i, textLeftOffset + lineOffset, height); } this._setSVGTextLineText(textSpans, i, textLeftOffset + lineOffset, height); height += this.getHeightOfLine(i); } return { textSpans, textBgRects }; } _createTextCharSpan(char, styleDecl, left, top, charBox) { const numFractionDigit = config.NUM_FRACTION_DIGITS; const styleProps = this.getSvgSpanStyles(styleDecl, char !== char.trim() || !!char.match(multipleSpacesRegex)), fillStyles = styleProps ? "style=\"".concat(styleProps, "\"") : '', dy = styleDecl.deltaY, dySpan = dy ? " dy=\"".concat(toFixed(dy, numFractionDigit), "\" ") : '', { angle, renderLeft, renderTop, width } = charBox; let angleAttr = ''; if (renderLeft !== undefined) { const wBy2 = width / 2; angle && (angleAttr = " rotate=\"".concat(toFixed(radiansToDegrees(angle), numFractionDigit), "\"")); const m = createRotateMatrix({ angle: radiansToDegrees(angle) }); m[4] = renderLeft; m[5] = renderTop; const renderPoint = new Point(-wBy2, 0).transform(m); left = renderPoint.x; top = renderPoint.y; } return "<tspan x=\"".concat(toFixed(left, numFractionDigit), "\" y=\"").concat(toFixed(top, numFractionDigit), "\" ").concat(dySpan).concat(angleAttr).concat(fillStyles, ">").concat(escapeXml(char), "</tspan>"); } _setSVGTextLineText(textSpans, lineIndex, textLeftOffset, textTopOffset) { const lineHeight = this.getHeightOfLine(lineIndex), isJustify = this.textAlign.includes(JUSTIFY), line = this._textLines[lineIndex]; let actualStyle, nextStyle, charsToRender = '', charBox, style, boxWidth = 0, timeToRender; textTopOffset += lineHeight * (1 - this._fontSizeFraction) / this.lineHeight; for (let i = 0, len = line.length - 1; i <= len; i++) { timeToRender = i === len || this.charSpacing || this.path; charsToRender += line[i]; charBox = this.__charBounds[lineIndex][i]; if (boxWidth === 0) { textLeftOffset += charBox.kernedWidth - charBox.width; boxWidth += charBox.width; } else { boxWidth += charBox.kernedWidth; } if (isJustify && !timeToRender) { if (this._reSpaceAndTab.test(line[i])) { timeToRender = true; } } if (!timeToRender) { // if we have charSpacing or a path, we render char by char actualStyle = actualStyle || this.getCompleteStyleDeclaration(lineIndex, i); nextStyle = this.getCompleteStyleDeclaration(lineIndex, i + 1); timeToRender = hasStyleChanged(actualStyle, nextStyle, true); } if (timeToRender) { style = this._getStyleDeclaration(lineIndex, i); textSpans.push(this._createTextCharSpan(charsToRender, style, textLeftOffset, textTopOffset, charBox)); charsToRender = ''; actualStyle = nextStyle; if (this.direction === 'rtl') { textLeftOffset -= boxWidth; } else { textLeftOffset += boxWidth; } boxWidth = 0; } } } _setSVGTextLineBg(textBgRects, i, leftOffset, textTopOffset) { const line = this._textLines[i], heightOfLine = this.getHeightOfLine(i) / this.lineHeight; let boxWidth = 0, boxStart = 0, currentColor, lastColor = this.getValueOfPropertyAt(i, 0, 'textBackgroundColor'); for (let j = 0; j < line.length; j++) { const { left, width, kernedWidth } = this.__charBounds[i][j]; currentColor = this.getValueOfPropertyAt(i, j, 'textBackgroundColor'); if (currentColor !== lastColor) { lastColor && textBgRects.push(...createSVGInlineRect(lastColor, leftOffset + boxStart, textTopOffset, boxWidth, heightOfLine)); boxStart = left; boxWidth = width; lastColor = currentColor; } else { boxWidth += kernedWidth; } } currentColor && textBgRects.push(...createSVGInlineRect(lastColor, leftOffset + boxStart, textTopOffset, boxWidth, heightOfLine)); } /** * @deprecated unused */ _getSVGLineTopOffset(lineIndex) { let lineTopOffset = 0, j; for (j = 0; j < lineIndex; j++) { lineTopOffset += this.getHeightOfLine(j); } const lastHeight = this.getHeightOfLine(j); return { lineTop: lineTopOffset, offset: (this._fontSizeMult - this._fontSizeFraction) * lastHeight / (this.lineHeight * this._fontSizeMult) }; } /** * Returns styles-string for svg-export * @param {Boolean} skipShadow a boolean to skip shadow filter output * @return {String} */ getSvgStyles(skipShadow) { return "".concat(super.getSvgStyles(skipShadow), " text-decoration-thickness: ").concat(toFixed(this.textDecorationThickness * this.getObjectScaling().y / 10, config.NUM_FRACTION_DIGITS), "%; white-space: pre;"); } /** * Returns styles-string for svg-export * @param {Object} style the object from which to retrieve style properties * @param {Boolean} useWhiteSpace a boolean to include an additional attribute in the style. * @return {String} */ getSvgSpanStyles(style, useWhiteSpace) { const { fontFamily, strokeWidth, stroke, fill, fontSize, fontStyle, fontWeight, deltaY, textDecorationThickness, linethrough, overline, underline } = style; const textDecoration = this.getSvgTextDecoration({ underline: underline !== null && underline !== void 0 ? underline : this.underline, overline: overline !== null && overline !== void 0 ? overline : this.overline, linethrough: linethrough !== null && linethrough !== void 0 ? linethrough : this.linethrough }); const thickness = textDecorationThickness || this.textDecorationThickness; return [stroke ? colorPropToSVG(STROKE, stroke) : '', strokeWidth ? "stroke-width: ".concat(strokeWidth, "; ") : '', fontFamily ? "font-family: ".concat(!fontFamily.includes("'") && !fontFamily.includes('"') ? "'".concat(fontFamily, "'") : fontFamily, "; ") : '', fontSize ? "font-size: ".concat(fontSize, "px; ") : '', fontStyle ? "font-style: ".concat(fontStyle, "; ") : '', fontWeight ? "font-weight: ".concat(fontWeight, "; ") : '', textDecoration ? "text-decoration: ".concat(textDecoration, "; text-decoration-thickness: ").concat(toFixed(thickness * this.getObjectScaling().y / 10, config.NUM_FRACTION_DIGITS), "%; ") : '', fill ? colorPropToSVG(FILL, fill) : '', deltaY ? "baseline-shift: ".concat(-deltaY, "; ") : '', useWhiteSpace ? 'white-space: pre; ' : ''].join(''); } /** * Returns text-decoration property for svg-export * @param {Object} style the object from which to retrieve style properties * @return {String} */ getSvgTextDecoration(style) { return ['overline', 'underline', 'line-through'].filter(decoration => style[decoration.replace('-', '')]).join(' '); } } export { TextSVGExportMixin }; //# sourceMappingURL=TextSVGExportMixin.mjs.map