devextreme
Version:
HTML5 JavaScript Component Suite for Responsive Web Development
743 lines (709 loc) • 26.9 kB
JavaScript
/**
* DevExtreme (esm/exporter/image_creator.js)
* Version: 21.1.4
* Build date: Mon Jun 21 2021
*
* Copyright (c) 2012 - 2021 Developer Express Inc. ALL RIGHTS RESERVED
* Read about DevExtreme licensing here: https://js.devexpress.com/Licensing/
*/
import $ from "../core/renderer";
import Color from "../color";
import {
isFunction,
isPromise,
isDefined
} from "../core/utils/type";
import {
getSvgElement
} from "../core/utils/svg";
import {
each as _each,
map as _map
} from "../core/utils/iterator";
import {
extend
} from "../core/utils/extend";
import domAdapter from "../core/dom_adapter";
import {
contains
} from "../core/utils/dom";
import {
getWindow
} from "../core/utils/window";
var window = getWindow();
import {
camelize
} from "../core/utils/inflector";
import {
Deferred,
fromPromise
} from "../core/utils/deferred";
var _math = Math;
var PI = _math.PI;
var _min = _math.min;
var _abs = _math.abs;
var _sqrt = _math.sqrt;
var _pow = _math.pow;
var _atan2 = _math.atan2;
var _cos = _math.cos;
var _sin = _math.sin;
var _number = Number;
var IMAGE_QUALITY = 1;
var TEXT_DECORATION_LINE_WIDTH_COEFF = .05;
var DEFAULT_FONT_SIZE = "10px";
var DEFAULT_FONT_FAMILY = "sans-serif";
var DEFAULT_TEXT_COLOR = "#000";
var parseAttributes;
function createCanvas(width, height, margin) {
var canvas = $("<canvas>")[0];
canvas.width = width + 2 * margin;
canvas.height = height + 2 * margin;
canvas.hidden = true;
return canvas
}
function getStringFromCanvas(canvas, mimeType) {
var dataURL = canvas.toDataURL(mimeType, IMAGE_QUALITY);
var imageData = window.atob(dataURL.substring(("data:" + mimeType + ";base64,").length));
return imageData
}
function arcTo(x1, y1, x2, y2, radius, largeArcFlag, clockwise, context) {
var cBx = (x1 + x2) / 2;
var cBy = (y1 + y2) / 2;
var aB = _atan2(y1 - y2, x1 - x2);
var k = largeArcFlag ? 1 : -1;
aB += PI / 180 * 90 * (clockwise ? 1 : -1);
var opSide = _sqrt(_pow(x2 - x1, 2) + _pow(y2 - y1, 2)) / 2;
var adjSide = _sqrt(_abs(_pow(radius, 2) - _pow(opSide, 2)));
var centerX = cBx + k * (adjSide * _cos(aB));
var centerY = cBy + k * (adjSide * _sin(aB));
var startAngle = _atan2(y1 - centerY, x1 - centerX);
var endAngle = _atan2(y2 - centerY, x2 - centerX);
context.arc(centerX, centerY, radius, startAngle, endAngle, !clockwise)
}
function getElementOptions(element, rootAppended) {
var attr = parseAttributes(element.attributes || {});
var options = extend({}, attr, {
text: element.textContent.replace(/\s+/g, " "),
textAlign: "middle" === attr["text-anchor"] ? "center" : attr["text-anchor"]
});
var transform = attr.transform;
var coords;
if (transform) {
coords = transform.match(/translate\(-*\d+([.]\d+)*(,*\s*-*\d+([.]\d+)*)*/);
if (coords) {
coords = coords[0].match(/-*\d+([.]\d+)*/g);
options.translateX = _number(coords[0]);
options.translateY = coords[1] ? _number(coords[1]) : 0
}
coords = transform.match(/rotate\(-*\d+([.]\d+)*(,*\s*-*\d+([.]\d+)*,*\s*-*\d+([.]\d+)*)*/);
if (coords) {
coords = coords[0].match(/-*\d+([.]\d+)*/g);
options.rotationAngle = _number(coords[0]);
options.rotationX = coords[1] && _number(coords[1]);
options.rotationY = coords[2] && _number(coords[2])
}
coords = transform.match(/scale\(-*\d+([.]\d+)*(,*\s*-*\d+([.]\d+)*)*/);
if (coords) {
coords = coords[0].match(/-*\d+([.]\d+)*/g);
options.scaleX = _number(coords[0]);
if (coords.length > 1) {
options.scaleY = _number(coords[1])
} else {
options.scaleY = options.scaleX
}
}
}
parseStyles(element, options, rootAppended);
return options
}
function drawRect(context, options) {
var x = options.x;
var y = options.y;
var width = options.width;
var height = options.height;
var cornerRadius = options.rx;
if (!cornerRadius) {
context.rect(x, y, width, height)
} else {
cornerRadius = _min(cornerRadius, width / 2, height / 2);
context.save();
context.translate(x, y);
context.moveTo(width / 2, 0);
context.arcTo(width, 0, width, height, cornerRadius);
context.arcTo(width, height, 0, height, cornerRadius);
context.arcTo(0, height, 0, 0, cornerRadius);
context.arcTo(0, 0, cornerRadius, 0, cornerRadius);
context.lineTo(width / 2, 0);
context.restore()
}
}
function drawImage(context, options, shared) {
var d = new Deferred;
var image = new window.Image;
image.onload = function() {
context.save();
context.globalAlpha = options.globalAlpha;
transformElement(context, options);
clipElement(context, options, shared);
context.drawImage(image, options.x, options.y, options.width, options.height);
context.restore();
d.resolve()
};
image.onerror = function() {
d.resolve()
};
image.setAttribute("crossOrigin", "anonymous");
image.src = options.href || options["xlink:href"];
return d
}
function drawPath(context, dAttr) {
var dArray = dAttr.replace(/,/g, " ").split(/([A-Z])/i).filter(item => "" !== item.trim());
var i = 0;
var params;
var prevParams;
var prevParamsLen;
do {
params = (dArray[i + 1] || "").trim().split(" ");
switch (dArray[i]) {
case "M":
context.moveTo(_number(params[0]), _number(params[1]));
i += 2;
break;
case "L":
for (var j = 0; j < params.length / 2; j++) {
context.lineTo(_number(params[2 * j]), _number(params[2 * j + 1]))
}
i += 2;
break;
case "C":
context.bezierCurveTo(_number(params[0]), _number(params[1]), _number(params[2]), _number(params[3]), _number(params[4]), _number(params[5]));
i += 2;
break;
case "a":
prevParams = dArray[i - 1].trim().split(" ");
prevParamsLen = prevParams.length - 1;
arcTo(_number(prevParams[prevParamsLen - 1]), _number(prevParams[prevParamsLen]), _number(prevParams[prevParamsLen - 1]) + _number(params[5]), _number(prevParams[prevParamsLen]) + _number(params[6]), _number(params[0]), _number(params[3]), _number(params[4]), context);
i += 2;
break;
case "A":
prevParams = dArray[i - 1].trim().split(" ");
prevParamsLen = prevParams.length - 1;
arcTo(_number(prevParams[prevParamsLen - 1]), _number(prevParams[prevParamsLen]), _number(params[5]), _number(params[6]), _number(params[0]), _number(params[3]), _number(params[4]), context);
i += 2;
break;
case "Z":
context.closePath();
i += 1;
break;
default:
i++
}
} while (i < dArray.length)
}
function parseStyles(element, options, rootAppended) {
var style = element.style || {};
var field;
for (field in style) {
if ("" !== style[field]) {
options[camelize(field)] = style[field]
}
}
if (rootAppended && domAdapter.isElementNode(element)) {
style = window.getComputedStyle(element);
["fill", "stroke", "stroke-width", "font-family", "font-size", "font-style", "font-weight"].forEach((function(prop) {
if (prop in style && "" !== style[prop]) {
options[camelize(prop)] = style[prop]
}
}));
["opacity", "fill-opacity", "stroke-opacity"].forEach((function(prop) {
if (prop in style && "" !== style[prop] && "1" !== style[prop]) {
options[prop] = _number(style[prop])
}
}))
}
options.textDecoration = options.textDecoration || options.textDecorationLine;
options.globalAlpha = isDefined(options.opacity) ? options.opacity : options.globalAlpha
}
function parseUrl(urlString) {
var matches = urlString && urlString.match(/url\(.*#(.*?)["']?\)/i);
return matches && matches[1]
}
function setFontStyle(context, options) {
var fontParams = [];
options.fontSize = options.fontSize || DEFAULT_FONT_SIZE;
options.fontFamily = options.fontFamily || DEFAULT_FONT_FAMILY;
options.fill = options.fill || DEFAULT_TEXT_COLOR;
options.fontStyle && fontParams.push(options.fontStyle);
options.fontWeight && fontParams.push(options.fontWeight);
fontParams.push(options.fontSize);
fontParams.push(options.fontFamily);
context.font = fontParams.join(" ");
context.textAlign = options.textAlign;
context.fillStyle = options.fill;
context.globalAlpha = options.globalAlpha
}
function drawText(context, options, shared) {
setFontStyle(context, options);
applyFilter(context, options, shared);
options.text && context.fillText(options.text, options.x || 0, options.y || 0);
strokeElement(context, options, true);
drawTextDecoration(context, options, shared)
}
function drawTextDecoration(context, options, shared) {
if (!options.textDecoration || "none" === options.textDecoration) {
return
}
var x = options.x;
var textWidth = context.measureText(options.text).width;
var textHeight = parseInt(options.fontSize, 10);
var lineHeight = textHeight * TEXT_DECORATION_LINE_WIDTH_COEFF < 1 ? 1 : textHeight * TEXT_DECORATION_LINE_WIDTH_COEFF;
var y = options.y;
switch (options.textDecoration) {
case "line-through":
y -= textHeight / 3 + lineHeight / 2;
break;
case "overline":
y -= textHeight - lineHeight;
break;
case "underline":
y += lineHeight
}
context.rect(x, y, textWidth, lineHeight);
fillElement(context, options, shared);
strokeElement(context, options)
}
function aggregateOpacity(options) {
options.strokeOpacity = void 0 !== options["stroke-opacity"] ? options["stroke-opacity"] : 1;
options.fillOpacity = void 0 !== options["fill-opacity"] ? options["fill-opacity"] : 1;
if (void 0 !== options.opacity) {
options.strokeOpacity *= options.opacity;
options.fillOpacity *= options.opacity
}
}
function hasTspan(element) {
var nodes = element.childNodes;
for (var i = 0; i < nodes.length; i++) {
if ("tspan" === nodes[i].tagName) {
return true
}
}
return false
}
function drawTextElement(childNodes, context, options, shared) {
var lines = [];
var line;
var offset = 0;
for (var i = 0; i < childNodes.length; i++) {
var element = childNodes[i];
if (void 0 === element.tagName) {
drawElement(element, context, options, shared)
} else if ("tspan" === element.tagName || "text" === element.tagName) {
var elementOptions = getElementOptions(element, shared.rootAppended);
var mergedOptions = extend({}, options, elementOptions);
if ("tspan" === element.tagName && hasTspan(element)) {
drawTextElement(element.childNodes, context, mergedOptions, shared);
continue
}
mergedOptions.textAlign = "start";
if (!line || void 0 !== elementOptions.x) {
line = {
elements: [],
options: [],
widths: [],
offsets: []
};
lines.push(line)
}
if (void 0 !== elementOptions.y) {
offset = 0
}
if (void 0 !== elementOptions.dy) {
offset += parseFloat(elementOptions.dy)
}
line.elements.push(element);
line.options.push(mergedOptions);
line.offsets.push(offset);
setFontStyle(context, mergedOptions);
line.widths.push(context.measureText(mergedOptions.text).width)
}
}
lines.forEach((function(line) {
var commonWidth = line.widths.reduce((function(commonWidth, width) {
return commonWidth + width
}), 0);
var xDiff = 0;
var currentOffset = 0;
if ("center" === options.textAlign) {
xDiff = commonWidth / 2
}
if ("end" === options.textAlign) {
xDiff = commonWidth
}
line.options.forEach((function(o, index) {
var width = line.widths[index];
o.x = o.x - xDiff + currentOffset;
o.y += line.offsets[index];
currentOffset += width
}));
line.elements.forEach((function(element, index) {
drawTextElement(element.childNodes, context, line.options[index], shared)
}))
}))
}
function drawElement(element, context, parentOptions, shared) {
var tagName = element.tagName;
var isText = "text" === tagName || "tspan" === tagName || void 0 === tagName;
var isImage = "image" === tagName;
var options = extend({}, parentOptions, getElementOptions(element, shared.rootAppended));
if ("hidden" === options.visibility || options["hidden-for-export"]) {
return
}
context.save();
!isImage && transformElement(context, options);
clipElement(context, options, shared);
aggregateOpacity(options);
var promise;
context.beginPath();
switch (element.tagName) {
case void 0:
drawText(context, options, shared);
break;
case "text":
case "tspan":
drawTextElement(element.childNodes, context, options, shared);
break;
case "image":
promise = drawImage(context, options, shared);
break;
case "path":
drawPath(context, options.d);
break;
case "rect":
drawRect(context, options);
context.closePath();
break;
case "circle":
context.arc(options.cx, options.cy, options.r, 0, 2 * PI, 1)
}
if (!isText) {
applyFilter(context, options, shared);
fillElement(context, options, shared);
strokeElement(context, options)
}
applyGradient(context, options, shared, element);
context.restore();
return promise
}
function applyGradient(context, options, _ref, element) {
var {
gradients: gradients
} = _ref;
if (0 === gradients.length) {
return
}
var id = parseUrl(options.fill);
if (id && gradients[id]) {
var box = element.getBBox();
var gradient = context.createLinearGradient(box.x, 0, box.x + box.width, 0);
gradients[id].forEach(opt => {
var offset = parseInt(opt.offset.replace(/%/, ""));
gradient.addColorStop(offset / 100, opt.stopColor)
});
context.globalAlpha = options.opacity;
context.fillStyle = gradient;
context.fill()
}
}
function applyFilter(context, options, shared) {
var filterOptions;
var id = parseUrl(options.filter);
if (id) {
filterOptions = shared.filters[id];
if (!filterOptions) {
filterOptions = {
offsetX: 0,
offsetY: 0,
blur: 0,
color: "#000"
}
}
context.shadowOffsetX = filterOptions.offsetX;
context.shadowOffsetY = filterOptions.offsetY;
context.shadowColor = filterOptions.color;
context.shadowBlur = filterOptions.blur
}
}
function transformElement(context, options) {
context.translate(options.translateX || 0, options.translateY || 0);
options.translateX = void 0;
options.translateY = void 0;
if (options.rotationAngle) {
context.translate(options.rotationX || 0, options.rotationY || 0);
context.rotate(options.rotationAngle * PI / 180);
context.translate(-(options.rotationX || 0), -(options.rotationY || 0));
options.rotationAngle = void 0;
options.rotationX = void 0;
options.rotationY = void 0
}
if (isFinite(options.scaleX)) {
context.scale(options.scaleX, options.scaleY);
options.scaleX = void 0;
options.scaleY = void 0
}
}
function clipElement(context, options, shared) {
if (options["clip-path"]) {
drawElement(shared.clipPaths[parseUrl(options["clip-path"])], context, {}, shared);
context.clip();
options["clip-path"] = void 0
}
}
function hex2rgba(hexColor, alpha) {
var color = new Color(hexColor);
return "rgba(" + color.r + "," + color.g + "," + color.b + "," + alpha + ")"
}
function createGradient(element) {
var options = [];
_each(element.childNodes, (_, _ref2) => {
var {
attributes: attributes
} = _ref2;
options.push({
offset: attributes.offset.value,
stopColor: attributes["stop-color"].value
})
});
return options
}
function createFilter(element) {
var color;
var opacity;
var filterOptions = {};
_each(element.childNodes, (function(_, node) {
var attr = node.attributes;
if (!attr.result) {
return
}
switch (attr.result.value) {
case "gaussianBlurResult":
filterOptions.blur = _number(attr.stdDeviation.value);
break;
case "offsetResult":
filterOptions.offsetX = _number(attr.dx.value);
filterOptions.offsetY = _number(attr.dy.value);
break;
case "floodResult":
color = attr["flood-color"] ? attr["flood-color"].value : "#000";
opacity = attr["flood-opacity"] ? attr["flood-opacity"].value : 1;
filterOptions.color = hex2rgba(color, opacity)
}
}));
return filterOptions
}
function asyncEach(array, callback) {
var d = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : new Deferred;
var i = 0;
for (; i < array.length; i++) {
var result = callback(array[i]);
if (isPromise(result)) {
result.then(() => {
asyncEach(Array.prototype.slice.call(array, i + 1), callback, d)
});
break
}
}
if (i === array.length) {
d.resolve()
}
return d
}
function drawCanvasElements(elements, context, parentOptions, shared) {
return asyncEach(elements, (function(element) {
switch (element.tagName && element.tagName.toLowerCase()) {
case "g":
case "svg":
var options = extend({}, parentOptions, getElementOptions(element, shared.rootAppended));
context.save();
transformElement(context, options);
clipElement(context, options, shared);
var onDone = () => {
context.restore()
};
var d = drawCanvasElements(element.childNodes, context, options, shared);
if (isPromise(d)) {
d.then(onDone)
} else {
onDone()
}
return d;
case "defs":
return drawCanvasElements(element.childNodes, context, {}, shared);
case "clippath":
shared.clipPaths[element.attributes.id.textContent] = element.childNodes[0];
break;
case "pattern":
shared.patterns[element.attributes.id.textContent] = element;
break;
case "filter":
shared.filters[element.id] = createFilter(element);
break;
case "lineargradient":
shared.gradients[element.attributes.id.textContent] = createGradient(element);
break;
default:
return drawElement(element, context, parentOptions, shared)
}
}))
}
function setLineDash(context, options) {
var matches = options["stroke-dasharray"] && options["stroke-dasharray"].match(/(\d+)/g);
if (matches && matches.length) {
matches = _map(matches, (function(item) {
return _number(item)
}));
context.setLineDash(matches)
}
}
function strokeElement(context, options, isText) {
var stroke = options.stroke;
if (stroke && "none" !== stroke && 0 !== options["stroke-width"]) {
setLineDash(context, options);
context.lineJoin = options["stroke-linejoin"];
context.lineWidth = options["stroke-width"];
context.globalAlpha = options.strokeOpacity;
context.strokeStyle = stroke;
isText ? context.strokeText(options.text, options.x, options.y) : context.stroke();
context.globalAlpha = 1
}
}
function getPattern(context, pattern, shared) {
var options = getElementOptions(pattern, shared.rootAppended);
var patternCanvas = createCanvas(options.width, options.height, 0);
var patternContext = patternCanvas.getContext("2d");
drawCanvasElements(pattern.childNodes, patternContext, options, shared);
return context.createPattern(patternCanvas, "repeat")
}
function fillElement(context, options, shared) {
var fill = options.fill;
if (fill && "none" !== fill) {
if (-1 === fill.search(/url/)) {
context.fillStyle = fill
} else {
var pattern = shared.patterns[parseUrl(fill)];
if (!pattern) {
return
}
context.fillStyle = getPattern(context, pattern, shared)
}
context.globalAlpha = options.fillOpacity;
context.fill();
context.globalAlpha = 1
}
}
parseAttributes = function(attributes) {
var newAttributes = {};
var attr;
_each(attributes, (function(index, item) {
attr = item.textContent;
if (isFinite(attr)) {
attr = _number(attr)
}
newAttributes[item.name.toLowerCase()] = attr
}));
return newAttributes
};
function drawBackground(context, width, height, backgroundColor, margin) {
context.fillStyle = backgroundColor || "#ffffff";
context.fillRect(-margin, -margin, width + 2 * margin, height + 2 * margin)
}
function createInvisibleDiv() {
var invisibleDiv = domAdapter.createElement("div");
invisibleDiv.style.left = "-9999px";
invisibleDiv.style.position = "absolute";
return invisibleDiv
}
function convertSvgToCanvas(svg, canvas, rootAppended) {
return drawCanvasElements(svg.childNodes, canvas.getContext("2d"), {}, {
clipPaths: {},
patterns: {},
filters: {},
gradients: {},
rootAppended: rootAppended
})
}
function getCanvasFromSvg(markup, width, height, backgroundColor, margin, pixelRatio) {
var svgToCanvas = arguments.length > 6 && void 0 !== arguments[6] ? arguments[6] : convertSvgToCanvas;
var canvas = createCanvas(width, height, margin);
var context = canvas.getContext("2d");
context.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
var svgElem = getSvgElement(markup);
var invisibleDiv;
var markupIsDomElement = domAdapter.isElementNode(markup);
context.translate(margin, margin);
domAdapter.getBody().appendChild(canvas);
if (!markupIsDomElement) {
invisibleDiv = createInvisibleDiv();
invisibleDiv.appendChild(svgElem);
domAdapter.getBody().appendChild(invisibleDiv)
}
if (svgElem.attributes.direction) {
canvas.dir = svgElem.attributes.direction.textContent
}
drawBackground(context, width, height, backgroundColor, margin);
return fromPromise(svgToCanvas(svgElem, canvas, markupIsDomElement && contains(domAdapter.getBody(), markup))).then(() => canvas).always(() => {
invisibleDiv && domAdapter.getBody().removeChild(invisibleDiv);
domAdapter.getBody().removeChild(canvas)
})
}
export var imageCreator = {
getImageData: function(markup, options) {
var pixelRatio = window.devicePixelRatio || 1;
var mimeType = "image/" + options.format;
var width = options.width * pixelRatio;
var height = options.height * pixelRatio;
var backgroundColor = options.backgroundColor;
if (isFunction(options.__parseAttributesFn)) {
parseAttributes = options.__parseAttributesFn
}
return getCanvasFromSvg(markup, width, height, backgroundColor, options.margin, pixelRatio, options.svgToCanvas).then(canvas => getStringFromCanvas(canvas, mimeType))
},
getData: function(markup, options) {
var that = this;
return imageCreator.getImageData(markup, options).then(binaryData => {
var mimeType = "image/" + options.format;
var data = isFunction(window.Blob) && !options.forceProxy ? that._getBlob(binaryData, mimeType) : that._getBase64(binaryData);
return data
})
},
_getBlob: function(binaryData, mimeType) {
var i;
var dataArray = new Uint8Array(binaryData.length);
for (i = 0; i < binaryData.length; i++) {
dataArray[i] = binaryData.charCodeAt(i)
}
return new window.Blob([dataArray.buffer], {
type: mimeType
})
},
_getBase64: function(binaryData) {
return window.btoa(binaryData)
}
};
export function getData(data, options) {
return imageCreator.getData(data, options)
}
export function testFormats(formats) {
var canvas = createCanvas(100, 100, 0);
return formats.reduce((function(r, f) {
var mimeType = ("image/" + f).toLowerCase();
if (-1 !== canvas.toDataURL(mimeType).indexOf(mimeType)) {
r.supported.push(f)
} else {
r.unsupported.push(f)
}
return r
}), {
supported: [],
unsupported: []
})
}