Source: svgcanvas/svg-exec.js

/**
 * Tools for svg.
 * @module svg
 * @license MIT
 * @copyright 2011 Jeff Schiller
 */

import { jsPDF } from 'jspdf/dist/jspdf.es.min.js';
import 'svg2pdf.js/dist/svg2pdf.es.js';
import html2canvas from 'html2canvas';
import * as hstry from './history.js';
import {
  text2xml, cleanupElement, findDefs, getHref, preventClickDefault,
  toXml, getStrokedBBoxDefaultVisible, encode64, createObjectURL,
  dataURLToObjectURL, walkTree, getBBox as utilsGetBBox
} from './utilities.js';
import {
  transformPoint, transformListToTransform
} from './math.js';
import {
  convertUnit, shortFloat, convertToNum
} from '../common/units.js';
import { isGecko, isChrome, isWebkit } from '../common/browser.js';
import * as pathModule from './path.js';
import { NS } from './namespaces.js';
import * as draw from './draw.js';
import {
  recalculateDimensions
} from './recalculate.js';
import { getParents, getClosest } from '../editor/components/jgraduate/Util.js';

const {
  InsertElementCommand, RemoveElementCommand,
  ChangeElementCommand, BatchCommand
} = hstry;

let svgContext_ = null;
let svgCanvas = null;

/**
* @function module:svg-exec.init
* @param {module:svg-exec.SvgCanvas#init} svgContext
* @returns {void}
*/
export const init = function (svgContext) {
  svgContext_ = svgContext;
  svgCanvas = svgContext_.getCanvas();
};

/**
* Main function to set up the SVG content for output.
* @function module:svgcanvas.SvgCanvas#svgCanvasToString
* @returns {string} The SVG image for output
*/
export const svgCanvasToString = function () {
  // keep calling it until there are none to remove
  while (svgCanvas.removeUnusedDefElems() > 0) { } // eslint-disable-line no-empty

  svgCanvas.pathActions.clear(true);

  // Keep SVG-Edit comment on top
  const childNodesElems = svgContext_.getSVGContent().childNodes;
  childNodesElems.forEach(function (node, i) {
    if (i && node.nodeType === 8 && node.data.includes('Created with')) {
      svgContext_.getSVGContent().firstChild.before(node);
    }
  });

  // Move out of in-group editing mode
  if (svgContext_.getCurrentGroup()) {
    draw.leaveContext();
    svgCanvas.selectOnly([ svgContext_.getCurrentGroup() ]);
  }

  const nakedSvgs = [];

  // Unwrap gsvg if it has no special attributes (only id and style)
  const gsvgElems = svgContext_.getSVGContent().querySelectorAll('g[data-gsvg]');
  Array.prototype.forEach.call(gsvgElems, function (element) {
    const attrs = element.attributes;
    let len = attrs.length;
    for (let i = 0; i < len; i++) {
      if (attrs[i].nodeName === 'id' || attrs[i].nodeName === 'style') {
        len--;
      }
    }
    // No significant attributes, so ungroup
    if (len <= 0) {
      const svg = element.firstChild;
      nakedSvgs.push(svg);
      element.replaceWith(svg);
    }
  });
  const output = svgCanvas.svgToString(svgContext_.getSVGContent(), 0);

  // Rewrap gsvg
  if (nakedSvgs.length) {
    Array.prototype.forEach.call(nakedSvgs, function (el) {
      svgCanvas.groupSvgElem(el);
    });
  }

  return output;
};

