/* globals jQuery, ActiveXObject */
/**
* Package: svgedit.utilities
*
* Licensed under the MIT License
*
* Copyright(c) 2010 Alexis Deveria
* Copyright(c) 2010 Jeff Schiller
*/
import './pathseg.js';
import RGBColor from './canvg/rgbcolor.js';
import jqPluginSVG from './jquery-svg.js'; // Needed for SVG attribute setting and array form with `attr`
import {importScript, importModule} from './external/dynamic-import-polyfill/importModule.js';
import {NS} from './svgedit.js';
import {getTransformList} from './svgtransformlist.js';
import {setUnitAttr, getTypeMap} from './units.js';
import {convertPath} from './path.js';
import {
hasMatrixTransform, transformListToTransform, transformBox
} from './math.js';
import {
isWebkit, supportsHVLineContainerBBox, supportsPathBBox, supportsXpath,
supportsSelectors
} from './browser.js';
// Constants
const $ = jqPluginSVG(jQuery);
// String used to encode base64.
const KEYSTR = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
// Much faster than running getBBox() every time
const visElems = 'a,circle,ellipse,foreignObject,g,image,line,path,polygon,polyline,rect,svg,text,tspan,use';
const visElemsArr = visElems.split(',');
// const hidElems = 'clipPath,defs,desc,feGaussianBlur,filter,linearGradient,marker,mask,metadata,pattern,radialGradient,stop,switch,symbol,title,textPath';
let editorContext_ = null;
let domdoc_ = null;
let domcontainer_ = null;
let svgroot_ = null;
export const init = function (editorContext) {
editorContext_ = editorContext;
domdoc_ = editorContext.getDOMDocument();
domcontainer_ = editorContext.getDOMContainer();
svgroot_ = editorContext.getSVGRoot();
};
// Converts characters in a string to XML-friendly entities.
//
// Example: '&' becomes '&'
//
// Parameters:
// str - The string to be converted
//
// Returns:
// The converted string
export const toXml = function (str) {
// ' is ok in XML, but not HTML
// > does not normally need escaping, though it can if within a CDATA expression (and preceded by "]]")
return str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/, ''');
};
// Converts XML entities in a string to single characters.
// Example: '&' becomes '&'
//
// Parameters:
// str - The string to be converted
//
// Returns:
// The converted string
export const fromXml = function (str) {
return $('
').html(str).text();
};
// This code was written by Tyler Akins and has been placed in the
// public domain. It would be nice if you left this header intact.
// Base64 code from Tyler Akins -- http://rumkin.com
// schiller: Removed string concatenation in favour of Array.join() optimization,
// also precalculate the size of the array needed.
// Converts a string to base64
export const encode64 = function (input) {
// base64 strings are 4/3 larger than the original string
input = encodeUTF8(input); // convert non-ASCII characters
// input = convertToXMLReferences(input);
if (window.btoa) {
return window.btoa(input); // Use native if available
}
const output = [];
output.length = Math.floor((input.length + 2) / 3) * 4;
let i = 0, p = 0;
do {
const chr1 = input.charCodeAt(i++);
const chr2 = input.charCodeAt(i++);
const chr3 = input.charCodeAt(i++);
const enc1 = chr1 >> 2;
const enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
let enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
let enc4 = chr3 & 63;
if (isNaN(chr2)) {
enc3 = enc4 = 64;
} else if (isNaN(chr3)) {
enc4 = 64;
}
output[p++] = KEYSTR.charAt(enc1);
output[p++] = KEYSTR.charAt(enc2);
output[p++] = KEYSTR.charAt(enc3);
output[p++] = KEYSTR.charAt(enc4);
} while (i < input.length);
return output.join('');
};
// Converts a string from base64
export const decode64 = function (input) {
if (window.atob) {
return decodeUTF8(window.atob(input));
}
// remove all characters that are not A-Z, a-z, 0-9, +, /, or =
input = input.replace(/[^A-Za-z0-9+/=]/g, '');
let output = '';
let i = 0;
do {
const enc1 = KEYSTR.indexOf(input.charAt(i++));
const enc2 = KEYSTR.indexOf(input.charAt(i++));
const enc3 = KEYSTR.indexOf(input.charAt(i++));
const enc4 = KEYSTR.indexOf(input.charAt(i++));
const chr1 = (enc1 << 2) | (enc2 >> 4);
const chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
const chr3 = ((enc3 & 3) << 6) | enc4;
output += String.fromCharCode(chr1);
if (enc3 !== 64) {
output = output + String.fromCharCode(chr2);
}
if (enc4 !== 64) {
output = output + String.fromCharCode(chr3);
}
} while (i < input.length);
return decodeUTF8(output);
};
export const decodeUTF8 = function (argString) {
return decodeURIComponent(escape(argString));
};
// codedread:does not seem to work with webkit-based browsers on OSX // Brettz9: please test again as function upgraded
export const encodeUTF8 = function (argString) {
return unescape(encodeURIComponent(argString));
};
/**
* convert dataURL to object URL
* @param {string} dataurl
* @return {string} object URL or empty string
*/
export const dataURLToObjectURL = function (dataurl) {
if (typeof Uint8Array === 'undefined' || typeof Blob === 'undefined' || typeof URL === 'undefined' || !URL.createObjectURL) {
return '';
}
const arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]);
let n = bstr.length;
const u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
const blob = new Blob([u8arr], {type: mime});
return URL.createObjectURL(blob);
};
/**
* get object URL for a blob object
* @param {Blob} blob A Blob object or File object
* @return {string} object URL or empty string
*/
export const createObjectURL = function (blob) {
if (!blob || typeof URL === 'undefined' || !URL.createObjectURL) {
return '';
}
return URL.createObjectURL(blob);
};
/**
* @property {string} blankPageObjectURL
*/
export const blankPageObjectURL = (function () {
if (typeof Blob === 'undefined') {
return '';
}
const blob = new Blob(['SVG-edit '], {type: 'text/html'});
return createObjectURL(blob);
})();
// Converts a string to use XML references
export const convertToXMLReferences = function (input) {
let n,
output = '';
for (n = 0; n < input.length; n++) {
const c = input.charCodeAt(n);
if (c < 128) {
output += input[n];
} else if (c > 127) {
output += ('' + c + ';');
}
}
return output;
};
// Cross-browser compatible method of converting a string to an XML tree
// found this function here: http://groups.google.com/group/jquery-dev/browse_thread/thread/c6d11387c580a77f
export const text2xml = function (sXML) {
if (sXML.includes('
// *
// *
//
// Parameters:
// attrVal - The attribute value as a string
//
// Returns:
// String with just the URL, like someFile.svg#foo
export const getUrlFromAttr = function (attrVal) {
if (attrVal) {
// url("#somegrad")
if (attrVal.startsWith('url("')) {
return attrVal.substring(5, attrVal.indexOf('"', 6));
}
// url('#somegrad')
if (attrVal.startsWith("url('")) {
return attrVal.substring(5, attrVal.indexOf("'", 6));
}
if (attrVal.startsWith('url(')) {
return attrVal.substring(4, attrVal.indexOf(')'));
}
}
return null;
};
// Returns the given element's xlink:href value
export let getHref = function (elem) {
return elem.getAttributeNS(NS.XLINK, 'href');
};
// Sets the given element's xlink:href value
export let setHref = function (elem, val) {
elem.setAttributeNS(NS.XLINK, 'xlink:href', val);
};
// Returns:
// The document's element, create it first if necessary
export const findDefs = function () {
const svgElement = editorContext_.getSVGContent();
let defs = svgElement.getElementsByTagNameNS(NS.SVG, 'defs');
if (defs.length > 0) {
defs = defs[0];
} else {
defs = svgElement.ownerDocument.createElementNS(NS.SVG, 'defs');
if (svgElement.firstChild) {
// first child is a comment, so call nextSibling
svgElement.insertBefore(defs, svgElement.firstChild.nextSibling);
} else {
svgElement.appendChild(defs);
}
}
return defs;
};
// TODO(codedread): Consider moving the next to functions to bbox.js
// Get correct BBox for a path in Webkit
// Converted from code found here:
// http://blog.hackers-cafe.net/2009/06/how-to-calculate-bezier-curves-bounding.html
//
// Parameters:
// path - The path DOM element to get the BBox for
//
// Returns:
// A BBox-like object
export const getPathBBox = function (path) {
const seglist = path.pathSegList;
const tot = seglist.numberOfItems;
const bounds = [[], []];
const start = seglist.getItem(0);
let P0 = [start.x, start.y];
for (let i = 0; i < tot; i++) {
const seg = seglist.getItem(i);
if (seg.x === undefined) { continue; }
// Add actual points to limits
bounds[0].push(P0[0]);
bounds[1].push(P0[1]);
if (seg.x1) {
const P1 = [seg.x1, seg.y1],
P2 = [seg.x2, seg.y2],
P3 = [seg.x, seg.y];
for (let j = 0; j < 2; j++) {
const calc = function (t) {
return Math.pow(1 - t, 3) * P0[j] +
3 * Math.pow(1 - t, 2) * t * P1[j] +
3 * (1 - t) * Math.pow(t, 2) * P2[j] +
Math.pow(t, 3) * P3[j];
};
const b = 6 * P0[j] - 12 * P1[j] + 6 * P2[j];
const a = -3 * P0[j] + 9 * P1[j] - 9 * P2[j] + 3 * P3[j];
const c = 3 * P1[j] - 3 * P0[j];
if (a === 0) {
if (b === 0) {
continue;
}
const t = -c / b;
if (t > 0 && t < 1) {
bounds[j].push(calc(t));
}
continue;
}
const b2ac = Math.pow(b, 2) - 4 * c * a;
if (b2ac < 0) { continue; }
const t1 = (-b + Math.sqrt(b2ac)) / (2 * a);
if (t1 > 0 && t1 < 1) { bounds[j].push(calc(t1)); }
const t2 = (-b - Math.sqrt(b2ac)) / (2 * a);
if (t2 > 0 && t2 < 1) { bounds[j].push(calc(t2)); }
}
P0 = P3;
} else {
bounds[0].push(seg.x);
bounds[1].push(seg.y);
}
}
const x = Math.min.apply(null, bounds[0]);
const w = Math.max.apply(null, bounds[0]) - x;
const y = Math.min.apply(null, bounds[1]);
const h = Math.max.apply(null, bounds[1]) - y;
return {
x,
y,
width: w,
height: h
};
};
// Get the given/selected element's bounding box object, checking for
// horizontal/vertical lines (see issue 717)
// Note that performance is currently terrible, so some way to improve would
// be great.
//
// Parameters:
// selected - Container or