/**
* Sub function ran on each SVG element to convert it to a string as desired.
* @function module:svgcanvas.SvgCanvas#svgToString
* @param {Element} elem - The SVG element to convert
* @param {Integer} indent - Number of spaces to indent this tag
* @returns {string} The given element as an SVG tag
*/
export const svgToString = function (elem, indent) {
  const curConfig = svgContext_.getCurConfig();
  const nsMap = svgContext_.getNsMap();
  const out = [];
  const unit = curConfig.baseUnit;
  const unitRe = new RegExp('^-?[\\d\\.]+' + unit + '$');

  if (elem) {
    cleanupElement(elem);
    const attrs = [ ...elem.attributes ];
    const childs = elem.childNodes;
    attrs.sort((a, b) => {
      return a.name > b.name ? -1 : 1;
    });

    for (let i = 0; i < indent; i++) { out.push(' '); }
    out.push('<'); out.push(elem.nodeName);
    if (elem.id === 'svgcontent') {
      // Process root element separately
      const res = svgCanvas.getResolution();

      const vb = '';
      // TODO: Allow this by dividing all values by current baseVal
      // Note that this also means we should properly deal with this on import
      // if (curConfig.baseUnit !== 'px') {
      //   const unit = curConfig.baseUnit;
      //   const unitM = getTypeMap()[unit];
      //   res.w = shortFloat(res.w / unitM);
      //   res.h = shortFloat(res.h / unitM);
      //   vb = ' viewBox="' + [0, 0, res.w, res.h].join(' ') + '"';
      //   res.w += unit;
      //   res.h += unit;
      // }

      if (unit !== 'px') {
        res.w = convertUnit(res.w, unit) + unit;
        res.h = convertUnit(res.h, unit) + unit;
      }

      out.push(' width="' + res.w + '" height="' + res.h + '"' + vb + ' xmlns="' + NS.SVG + '"');

      const nsuris = {};

      // Check elements for namespaces, add if found
      const csElements = elem.querySelectorAll('*');
      const cElements = Array.prototype.slice.call(csElements);
      cElements.push(elem);
      Array.prototype.forEach.call(cElements, function (el) {
        // const el = this;
        // for some elements have no attribute
        const uri = el.namespaceURI;
        if (uri && !nsuris[uri] && nsMap[uri] && nsMap[uri] !== 'xmlns' && nsMap[uri] !== 'xml') {
          nsuris[uri] = true;
          out.push(' xmlns:' + nsMap[uri] + '="' + uri + '"');
        }
        if (el.attributes.length > 0) {
          for (const [ , attr ] of Object.entries(el.attributes)) {
            const u = attr.namespaceURI;
            if (u && !nsuris[u] && nsMap[u] !== 'xmlns' && nsMap[u] !== 'xml') {
              nsuris[u] = true;
              out.push(' xmlns:' + nsMap[u] + '="' + u + '"');
            }
          }
        }
      });

      let i = attrs.length;
      const attrNames = [ 'width', 'height', 'xmlns', 'x', 'y', 'viewBox', 'id', 'overflow' ];
      while (i--) {
        const attr = attrs[i];
        const attrVal = toXml(attr.value);

        // Namespaces have already been dealt with, so skip
        if (attr.nodeName.startsWith('xmlns:')) { continue; }

        // only serialize attributes we don't use internally
        if (attrVal !== '' && !attrNames.includes(attr.localName) && (!attr.namespaceURI || nsMap[attr.namespaceURI])) {
          out.push(' ');
          out.push(attr.nodeName); out.push('="');
          out.push(attrVal); out.push('"');
        }
      }
    } else {
      // Skip empty defs
      if (elem.nodeName === 'defs' && !elem.firstChild) { return ''; }

      const mozAttrs = [ '-moz-math-font-style', '_moz-math-font-style' ];
      for (let i = attrs.length - 1; i >= 0; i--) {
        const attr = attrs[i];
        let attrVal = toXml(attr.value);
        // remove bogus attributes added by Gecko
        if (mozAttrs.includes(attr.localName)) { continue; }
        if (attrVal === 'null') {
          const styleName = attr.localName.replace(/-[a-z]/g, (s) => s[1].toUpperCase());
          if (Object.prototype.hasOwnProperty.call(elem.style, styleName)) { continue; }
        }
        if (attrVal !== '') {
          if (attrVal.startsWith('pointer-events')) { continue; }
          if (attr.localName === 'class' && attrVal.startsWith('se_')) { continue; }
          out.push(' ');
          if (attr.localName === 'd') { attrVal = svgCanvas.pathActions.convertPath(elem, true); }
          if (!isNaN(attrVal)) {
            attrVal = shortFloat(attrVal);
          } else if (unitRe.test(attrVal)) {
            attrVal = shortFloat(attrVal) + unit;
          }

          // Embed images when saving
          if (svgContext_.getSvgOptionApply() &&
            elem.nodeName === 'image' &&
            attr.localName === 'href' &&
            svgContext_.getSvgOptionImages() &&
            svgContext_.getSvgOptionImages() === 'embed'
          ) {
            const img = svgContext_.getEncodableImages(attrVal);
            if (img) { attrVal = img; }
          }

          // map various namespaces to our fixed namespace prefixes
          // (the default xmlns attribute itself does not get a prefix)
          if (!attr.namespaceURI || attr.namespaceURI === NS.SVG || nsMap[attr.namespaceURI]) {
            out.push(attr.nodeName); out.push('="');
            out.push(attrVal); out.push('"');
          }
        }
      }
    }

    if (elem.hasChildNodes()) {
      out.push('>');
      indent++;
      let bOneLine = false;

      for (let i = 0; i < childs.length; i++) {
        const child = childs.item(i);
        switch (child.nodeType) {
        case 1: // element node
          out.push('\n');
          out.push(svgCanvas.svgToString(child, indent));
          break;
        case 3: { // text node
          const str = child.nodeValue.replace(/^\s+|\s+$/g, '');
          if (str !== '') {
            bOneLine = true;
            out.push(String(toXml(str)));
          }
          break;
        } case 4: // cdata node
          out.push('\n');
          out.push(new Array(indent + 1).join(' '));
          out.push('<![CDATA[');
          out.push(child.nodeValue);
          out.push(']]>');
          break;
        case 8: // comment
          out.push('\n');
          out.push(new Array(indent + 1).join(' '));
          out.push('<!--');
          out.push(child.data);
          out.push('-->');
          break;
        } // switch on node type
      }
      indent--;
      if (!bOneLine) {
        out.push('\n');
        for (let i = 0; i < indent; i++) { out.push(' '); }
      }
      out.push('</'); out.push(elem.nodeName); out.push('>');
    } else {
      out.push('/>');
    }
  }
  return out.join('');
}; // end svgToString()

/**
* This function sets the current drawing as the input SVG XML.
* @function module:svgcanvas.SvgCanvas#setSvgString
* @param {string} xmlString - The SVG as XML text.
* @param {boolean} [preventUndo=false] - Indicates if we want to do the
* changes without adding them to the undo stack - e.g. for initializing a
* drawing on page load.
* @fires module:svgcanvas.SvgCanvas#event:setnonce
* @fires module:svgcanvas.SvgCanvas#event:unsetnonce
* @fires module:svgcanvas.SvgCanvas#event:changed
* @returns {boolean} This function returns `false` if the set was
*     unsuccessful, `true` otherwise.
*/
export const setSvgString = function (xmlString, preventUndo) {
  const curConfig = svgContext_.getCurConfig();
  const dataStorage = svgContext_.getDataStorage();
  try {
    // convert string into XML document
    const newDoc = text2xml(xmlString);
    if (newDoc.firstElementChild &&
      newDoc.firstElementChild.namespaceURI !== NS.SVG) {
      return false;
    }

    svgCanvas.prepareSvg(newDoc);

    const batchCmd = new BatchCommand('Change Source');

    // remove old svg document
    const { nextSibling } = svgContext_.getSVGContent();

    svgContext_.getSVGContent().remove();
    const oldzoom = svgContext_.getSVGContent();
    batchCmd.addSubCommand(new RemoveElementCommand(oldzoom, nextSibling, svgContext_.getSVGRoot()));

    // set new svg document
    // If DOM3 adoptNode() available, use it. Otherwise fall back to DOM2 importNode()
    if (svgContext_.getDOMDocument().adoptNode) {
      svgContext_.setSVGContent(svgContext_.getDOMDocument().adoptNode(newDoc.documentElement));
    } else {
      svgContext_.setSVGContent(svgContext_.getDOMDocument().importNode(newDoc.documentElement, true));
    }

    svgContext_.getSVGRoot().append(svgContext_.getSVGContent());
    const content = svgContext_.getSVGContent();

    svgCanvas.current_drawing_ = new draw.Drawing(svgContext_.getSVGContent(), svgContext_.getIdPrefix());

    // retrieve or set the nonce
    const nonce = svgCanvas.getCurrentDrawing().getNonce();
    if (nonce) {
      svgContext_.call('setnonce', nonce);
    } else {
      svgContext_.call('unsetnonce');
    }

    // change image href vals if possible
    const elements = content.querySelectorAll('image');
    Array.prototype.forEach.call(elements, function (image) {
      preventClickDefault(image);
      const val = svgCanvas.getHref(image);
      if (val) {
        if (val.startsWith('data:')) {
          // Check if an SVG-edit data URI
          const m = val.match(/svgedit_url=(.*?);/);
          // const m = val.match(/svgedit_url=(?<url>.*?);/);
          if (m) {
            const url = decodeURIComponent(m[1]);
            // const url = decodeURIComponent(m.groups.url);
            const iimg = new Image();
            iimg.addEventListener("load", () => {
              image.setAttributeNS(NS.XLINK, 'xlink:href', url);
            });
            iimg.src = url;
          }
        }
        // Add to encodableImages if it loads
        svgCanvas.embedImage(val);
      }
    });
    // Duplicate id replace changes
    const nodes = content.querySelectorAll('[id]');
    const ids = {};
    const totalNodes = nodes.length;

    for(let i=0; i<totalNodes; i++) {
      const currentId = nodes[i].id ? nodes[i].id : "undefined";
      if(isNaN(ids[currentId])) {
        ids[currentId] = 0;
      }
      ids[currentId]++;
    }

    Object.entries(ids).forEach(([ key, value ]) => {
      if (value > 1) {
        const nodes = content.querySelectorAll('[id="'+key+'"]');
        for(let i=1; i<nodes.length; i++) {
          nodes[i].setAttribute('id', svgCanvas.getNextId());
        }
      }
    });


    // Wrap child SVGs in group elements
    const svgElements = content.querySelectorAll('svg');
    Array.prototype.forEach.call(svgElements, function (element) {
      // Skip if it's in a <defs>
      if (getClosest(element.parentNode, 'defs')) { return; }

      svgCanvas.uniquifyElems(element);

      // Check if it already has a gsvg group
      const pa = element.parentNode;
      if (pa.childNodes.length === 1 && pa.nodeName === 'g') {
        dataStorage.put(pa, 'gsvg', element);
        pa.id = pa.id || svgCanvas.getNextId();
      } else {
        svgCanvas.groupSvgElem(element);
      }
    });

    // For Firefox: Put all paint elems in defs
    if (isGecko()) {
      const svgDefs = findDefs();
      const findElems = content.querySelectorAll('linearGradient, radialGradient, pattern');
      Array.prototype.forEach.call(findElems, function (ele) {
        svgDefs.appendChild(ele);
      });
    }

    // Set ref element for <use> elements

    // TODO: This should also be done if the object is re-added through "redo"
    svgCanvas.setUseData(content);

    svgCanvas.convertGradients(content);

    const attrs = {
      id: 'svgcontent',
      overflow: curConfig.show_outside_canvas ? 'visible' : 'hidden'
    };

    let percs = false;

    // determine proper size
    if (content.getAttribute('viewBox')) {
      const viBox = content.getAttribute('viewBox');
      const vb = viBox.split(' ');
      attrs.width = vb[2];
      attrs.height = vb[3];
      // handle content that doesn't have a viewBox
    } else {
      [ 'width', 'height' ].forEach(function (dim) {
        // Set to 100 if not given
        const val = content.getAttribute(dim) || '100%';
        if (String(val).substr(-1) === '%') {
          // Use user units if percentage given
          percs = true;
        } else {
          attrs[dim] = convertToNum(dim, val);
        }
      });
    }

    // identify layers
    draw.identifyLayers();

    // Give ID for any visible layer children missing one
    const chiElems = content.children;
    Array.prototype.forEach.call(chiElems, function (chiElem) {
      const visElems = chiElem.querySelectorAll(svgContext_.getVisElems());
      Array.prototype.forEach.call(visElems, function (elem) {
        if (!elem.id) { elem.id = svgCanvas.getNextId(); }
      });
    });

    // Percentage width/height, so let's base it on visible elements
    if (percs) {
      const bb = getStrokedBBoxDefaultVisible();
      attrs.width = bb.width + bb.x;
      attrs.height = bb.height + bb.y;
    }

    // Just in case negative numbers are given or
    // result from the percs calculation
    if (attrs.width <= 0) { attrs.width = 100; }
    if (attrs.height <= 0) { attrs.height = 100; }

    for (const [ key, value ] of Object.entries(attrs)) {
      content.setAttribute(key, value);
    }
    svgCanvas.contentW = attrs.width;
    svgCanvas.contentH = attrs.height;

    batchCmd.addSubCommand(new InsertElementCommand(svgContext_.getSVGContent()));
    // update root to the correct size
    const width = content.getAttribute('width');
    const height = content.getAttribute('height');
    const changes = { width: width, height: height };
    batchCmd.addSubCommand(new ChangeElementCommand(svgContext_.getSVGRoot(), changes));

    // reset zoom
    // svgContext_.setCurrentZoom(1);

    svgCanvas.clearSelection();
    pathModule.clearData();
    svgContext_.getSVGRoot().append(svgCanvas.selectorManager.selectorParentGroup);

    if (!preventUndo) svgContext_.addCommandToHistory(batchCmd);
    svgContext_.call('changed', [ svgContext_.getSVGContent() ]);
  } catch (e) {
    console.error(e);
    return false;
  }

  return true;
};

/**
* This function imports the input SVG XML as a `<symbol>` in the `<defs>`, then adds a
* `<use>` to the current layer.
* @function module:svgcanvas.SvgCanvas#importSvgString
* @param {string} xmlString - The SVG as XML text.
* @fires module:svgcanvas.SvgCanvas#event:changed
* @returns {null|Element} This function returns null if the import was unsuccessful, or the element otherwise.
* @todo
* - properly handle if namespace is introduced by imported content (must add to svgcontent
* and update all prefixes in the imported node)
* - properly handle recalculating dimensions, `recalculateDimensions()` doesn't handle
* arbitrary transform lists, but makes some assumptions about how the transform list
* was obtained
*/
export const importSvgString = function (xmlString) {
  const dataStorage = svgContext_.getDataStorage();
  let j; let ts; let useEl;
  try {
    // Get unique ID
    const uid = encode64(xmlString.length + xmlString).substr(0, 32);

    let useExisting = false;
    // Look for symbol and make sure symbol exists in image
    if (svgContext_.getImportIds(uid) && svgContext_.getImportIds(uid).symbol) {
      const parents = getParents(svgContext_.getImportIds(uid).symbol, '#svgroot');
      if (parents.length) {
        useExisting = true;
      }
    }

    const batchCmd = new BatchCommand('Import Image');
    let symbol;
    if (useExisting) {
      symbol = svgContext_.getImportIds(uid).symbol;
      ts = svgContext_.getImportIds(uid).xform;
    } else {
      // convert string into XML document
      const newDoc = text2xml(xmlString);

      svgCanvas.prepareSvg(newDoc);

      // import new svg document into our document
      // If DOM3 adoptNode() available, use it. Otherwise fall back to DOM2 importNode()
      const svg = svgContext_.getDOMDocument().adoptNode
        ? svgContext_.getDOMDocument().adoptNode(newDoc.documentElement)
        : svgContext_.getDOMDocument().importNode(newDoc.documentElement, true);

      svgCanvas.uniquifyElems(svg);

      const innerw = convertToNum('width', svg.getAttribute('width'));
      const innerh = convertToNum('height', svg.getAttribute('height'));
      const innervb = svg.getAttribute('viewBox');
      // if no explicit viewbox, create one out of the width and height
      const vb = innervb ? innervb.split(' ') : [ 0, 0, innerw, innerh ];
      for (j = 0; j < 4; ++j) {
        vb[j] = Number(vb[j]);
      }

      // TODO: properly handle preserveAspectRatio
      const // canvasw = +svgcontent.getAttribute('width'),
        canvash = Number(svgContext_.getSVGContent().getAttribute('height'));
      // imported content should be 1/3 of the canvas on its largest dimension

      ts = innerh > innerw ? 'scale(' + (canvash / 3) / vb[3] + ')' : 'scale(' + (canvash / 3) / vb[2] + ')';

      // Hack to make recalculateDimensions understand how to scale
      ts = 'translate(0) ' + ts + ' translate(0)';

      symbol = svgContext_.getDOMDocument().createElementNS(NS.SVG, 'symbol');
      const defs = findDefs();

      if (isGecko()) {
        // Move all gradients into root for Firefox, workaround for this bug:
        // https://bugzilla.mozilla.org/show_bug.cgi?id=353575
        // TODO: Make this properly undo-able.
        const elements = svg.querySelectorAll('linearGradient, radialGradient, pattern');
        Array.prototype.forEach.call(elements, function (el) {
          defs.appendChild(el);
        });
      }

      while (svg.firstChild) {
        const first = svg.firstChild;
        symbol.append(first);
      }
      const attrs = svg.attributes;
      for (const attr of attrs) { // Ok for `NamedNodeMap`
        symbol.setAttribute(attr.nodeName, attr.value);
      }
      symbol.id = svgCanvas.getNextId();

      // Store data
      svgContext_.setImportIds(uid, {
        symbol,
        xform: ts
      });

      findDefs().append(symbol);
      batchCmd.addSubCommand(new InsertElementCommand(symbol));
    }

    useEl = svgContext_.getDOMDocument().createElementNS(NS.SVG, 'use');
    useEl.id = svgCanvas.getNextId();
    svgCanvas.setHref(useEl, '#' + symbol.id);

    (svgContext_.getCurrentGroup() || svgCanvas.getCurrentDrawing().getCurrentLayer()).append(useEl);
    batchCmd.addSubCommand(new InsertElementCommand(useEl));
    svgCanvas.clearSelection();

    useEl.setAttribute('transform', ts);
    recalculateDimensions(useEl);
    dataStorage.put(useEl, 'symbol', symbol);
    dataStorage.put(useEl, 'ref', symbol);
    svgCanvas.addToSelection([ useEl ]);

    // TODO: Find way to add this in a recalculateDimensions-parsable way
    // if (vb[0] !== 0 || vb[1] !== 0) {
    //   ts = 'translate(' + (-vb[0]) + ',' + (-vb[1]) + ') ' + ts;
    // }
    svgContext_.addCommandToHistory(batchCmd);
    svgContext_.call('changed', [ svgContext_.getSVGContent() ]);
  } catch (e) {
    console.error(e);
    return null;
  }

  // we want to return the element so we can automatically select it
  return useEl;
};
/**
 * Function to run when image data is found.
 * @callback module:svgcanvas.ImageEmbeddedCallback
 * @param {string|false} result Data URL
 * @returns {void}
 */
/**
* Converts a given image file to a data URL when possible, then runs a given callback.
* @function module:svgcanvas.SvgCanvas#embedImage
* @param {string} src - The path/URL of the image
* @returns {Promise<string|false>} Resolves to a Data URL (string|false)
*/
export const embedImage = function (src) {
  // Todo: Remove this Promise in favor of making an async/await `Image.load` utility
  return new Promise(function (resolve, reject) {
    // load in the image and once it's loaded, get the dimensions
    const imgI = new Image();
    imgI.addEventListener("load", (e) => {
      // create a canvas the same size as the raster image
      const cvs = document.createElement('canvas');
      cvs.width = e.currentTarget.width;
      cvs.height = e.currentTarget.height;
      // load the raster image into the canvas
      cvs.getContext('2d').drawImage(e.currentTarget, 0, 0);
      // retrieve the data: URL
      try {
        let urldata = ';svgedit_url=' + encodeURIComponent(src);
        urldata = cvs.toDataURL().replace(';base64', urldata + ';base64');
        svgContext_.setEncodableImages(src, urldata);
      } catch (e) {
        svgContext_.setEncodableImages(src, false);
      }
      svgCanvas.setGoodImage(src);
      resolve(svgContext_.getEncodableImages(src));
    });
    imgI.addEventListener("error", (e) => {
      reject(`error loading image: ${e.currentTarget.attributes.src.value}`);
    });
    imgI.setAttribute('src', src);
  });
};

/**
* @typedef {PlainObject} module:svgcanvas.IssuesAndCodes
* @property {string[]} issueCodes The locale-independent code names
* @property {string[]} issues The localized descriptions
*/

/**
* Codes only is useful for locale-independent detection.
* @returns {module:svgcanvas.IssuesAndCodes}
*/
function getIssues() {
  const uiStrings = svgContext_.getUIStrings();
  // remove the selected outline before serializing
  svgCanvas.clearSelection();

  // Check for known CanVG issues
  const issues = [];
  const issueCodes = [];

  // Selector and notice
  const issueList = {
    feGaussianBlur: uiStrings.exportNoBlur,
    foreignObject: uiStrings.exportNoforeignObject,
    '[stroke-dasharray]': uiStrings.exportNoDashArray
  };
  const content = svgContext_.getSVGContent();

  // Add font/text check if Canvas Text API is not implemented
  if (!('font' in document.querySelector('CANVAS').getContext('2d'))) {
    issueList.text = uiStrings.exportNoText;
  }

  for (const [ sel, descr ] of Object.entries(issueList)) {
    if (content.querySelectorAll(sel).length) {
      issueCodes.push(sel);
      issues.push(descr);
    }
  }
  return { issues, issueCodes };
}
/**
* @typedef {PlainObject} module:svgcanvas.ImageExportedResults
* @property {string} datauri Contents as a Data URL
* @property {string} bloburl May be the empty string
* @property {string} svg The SVG contents as a string
* @property {string[]} issues The localization messages of `issueCodes`
* @property {module:svgcanvas.IssueCode[]} issueCodes CanVG issues found with the SVG
* @property {"PNG"|"JPEG"|"BMP"|"WEBP"|"ICO"} type The chosen image type
* @property {"image/png"|"image/jpeg"|"image/bmp"|"image/webp"} mimeType The image MIME type
* @property {Float} quality A decimal between 0 and 1 (for use with JPEG or WEBP)
* @property {string} exportWindowName A convenience for passing along a `window.name` to target a window on which the export could be added
*/

/**
* Generates a PNG (or JPG, BMP, WEBP) Data URL based on the current image,
* then calls "exported" with an object including the string, image
* information, and any issues found.
* @function module:svgcanvas.SvgCanvas#rasterExport
* @param {"PNG"|"JPEG"|"BMP"|"WEBP"|"ICO"} [imgType="PNG"]
* @param {Float} [quality] Between 0 and 1
* @param {string} [exportWindowName]
* @param {PlainObject} [opts]
* @param {boolean} [opts.avoidEvent]
* @fires module:svgcanvas.SvgCanvas#event:exported
* @todo Confirm/fix ICO type
* @returns {Promise<module:svgcanvas.ImageExportedResults>} Resolves to {@link module:svgcanvas.ImageExportedResults}
*/
export const rasterExport = async function (imgType, quality, exportWindowName, opts = {}) {
  const type = imgType === 'ICO' ? 'BMP' : (imgType || 'PNG');
  const mimeType = 'image/' + type.toLowerCase();
  const { issues, issueCodes } = getIssues();
  const svg = svgCanvas.svgCanvasToString();

  const iframe = document.createElement('iframe');
  iframe.onload = function() {
    const iframedoc=iframe.contentDocument||iframe.contentWindow.document;
    const ele = svgContext_.getSVGContent();
    const cln = ele.cloneNode(true);
    iframedoc.body.appendChild(cln);
    setTimeout(function(){
      // eslint-disable-next-line promise/catch-or-return
      html2canvas(iframedoc.body, { useCORS: true, allowTaint: true }).then((canvas) => {
        return new Promise((resolve) => {
          const dataURLType = type.toLowerCase();
          const datauri = quality
            ? canvas.toDataURL('image/' + dataURLType, quality)
            : canvas.toDataURL('image/' + dataURLType);
          iframe.parentNode.removeChild(iframe);
          let bloburl;

          function done() {
            const obj = {
              datauri, bloburl, svg, issues, issueCodes, type: imgType,
              mimeType, quality, exportWindowName
            };
            if (!opts.avoidEvent) {
              svgContext_.call('exported', obj);
            }
            resolve(obj);
          }
          if (canvas.toBlob) {
            canvas.toBlob((blob) => {
              bloburl = createObjectURL(blob);
              done();
            }, mimeType, quality);
            return;
          }
          bloburl = dataURLToObjectURL(datauri);
          done();
        });
      });
    }, 1000);
  };
  document.body.appendChild(iframe);
};

/**
* @typedef {void|"save"|"arraybuffer"|"blob"|"datauristring"|"dataurlstring"|"dataurlnewwindow"|"datauri"|"dataurl"} external:jsPDF.OutputType
* @todo Newer version to add also allows these `outputType` values "bloburi"|"bloburl" which return strings, so document here and for `outputType` of `module:svgcanvas.PDFExportedResults` below if added
*/
/**
* @typedef {PlainObject} module:svgcanvas.PDFExportedResults
* @property {string} svg The SVG PDF output
* @property {string|ArrayBuffer|Blob|window} output The output based on the `outputType`;
* if `undefined`, "datauristring", "dataurlstring", "datauri",
* or "dataurl", will be a string (`undefined` gives a document, while the others
* build as Data URLs; "datauri" and "dataurl" change the location of the current page); if
* "arraybuffer", will return `ArrayBuffer`; if "blob", returns a `Blob`;
* if "dataurlnewwindow", will change the current page's location and return a string
* if in Safari and no window object is found; otherwise opens in, and returns, a new `window`
* object; if "save", will have the same return as "dataurlnewwindow" if
* `navigator.getUserMedia` support is found without `URL.createObjectURL` support; otherwise
* returns `undefined` but attempts to save
* @property {external:jsPDF.OutputType} outputType
* @property {string[]} issues The human-readable localization messages of corresponding `issueCodes`
* @property {module:svgcanvas.IssueCode[]} issueCodes
* @property {string} exportWindowName
*/

/**
* Generates a PDF based on the current image, then calls "exportedPDF" with
* an object including the string, the data URL, and any issues found.
* @function module:svgcanvas.SvgCanvas#exportPDF
* @param {string} [exportWindowName] Will also be used for the download file name here
* @param {external:jsPDF.OutputType} [outputType="dataurlstring"]
* @fires module:svgcanvas.SvgCanvas#event:exportedPDF
* @returns {Promise<module:svgcanvas.PDFExportedResults>} Resolves to {@link module:svgcanvas.PDFExportedResults}
*/
export const exportPDF = async (
  exportWindowName,
  outputType = isChrome() ? 'save' : undefined
) => {
  const res = svgCanvas.getResolution();
  const orientation = res.w > res.h ? 'landscape' : 'portrait';
  const unit = 'pt'; // curConfig.baseUnit; // We could use baseUnit, but that is presumably not intended for export purposes
  const iframe = document.createElement('iframe');
  iframe.onload = function() {
    const iframedoc=iframe.contentDocument||iframe.contentWindow.document;
    const ele = svgContext_.getSVGContent();
    const cln = ele.cloneNode(true);
    iframedoc.body.appendChild(cln);
    setTimeout(function(){
      // eslint-disable-next-line promise/catch-or-return
      html2canvas(iframedoc.body, { useCORS: true, allowTaint: true }).then((canvas) => {
        const imgData = canvas.toDataURL('image/png');
        const doc = new jsPDF({
          orientation: orientation,
          unit: unit,
          format: [ res.w, res.h ]
        });
        const docTitle = svgCanvas.getDocumentTitle();
        doc.setProperties({
          title: docTitle
        });
        doc.addImage(imgData, 'PNG', 0, 0, res.w, res.h);
        iframe.parentNode.removeChild(iframe);
        const { issues, issueCodes } = getIssues();
        outputType = outputType || 'dataurlstring';
        const obj = { issues, issueCodes, exportWindowName, outputType };
        obj.output = doc.output(outputType, outputType === 'save' ? (exportWindowName || 'svg.pdf') : undefined);
        svgContext_.call('exportedPDF', obj);
        return obj;
      });
    }, 1000);
  };
  document.body.appendChild(iframe);
};
/**
* Ensure each element has a unique ID.
* @function module:svgcanvas.SvgCanvas#uniquifyElems
* @param {Element} g - The parent element of the tree to give unique IDs
* @returns {void}
*/
export const uniquifyElemsMethod = function (g) {
  const ids = {};
  // TODO: Handle markers and connectors. These are not yet re-identified properly
  // as their referring elements do not get remapped.
  //
  // <marker id='se_marker_end_svg_7'/>
  // <polyline id='svg_7' se:connector='svg_1 svg_6' marker-end='url(#se_marker_end_svg_7)'/>
  //
  // Problem #1: if svg_1 gets renamed, we do not update the polyline's se:connector attribute
  // Problem #2: if the polyline svg_7 gets renamed, we do not update the marker id nor the polyline's marker-end attribute
  const refElems = [ 'filter', 'linearGradient', 'pattern', 'radialGradient', 'symbol', 'textPath', 'use' ];

  walkTree(g, function (n) {
    // if it's an element node
    if (n.nodeType === 1) {
      // and the element has an ID
      if (n.id) {
        // and we haven't tracked this ID yet
        if (!(n.id in ids)) {
          // add this id to our map
          ids[n.id] = { elem: null, attrs: [], hrefs: [] };
        }
        ids[n.id].elem = n;
      }

      // now search for all attributes on this element that might refer
      // to other elements
      svgContext_.getrefAttrs().forEach(function(attr){
        const attrnode = n.getAttributeNode(attr);
        if (attrnode) {
          // the incoming file has been sanitized, so we should be able to safely just strip off the leading #
          const url = svgCanvas.getUrlFromAttr(attrnode.value);
          const refid = url ? url.substr(1) : null;
          if (refid) {
            if (!(refid in ids)) {
              // add this id to our map
              ids[refid] = { elem: null, attrs: [], hrefs: [] };
            }
            ids[refid].attrs.push(attrnode);
          }
        }
      });

      // check xlink:href now
      const href = svgCanvas.getHref(n);
      // TODO: what if an <image> or <a> element refers to an element internally?
      if (href && refElems.includes(n.nodeName)) {
        const refid = href.substr(1);
        if (refid) {
          if (!(refid in ids)) {
            // add this id to our map
            ids[refid] = { elem: null, attrs: [], hrefs: [] };
          }
          ids[refid].hrefs.push(n);
        }
      }
    }
  });

  // in ids, we now have a map of ids, elements and attributes, let's re-identify
  for (const oldid in ids) {
    if (!oldid) { continue; }
    const { elem } = ids[oldid];
    if (elem) {
      const newid = svgCanvas.getNextId();

      // assign element its new id
      elem.id = newid;

      // remap all url() attributes
      const { attrs } = ids[oldid];
      let j = attrs.length;
      while (j--) {
        const attr = attrs[j];
        attr.ownerElement.setAttribute(attr.name, 'url(#' + newid + ')');
      }

      // remap all href attributes
      const hreffers = ids[oldid].hrefs;
      let k = hreffers.length;
      while (k--) {
        const hreffer = hreffers[k];
        svgCanvas.setHref(hreffer, '#' + newid);
      }
    }
  }
};

/**
* Assigns reference data for each use element.
* @function module:svgcanvas.SvgCanvas#setUseData
* @param {Element} parent
* @returns {void}
*/
export const setUseDataMethod = function (parent) {
  let elems = parent;

  if (parent.tagName !== 'use') {
    // elems = elems.find('use');
    elems = elems.querySelectorAll('use');
  }

  Array.prototype.forEach.call(elems, function (el, _) {
    const dataStorage = svgContext_.getDataStorage();
    const id = svgCanvas.getHref(el).substr(1);
    const refElem = svgCanvas.getElem(id);
    if (!refElem) { return; }
    dataStorage.put(el, 'ref', refElem);
    if (refElem.tagName === 'symbol' || refElem.tagName === 'svg') {
      dataStorage.put(el, 'symbol', refElem);
      dataStorage.put(el, 'ref', refElem);
    }
  });
};

/**
* Looks at DOM elements inside the `<defs>` to see if they are referred to,
* removes them from the DOM if they are not.
* @function module:svgcanvas.SvgCanvas#removeUnusedDefElems
* @returns {Integer} The number of elements that were removed
*/
export const removeUnusedDefElemsMethod = function () {
  const defs = svgContext_.getSVGContent().getElementsByTagNameNS(NS.SVG, 'defs');
  if (!defs || !defs.length) { return 0; }

  // if (!defs.firstChild) { return; }

  const defelemUses = [];
  let numRemoved = 0;
  const attrs = [ 'fill', 'stroke', 'filter', 'marker-start', 'marker-mid', 'marker-end' ];
  const alen = attrs.length;

  const allEls = svgContext_.getSVGContent().getElementsByTagNameNS(NS.SVG, '*');
  const allLen = allEls.length;

  let i; let j;
  for (i = 0; i < allLen; i++) {
    const el = allEls[i];
    for (j = 0; j < alen; j++) {
      const ref = svgCanvas.getUrlFromAttr(el.getAttribute(attrs[j]));
      if (ref) {
        defelemUses.push(ref.substr(1));
      }
    }

    // gradients can refer to other gradients
    const href = getHref(el);
    if (href && href.startsWith('#')) {
      defelemUses.push(href.substr(1));
    }
  }

  Array.prototype.forEach.call(defs, function (def, i) {
    const defelems = def.querySelectorAll('linearGradient, radialGradient, filter, marker, svg, symbol');
    i = defelems.length;
    while (i--) {
      const defelem = defelems[i];
      const { id } = defelem;
      if (!defelemUses.includes(id)) {
        // Not found, so remove (but remember)
        svgContext_.setRemovedElements(id, defelem);
        defelem.remove();
        numRemoved++;
      }
    }
  });

  return numRemoved;
};
/**
* Converts gradients from userSpaceOnUse to objectBoundingBox.
* @function module:svgcanvas.SvgCanvas#convertGradients
* @param {Element} elem
* @returns {void}
*/
export const convertGradientsMethod = function (elem) {
  let elems = elem.querySelectorAll('linearGradient, radialGradient');
  if (!elems.length && isWebkit()) {
    // Bug in webkit prevents regular *Gradient selector search
    elems = Array.prototype.filter.call(elem.querySelectorAll('*'), function (curThis) {
      return (curThis.tagName.includes('Gradient'));
    });
  }
  Array.prototype.forEach.call(elems, function (grad) {
    if (grad.getAttribute('gradientUnits') === 'userSpaceOnUse') {
      const svgcontent = svgContext_.getSVGContent();
      // TODO: Support more than one element with this ref by duplicating parent grad
      let fillStrokeElems = svgcontent.querySelectorAll('[fill="url(#' + grad.id + ')"],[stroke="url(#' + grad.id + ')"]');
      if (!fillStrokeElems.length) {
        const tmpFillStrokeElems = svgcontent.querySelectorAll('[*|href="#' + grad.id + '"]');
        if (!tmpFillStrokeElems.length) {
          return;
        } else {
          if((tmpFillStrokeElems[0].tagName === "linearGradient" || tmpFillStrokeElems[0].tagName === "radialGradient") && tmpFillStrokeElems[0].getAttribute('gradientUnits') === 'userSpaceOnUse') {
            fillStrokeElems = svgcontent.querySelectorAll('[fill="url(#' + tmpFillStrokeElems[0].id + ')"],[stroke="url(#' + tmpFillStrokeElems[0].id + ')"]');
          } else {
            return;
          }
        }
      }
      // get object's bounding box
      const bb = utilsGetBBox(fillStrokeElems[0]);

      // This will occur if the element is inside a <defs> or a <symbol>,
      // in which we shouldn't need to convert anyway.
      if (!bb) { return; }
      if (grad.tagName === 'linearGradient') {
        const gCoords = {
          x1: grad.getAttribute('x1'),
          y1: grad.getAttribute('y1'),
          x2: grad.getAttribute('x2'),
          y2: grad.getAttribute('y2')
        };

        // If has transform, convert
        const tlist = grad.gradientTransform.baseVal;
        if (tlist && tlist.numberOfItems > 0) {
          const m = transformListToTransform(tlist).matrix;
          const pt1 = transformPoint(gCoords.x1, gCoords.y1, m);
          const pt2 = transformPoint(gCoords.x2, gCoords.y2, m);

          gCoords.x1 = pt1.x;
          gCoords.y1 = pt1.y;
          gCoords.x2 = pt2.x;
          gCoords.y2 = pt2.y;
          grad.removeAttribute('gradientTransform');
        }
        grad.setAttribute('x1', (gCoords.x1 - bb.x) / bb.width);
        grad.setAttribute('y1', (gCoords.y1 - bb.y) / bb.height);
        grad.setAttribute('x2', (gCoords.x2 - bb.x) / bb.width);
        grad.setAttribute('y2', (gCoords.y2 - bb.y) / bb.height);
        grad.removeAttribute('gradientUnits');
      }
    }
  });
};