├── .gitignore ├── .npmignore ├── .travis.yml ├── .zuul.yml ├── API.md ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── bower.json ├── browser.js ├── browser.min.js ├── demo ├── index.html ├── jsdom-demo.js └── phantomjs-demo.js ├── esdoc.json ├── index.js ├── lib └── svgsaver.js ├── package.json ├── src ├── clonesvg.js ├── collection.js ├── index.js ├── saveuri.js ├── svgsaver.js └── utils.js ├── test └── svgsaver-spec.js ├── todo.md └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | bower_components 29 | 30 | # IDEs 31 | .vscode 32 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !src/* 3 | !lib/* 4 | !index.* 5 | !browser.* 6 | !README.md 7 | !package.json 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '6' 4 | -------------------------------------------------------------------------------- /.zuul.yml: -------------------------------------------------------------------------------- 1 | ui: tape 2 | browsers: 3 | - name: android 4 | version: [oldest, latest] 5 | - name: chrome 6 | version: [oldest, latest] 7 | - name: firefox 8 | version: [oldest, latest] 9 | - name: ie 10 | version: oldest..latest 11 | - name: iphone 12 | version: [oldest, latest] 13 | - name: opera 14 | version: oldest..latest 15 | - name: safari 16 | version: oldest..latest 17 | browserify: 18 | - transform: babelify 19 | -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## SvgSaver 4 | **Kind**: global class 5 | **Api**: public 6 | 7 | * [SvgSaver](#SvgSaver) 8 | * [new SvgSaver()](#new_SvgSaver_new) 9 | * [.cloneSVG(el)](#SvgSaver+cloneSVG) ⇒ SVGElement 10 | * [.getHTML(el)](#SvgSaver+getHTML) ⇒ String 11 | * [.getBlob(el)](#SvgSaver+getBlob) ⇒ Blog 12 | * [.getUri(el)](#SvgSaver+getUri) ⇒ String 13 | * [.asSvg(el, [filename])](#SvgSaver+asSvg) ⇒ [SvgSaver](#SvgSaver) 14 | * [.getPngUri(el, cb)](#SvgSaver+getPngUri) 15 | * [.asPng(el, [filename])](#SvgSaver+asPng) ⇒ [SvgSaver](#SvgSaver) 16 | 17 | 18 | 19 | ### new SvgSaver() 20 | SvgSaver constructor. 21 | 22 | **Example** 23 | ```js 24 | var svgsaver = new SvgSaver(); // creates a new instance 25 | var svg = document.querySelector('#mysvg'); // find the SVG element 26 | svgsaver.asSvg(svg); // save as SVG 27 | ``` 28 | 29 | 30 | ### svgSaver.cloneSVG(el) ⇒ SVGElement 31 | Return the cloned SVG after cleaning 32 | 33 | **Kind**: instance method of [SvgSaver](#SvgSaver) 34 | **Returns**: SVGElement - SVG text after cleaning 35 | **Api**: public 36 | 37 | | Param | Type | Description | 38 | | --- | --- | --- | 39 | | el | SVGElement | The element to copy. | 40 | 41 | 42 | 43 | ### svgSaver.getHTML(el) ⇒ String 44 | Return the SVG HTML text after cleaning 45 | 46 | **Kind**: instance method of [SvgSaver](#SvgSaver) 47 | **Returns**: String - SVG text after cleaning 48 | **Api**: public 49 | 50 | | Param | Type | Description | 51 | | --- | --- | --- | 52 | | el | SVGElement | The element to copy. | 53 | 54 | 55 | 56 | ### svgSaver.getBlob(el) ⇒ Blog 57 | Return the SVG, after cleaning, as a text/xml Blob 58 | 59 | **Kind**: instance method of [SvgSaver](#SvgSaver) 60 | **Returns**: Blog - SVG as a text/xml Blob 61 | **Api**: public 62 | 63 | | Param | Type | Description | 64 | | --- | --- | --- | 65 | | el | SVGElement | The element to copy. | 66 | 67 | 68 | 69 | ### svgSaver.getUri(el) ⇒ String 70 | Return the SVG, after cleaning, as a image/svg+xml;base64 URI encoded string 71 | 72 | **Kind**: instance method of [SvgSaver](#SvgSaver) 73 | **Returns**: String - SVG as image/svg+xml;base64 URI encoded string 74 | **Api**: public 75 | 76 | | Param | Type | Description | 77 | | --- | --- | --- | 78 | | el | SVGElement | The element to copy. | 79 | 80 | 81 | 82 | ### svgSaver.asSvg(el, [filename]) ⇒ [SvgSaver](#SvgSaver) 83 | Saves the SVG as a SVG file using method compatible with the browser 84 | 85 | **Kind**: instance method of [SvgSaver](#SvgSaver) 86 | **Returns**: [SvgSaver](#SvgSaver) - The SvgSaver instance 87 | **Api**: public 88 | 89 | | Param | Type | Description | 90 | | --- | --- | --- | 91 | | el | SVGElement | The element to copy. | 92 | | [filename] | string | The filename to save, defaults to the SVG title or 'untitled.svg' | 93 | 94 | 95 | 96 | ### svgSaver.getPngUri(el, cb) 97 | Gets the SVG as a PNG data URI. 98 | 99 | **Kind**: instance method of [SvgSaver](#SvgSaver) 100 | **Api**: public 101 | 102 | | Param | Type | Description | 103 | | --- | --- | --- | 104 | | el | SVGElement | The element to copy. | 105 | | cb | function | Call back called with the PNG data uri. | 106 | 107 | 108 | 109 | ### svgSaver.asPng(el, [filename]) ⇒ [SvgSaver](#SvgSaver) 110 | Saves the SVG as a PNG file using method compatible with the browser 111 | 112 | **Kind**: instance method of [SvgSaver](#SvgSaver) 113 | **Returns**: [SvgSaver](#SvgSaver) - The SvgSaver instance 114 | **Api**: public 115 | 116 | | Param | Type | Description | 117 | | --- | --- | --- | 118 | | el | SVGElement | The element to copy. | 119 | | [filename] | string | The filename to save, defaults to the SVG title or 'untitled.png' | 120 | 121 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | ## HEAD (Unreleased) 5 | _(none)_ 6 | 7 | -------------------- 8 | 9 | ## 0.9.0 (2017-05-08) 10 | * Now includes FileSaver as a dependency. 11 | * Fix duplicated xmlns in IE 11 #7. 12 | * Display error message in IE <= 11 in unsupported methods (`getPngUri` and `asPng`). 13 | * Disable unsupported methods in demo. 14 | 15 | ## 0.8.2 (2017-04-25) 16 | * Fix incorrect SVG being found 17 | * Fix tests 18 | 19 | ## 0.8.1 (2017-04-25) 20 | * Add xlink namespace declaration, fixes Safari and Firefox. 21 | 22 | ## 0.8.0 (2017-04-25) 23 | * Improved demo 24 | * Added cloneSVG public API 25 | * Added getPngUri public API 26 | * Added xlink:href to allowed attrs 27 | 28 | ## 0.7.0 (2017-04-12) 29 | * Fix phantomjs dev in package.json 30 | 31 | ## 0.6.2 (2016-11-11) 32 | * Fix #4, Add href to default attributes 33 | 34 | ## 0.6.1 (2015-11-17) 35 | * Fix #1, The string to be encoded contains characters outside of the Latin1 range. 36 | 37 | ## 0.6.0 (2015-11-16) 38 | * URI encode filenames 39 | * Flexible element selection 40 | * Throw error if no SVGs can be found 41 | 42 | ## 0.5.0 (2015-11-13) 43 | * Support Firefox without FileSaver 44 | 45 | ## 0.4.0 (2015-11-05) 46 | * Clean superfluous inheritable styles 47 | 48 | ## 0.3.3 (2015-10-28) 49 | * Use latest computed-styles, fixes IE11 50 | 51 | ## 0.3.2 (2015-10-27) 52 | * Internal change to use latest computed-styles 53 | 54 | ## 0.3.1 (2015-10-23) 55 | * Default is now to copy all inherited styles, regardless of value 56 | 57 | ## 0.3.0 (2015-10-20) 58 | * Now using copy-styles module 59 | * No longer removes styles from elements that match parent styles (not all styles are inheritable) 60 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2015 Jayson Harshbarger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # svgsaver 2 | 3 | Download an SVG element as an SVG or PNG file, including CSS defined styles. 4 | 5 | [![NPM version][npm-badge]][npm] 6 | [![Downloads][download-badge]][npm] 7 | ![Downloads][bower-badge] 8 | 9 | [![Build Status][travis-image]][travis-url] 10 | [![Codacy Badge][codacy-badge]][Codacy] 11 | 12 | [![js-semistandard-style][standard-badge]][semistandard] 13 | [![License][license-badge]][MIT License] 14 | 15 | ## Features 16 | - Download `` by element object. 17 | - Download as SVG or PNG file. 18 | - Copies SVG element styles as rendered in the browser, including styles defined in CSS style sheets. 19 | - Copies only SVG relevant and non-default styles. [See here](http://www.w3.org/TR/SVG/propidx.html). 20 | - Computed styles are in-lined for maximum compatibility. 21 | 22 | ## Install 23 | 24 | ### Node 25 | 26 | ```js 27 | npm install svgsaver 28 | ``` 29 | 30 | ### Bower 31 | 32 | ```js 33 | bower install svgsaver 34 | ``` 35 | 36 | ### JSPM 37 | 38 | ```js 39 | jspm install svgsaver=npm:svgsaver 40 | ``` 41 | 42 | ## Usage 43 | 44 | *For maximum compatibility across browsers include [eligrey/FileSaver.js/](https://github.com/eligrey/FileSaver.js) and [eligrey/canvas-toBlob.js](https://github.com/eligrey/canvas-toBlob.js). See [Compatibility-Chart](https://github.com/Hypercubed/svgsaver/wiki/Compatibility-Chart) for more information.* 45 | 46 | ### Example 47 | 48 | ``` 49 | var SvgSaver = require('svgsaver'); // if using CommonJS environment 50 | var svgsaver = new SvgSaver(); // creates a new instance 51 | var svg = document.querySelector('#mysvg'); // find the SVG element 52 | svgsaver.asSvg(svg); // save as SVG 53 | ``` 54 | 55 | ### Demos 56 | 57 | - [Epicyclic Gearing](http://bl.ocks.org/Hypercubed/db9e99d761f90d87cf43) - d3 58 | - [Superformula Explorer](http://bl.ocks.org/Hypercubed/58fff7215e53d6565f32) - d3 59 | - [City Construction Site](http://codepen.io/Hypercubed/pen/OyWadQ) - jQuery and TweenMax 60 | - [Chiasm Boilerplate (with download buttons)](http://bl.ocks.org/Hypercubed/b01a767b41b0e679aade) - Chiasm 61 | 62 | ## Acknowledgments 63 | Based on previous work on [Hypercubed/angular-downloadsvg-directive](https://github.com/Hypercubed/angular-downloadsvg-directive). Some portions of this code inspired by [raw](https://github.com/densitydesign/raw/blob/master/js/directives.js) and [moagrius/copycss](https://github.com/moagrius/copycss). 64 | 65 | ## License 66 | [MIT License] 67 | 68 | [npm]: https://npmjs.org/package/svgsaver 69 | [bower]: https://npmjs.org/package/svgsaver 70 | [semistandard]: https://github.com/Flet/semistandard 71 | [Codacy]: https://www.codacy.com/app/hypercubed/svgsaver 72 | [MIT License]: http://en.wikipedia.org/wiki/MIT_License 73 | [travis-url]: https://travis-ci.org/Hypercubed/svgsaver 74 | 75 | [travis-image]: https://img.shields.io/travis/Hypercubed/svgsaver.svg 76 | [npm-badge]: https://img.shields.io/npm/v/svgsaver.svg 77 | [bower-badge]: https://img.shields.io/bower/v/svgsaver.svg 78 | [standard-badge]: https://img.shields.io/badge/code%20style-semistandard-brightgreen.svg 79 | [download-badge]: http://img.shields.io/npm/dm/svgsaver.svg 80 | [codacy-badge]: https://api.codacy.com/project/badge/6fe47dae30b34d2da78572b3ea36cfe0 81 | [license-badge]: https://img.shields.io/badge/license-MIT-blue.svg 82 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svgsaver", 3 | "description": "download an SVG element with css styles", 4 | "main": "browser.js", 5 | "homepage": "https://github.com/Hypercubed/svgsaver", 6 | "authors": [ 7 | "J. Harshbarger " 8 | ], 9 | "moduleType": [ 10 | "amd", 11 | "globals", 12 | "node" 13 | ], 14 | "keywords": [ 15 | "svg", 16 | "FileSaver", 17 | "dom", 18 | "css", 19 | "png" 20 | ], 21 | "license": "MIT", 22 | "ignore": [ 23 | "**/.*", 24 | "node_modules", 25 | "bower_components", 26 | "test", 27 | "tests", 28 | "demo" 29 | ], 30 | "dependencies": { 31 | "FileSaver.js": "~0.0.2" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /browser.js: -------------------------------------------------------------------------------- 1 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.SvgSaver = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 2 | 3 | 4 | 5 | DownloadSVG demo 6 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 |

46 | 47 |
48 | 51 |
52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 |
78 |
79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /demo/jsdom-demo.js: -------------------------------------------------------------------------------- 1 | var jsdom = require('jsdom'); 2 | var SvgSaver = require('../'); 3 | 4 | const octocatText = ` 5 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | `; 31 | 32 | jsdom.env( 33 | octocatText, 34 | ['http://code.jquery.com/jquery.js'], 35 | function (_err, window) { 36 | global.window = window; 37 | const octocat = window.document.querySelector('#octocat'); 38 | 39 | const svgSaver = new SvgSaver(); 40 | 41 | console.log(svgSaver.getHTML(octocat)); 42 | } 43 | ); 44 | -------------------------------------------------------------------------------- /demo/phantomjs-demo.js: -------------------------------------------------------------------------------- 1 | /* global phantom */ 2 | 'use strict'; 3 | 4 | var SvgSaver = require('../'); 5 | 6 | var octocatText = '\n\t\n\t\n\t \n\t \n\t\t\n\t\t\n\t \n\t \n\t \n\t \n\t \n\t \n\t \n\t \n\t \n\t \n\t \n\t'; 7 | 8 | var pg = document.createElement('div'); 9 | document.body.appendChild(pg); 10 | pg.innerHTML = octocatText; 11 | 12 | var octocat = window.document.querySelector('#octocat'); 13 | var svgSaver = new SvgSaver(); 14 | 15 | console.log(svgSaver.getHTML(octocat)); 16 | phantom.exit(); 17 | -------------------------------------------------------------------------------- /esdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": "./src", 3 | "destination": "./esdoc", 4 | "test": { 5 | "type": "mocha", 6 | "source": "./test" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/svgsaver'); 2 | -------------------------------------------------------------------------------- /lib/svgsaver.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, '__esModule', { 4 | value: true 5 | }); 6 | 7 | var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); 8 | 9 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } 10 | 11 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } 12 | 13 | var _computedStyles = require('computed-styles'); 14 | 15 | var _computedStyles2 = _interopRequireDefault(_computedStyles); 16 | 17 | var _fileSaver = require('file-saver'); 18 | 19 | var _fileSaver2 = _interopRequireDefault(_fileSaver); 20 | 21 | var svgStyles = { // Whitelist of CSS styles and default values 22 | 'alignment-baseline': 'auto', 23 | 'baseline-shift': 'baseline', 24 | 'clip': 'auto', 25 | 'clip-path': 'none', 26 | 'clip-rule': 'nonzero', 27 | 'color': 'rgb(51, 51, 51)', 28 | 'color-interpolation': 'srgb', 29 | 'color-interpolation-filters': 'linearrgb', 30 | 'color-profile': 'auto', 31 | 'color-rendering': 'auto', 32 | 'cursor': 'auto', 33 | 'direction': 'ltr', 34 | 'display': 'inline', 35 | 'dominant-baseline': 'auto', 36 | 'enable-background': '', 37 | 'fill': 'rgb(0, 0, 0)', 38 | 'fill-opacity': '1', 39 | 'fill-rule': 'nonzero', 40 | 'filter': 'none', 41 | 'flood-color': 'rgb(0, 0, 0)', 42 | 'flood-opacity': '1', 43 | 'font': '', 44 | 'font-family': 'normal', 45 | 'font-size': 'medium', 46 | 'font-size-adjust': 'auto', 47 | 'font-stretch': 'normal', 48 | 'font-style': 'normal', 49 | 'font-variant': 'normal', 50 | 'font-weight': '400', 51 | 'glyph-orientation-horizontal': '0deg', 52 | 'glyph-orientation-vertical': 'auto', 53 | 'image-rendering': 'auto', 54 | 'kerning': 'auto', 55 | 'letter-spacing': '0', 56 | 'lighting-color': 'rgb(255, 255, 255)', 57 | 'marker': '', 58 | 'marker-end': 'none', 59 | 'marker-mid': 'none', 60 | 'marker-start': 'none', 61 | 'mask': 'none', 62 | 'opacity': '1', 63 | 'overflow': 'visible', 64 | 'paint-order': 'fill', 65 | 'pointer-events': 'auto', 66 | 'shape-rendering': 'auto', 67 | 'stop-color': 'rgb(0, 0, 0)', 68 | 'stop-opacity': '1', 69 | 'stroke': 'none', 70 | 'stroke-dasharray': 'none', 71 | 'stroke-dashoffset': '0', 72 | 'stroke-linecap': 'butt', 73 | 'stroke-linejoin': 'miter', 74 | 'stroke-miterlimit': '4', 75 | 'stroke-opacity': '1', 76 | 'stroke-width': '1', 77 | 'text-anchor': 'start', 78 | 'text-decoration': 'none', 79 | 'text-rendering': 'auto', 80 | 'unicode-bidi': 'normal', 81 | 'visibility': 'visible', 82 | 'word-spacing': '0px', 83 | 'writing-mode': 'lr-tb' 84 | }; 85 | 86 | var svgAttrs = [// white list of attributes 87 | 'id', 'xml: base', 'xml: lang', 'xml: space', // Core 88 | 'height', 'result', 'width', 'x', 'y', // Primitive 89 | 'xlink: href', // Xlink attribute 90 | 'href', 'style', 'class', 'd', 'pathLength', // Path 91 | 'x', 'y', 'dx', 'dy', 'glyphRef', 'format', 'x1', 'y1', 'x2', 'y2', 'rotate', 'textLength', 'cx', 'cy', 'r', 'rx', 'ry', 'fx', 'fy', 'width', 'height', 'refX', 'refY', 'orient', 'markerUnits', 'markerWidth', 'markerHeight', 'maskUnits', 'transform', 'viewBox', 'version', // Container 92 | 'preserveAspectRatio', 'xmlns', 'points', // Polygons 93 | 'offset', 'xlink:href']; 94 | 95 | // http://www.w3.org/TR/SVG/propidx.html 96 | // via https://github.com/svg/svgo/blob/master/plugins/_collections.js 97 | var inheritableAttrs = ['clip-rule', 'color', 'color-interpolation', 'color-interpolation-filters', 'color-profile', 'color-rendering', 'cursor', 'direction', 'fill', 'fill-opacity', 'fill-rule', 'font', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'glyph-orientation-horizontal', 'glyph-orientation-vertical', 'image-rendering', 'kerning', 'letter-spacing', 'marker', 'marker-end', 'marker-mid', 'marker-start', 'pointer-events', 'shape-rendering', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'text-anchor', 'text-rendering', 'transform', 'visibility', 'white-space', 'word-spacing', 'writing-mode']; 98 | 99 | /* Some simple utilities */ 100 | 101 | var isFunction = function isFunction(a) { 102 | return typeof a === 'function'; 103 | }; 104 | var isDefined = function isDefined(a) { 105 | return typeof a !== 'undefined'; 106 | }; 107 | var isUndefined = function isUndefined(a) { 108 | return typeof a === 'undefined'; 109 | }; 110 | var isObject = function isObject(a) { 111 | return a !== null && typeof a === 'object'; 112 | }; 113 | 114 | // from https://github.com/npm-dom/is-dom/blob/master/index.js 115 | function isNode(val) { 116 | if (!isObject(val)) { 117 | return false; 118 | } 119 | if (isDefined(window) && isObject(window.Node)) { 120 | return val instanceof window.Node; 121 | } 122 | return typeof val.nodeType === 'number' && typeof val.nodeName === 'string'; 123 | } 124 | 125 | /* Some utilities for cloning SVGs with inline styles */ 126 | // Removes attributes that are not valid for SVGs 127 | function cleanAttrs(el, attrs, styles) { 128 | // attrs === false - remove all, attrs === true - allow all 129 | if (attrs === true) { 130 | return; 131 | } 132 | 133 | Array.prototype.slice.call(el.attributes).forEach(function (attr) { 134 | // remove if it is not style nor on attrs whitelist 135 | // keeping attributes that are also styles because attributes override 136 | if (attr.specified) { 137 | if (attrs === '' || attrs === false || isUndefined(styles[attr.name]) && attrs.indexOf(attr.name) < 0) { 138 | el.removeAttribute(attr.name); 139 | } 140 | } 141 | }); 142 | } 143 | 144 | function cleanStyle(tgt, parentStyles) { 145 | parentStyles = parentStyles || tgt.parentNode.style; 146 | inheritableAttrs.forEach(function (key) { 147 | if (tgt.style[key] === parentStyles[key]) { 148 | tgt.style.removeProperty(key); 149 | } 150 | }); 151 | } 152 | 153 | function domWalk(src, tgt, down, up) { 154 | down(src, tgt); 155 | var children = src.childNodes; 156 | for (var i = 0; i < children.length; i++) { 157 | domWalk(children[i], tgt.childNodes[i], down, up); 158 | } 159 | up(src, tgt); 160 | } 161 | 162 | // Clones an SVGElement, copies approprate atttributes and styles. 163 | function cloneSvg(src, attrs, styles) { 164 | var clonedSvg = src.cloneNode(true); 165 | 166 | domWalk(src, clonedSvg, function (src, tgt) { 167 | if (tgt.style) { 168 | (0, _computedStyles2['default'])(src, tgt.style, styles); 169 | } 170 | }, function (src, tgt) { 171 | if (tgt.style && tgt.parentNode) { 172 | cleanStyle(tgt); 173 | } 174 | if (tgt.attributes) { 175 | cleanAttrs(tgt, attrs, styles); 176 | } 177 | }); 178 | 179 | return clonedSvg; 180 | } 181 | 182 | /* global Image, MouseEvent */ 183 | 184 | /* Some simple utilities for saving SVGs, including an alternative to saveAs */ 185 | 186 | // detection 187 | var DownloadAttributeSupport = typeof document !== 'undefined' && 'download' in document.createElement('a') && typeof MouseEvent === 'function'; 188 | 189 | function saveUri(uri, name) { 190 | if (DownloadAttributeSupport) { 191 | var dl = document.createElement('a'); 192 | dl.setAttribute('href', uri); 193 | dl.setAttribute('download', name); 194 | // firefox doesn't support `.click()`... 195 | // from https://github.com/sindresorhus/multi-download/blob/gh-pages/index.js 196 | dl.dispatchEvent(new MouseEvent('click')); 197 | return true; 198 | } else if (typeof window !== 'undefined') { 199 | window.open(uri, '_blank', ''); 200 | return true; 201 | } 202 | 203 | return false; 204 | } 205 | 206 | function createCanvas(uri, name, cb) { 207 | var canvas = document.createElement('canvas'); 208 | var context = canvas.getContext('2d'); 209 | 210 | var image = new Image(); 211 | image.onload = function () { 212 | canvas.width = image.width; 213 | canvas.height = image.height; 214 | context.drawImage(image, 0, 0); 215 | 216 | cb(canvas); 217 | }; 218 | image.src = uri; 219 | return true; 220 | } 221 | 222 | function savePng(uri, name) { 223 | return createCanvas(uri, name, function (canvas) { 224 | if (isDefined(canvas.toBlob)) { 225 | canvas.toBlob(function (blob) { 226 | _fileSaver2['default'].saveAs(blob, name); 227 | }); 228 | } else { 229 | saveUri(canvas.toDataURL('image/png'), name); 230 | } 231 | }); 232 | } 233 | 234 | /* global Blob */ 235 | 236 | var isIE11 = !!window.MSInputMethodContext && !!document.documentMode; 237 | 238 | // inheritable styles may be overridden by parent, always copy for now 239 | inheritableAttrs.forEach(function (k) { 240 | if (k in svgStyles) { 241 | svgStyles[k] = true; 242 | } 243 | }); 244 | 245 | var SvgSaver = (function () { 246 | _createClass(SvgSaver, null, [{ 247 | key: 'getSvg', 248 | value: function getSvg(el) { 249 | if (isUndefined(el) || el === '') { 250 | el = document.body.querySelector('svg'); 251 | } else if (typeof el === 'string') { 252 | el = document.body.querySelector(el); 253 | } 254 | if (el && el.tagName !== 'svg') { 255 | el = el.querySelector('svg'); 256 | } 257 | if (!isNode(el)) { 258 | throw new Error('svgsaver: Can\'t find an svg element'); 259 | } 260 | return el; 261 | } 262 | }, { 263 | key: 'getFilename', 264 | value: function getFilename(el, filename, ext) { 265 | if (!filename || filename === '') { 266 | filename = (el.getAttribute('title') || 'untitled') + '.' + ext; 267 | } 268 | return encodeURI(filename); 269 | } 270 | 271 | /** 272 | * SvgSaver constructor. 273 | * @constructs SvgSaver 274 | * @api public 275 | * 276 | * @example 277 | * var svgsaver = new SvgSaver(); // creates a new instance 278 | * var svg = document.querySelector('#mysvg'); // find the SVG element 279 | * svgsaver.asSvg(svg); // save as SVG 280 | */ 281 | }]); 282 | 283 | function SvgSaver() { 284 | var _ref = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; 285 | 286 | var attrs = _ref.attrs; 287 | var styles = _ref.styles; 288 | 289 | _classCallCheck(this, SvgSaver); 290 | 291 | this.attrs = attrs === undefined ? svgAttrs : attrs; 292 | this.styles = styles === undefined ? svgStyles : styles; 293 | } 294 | 295 | /** 296 | * Return the cloned SVG after cleaning 297 | * 298 | * @param {SVGElement} el The element to copy. 299 | * @returns {SVGElement} SVG text after cleaning 300 | * @api public 301 | */ 302 | 303 | _createClass(SvgSaver, [{ 304 | key: 'cloneSVG', 305 | value: function cloneSVG(el) { 306 | el = SvgSaver.getSvg(el); 307 | var svg = cloneSvg(el, this.attrs, this.styles); 308 | 309 | svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); 310 | svg.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink'); 311 | svg.setAttribute('version', 1.1); 312 | 313 | // height and width needed to download in FireFox 314 | svg.setAttribute('width', svg.getAttribute('width') || '500'); 315 | svg.setAttribute('height', svg.getAttribute('height') || '900'); 316 | 317 | return svg; 318 | } 319 | 320 | /** 321 | * Return the SVG HTML text after cleaning 322 | * 323 | * @param {SVGElement} el The element to copy. 324 | * @returns {String} SVG text after cleaning 325 | * @api public 326 | */ 327 | }, { 328 | key: 'getHTML', 329 | value: function getHTML(el) { 330 | var svg = this.cloneSVG(el); 331 | 332 | var html = svg.outerHTML; 333 | if (html) { 334 | return html; 335 | } 336 | 337 | // see http://stackoverflow.com/questions/19610089/unwanted-namespaces-on-svg-markup-when-using-xmlserializer-in-javascript-with-ie 338 | svg.removeAttribute('xmlns'); 339 | svg.removeAttribute('xmlns:xlink'); 340 | 341 | svg.setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns', 'http://www.w3.org/2000/svg'); 342 | svg.setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:xlink', 'http://www.w3.org/1999/xlink'); 343 | 344 | return new window.XMLSerializer().serializeToString(svg); 345 | } 346 | 347 | /** 348 | * Return the SVG, after cleaning, as a text/xml Blob 349 | * 350 | * @param {SVGElement} el The element to copy. 351 | * @returns {Blog} SVG as a text/xml Blob 352 | * @api public 353 | */ 354 | }, { 355 | key: 'getBlob', 356 | value: function getBlob(el) { 357 | var html = this.getHTML(el); 358 | return new Blob([html], { type: 'text/xml' }); 359 | } 360 | 361 | /** 362 | * Return the SVG, after cleaning, as a image/svg+xml;base64 URI encoded string 363 | * 364 | * @param {SVGElement} el The element to copy. 365 | * @returns {String} SVG as image/svg+xml;base64 URI encoded string 366 | * @api public 367 | */ 368 | }, { 369 | key: 'getUri', 370 | value: function getUri(el) { 371 | var html = encodeURIComponent(this.getHTML(el)); 372 | if (isDefined(window.btoa)) { 373 | // see http://stackoverflow.com/questions/23223718/failed-to-execute-btoa-on-window-the-string-to-be-encoded-contains-characte 374 | return 'data:image/svg+xml;base64,' + window.btoa(unescape(html)); 375 | } 376 | return 'data:image/svg+xml,' + html; 377 | } 378 | 379 | /** 380 | * Saves the SVG as a SVG file using method compatible with the browser 381 | * 382 | * @param {SVGElement} el The element to copy. 383 | * @param {string} [filename] The filename to save, defaults to the SVG title or 'untitled.svg' 384 | * @returns {SvgSaver} The SvgSaver instance 385 | * @api public 386 | */ 387 | }, { 388 | key: 'asSvg', 389 | value: function asSvg(el, filename) { 390 | el = SvgSaver.getSvg(el); 391 | filename = SvgSaver.getFilename(el, filename, 'svg'); 392 | if (isFunction(Blob)) { 393 | return _fileSaver2['default'].saveAs(this.getBlob(el), filename); 394 | } 395 | return saveUri(this.getUri(el), filename); 396 | } 397 | 398 | /** 399 | * Gets the SVG as a PNG data URI. 400 | * 401 | * @param {SVGElement} el The element to copy. 402 | * @param {Function} cb Call back called with the PNG data uri. 403 | * @api public 404 | */ 405 | }, { 406 | key: 'getPngUri', 407 | value: function getPngUri(el, cb) { 408 | if (isIE11) { 409 | console.error('svgsaver: getPngUri not supported on IE11'); 410 | } 411 | el = SvgSaver.getSvg(el); 412 | var filename = SvgSaver.getFilename(el, null, 'png'); 413 | return createCanvas(this.getUri(el), filename, function (canvas) { 414 | cb(canvas.toDataURL('image/png')); 415 | }); 416 | } 417 | 418 | /** 419 | * Saves the SVG as a PNG file using method compatible with the browser 420 | * 421 | * @param {SVGElement} el The element to copy. 422 | * @param {string} [filename] The filename to save, defaults to the SVG title or 'untitled.png' 423 | * @returns {SvgSaver} The SvgSaver instance 424 | * @api public 425 | */ 426 | }, { 427 | key: 'asPng', 428 | value: function asPng(el, filename) { 429 | if (isIE11) { 430 | console.error('svgsaver: asPng not supported on IE11'); 431 | } 432 | el = SvgSaver.getSvg(el); 433 | filename = SvgSaver.getFilename(el, filename, 'png'); 434 | return savePng(this.getUri(el), filename); 435 | } 436 | }]); 437 | 438 | return SvgSaver; 439 | })(); 440 | 441 | exports['default'] = SvgSaver; 442 | module.exports = exports['default']; 443 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svgsaver", 3 | "version": "0.9.0", 4 | "description": "download an SVG element with css styles", 5 | "main": "lib/svgsaver.js", 6 | "jsnext:main": "src/svgsaver.js", 7 | "scripts": { 8 | "rollup": "rollup ./src/index.js -e copy-styles | babel -o ./lib/svgsaver.js", 9 | "browserify": "browserify ./lib/svgsaver.js -o browser.js -s SvgSaver", 10 | "uglify": "uglifyjs browser.js -o browser.min.js", 11 | "test": "npm run check && npm run zuul:phantom", 12 | "compile": "npm run rollup && npm run browserify && npm run uglify", 13 | "build": "npm run compile && npm run jsdoc2md", 14 | "lint": "semistandard test/*.js src/*.js", 15 | "check": "npm run lint -s && dependency-check package.json --entry src", 16 | "watch": "watch \"npm run build\" src/", 17 | "watch:test": "watch \"npm test\" src/ test/", 18 | "demo": "live-server --open=demo --ignore=src", 19 | "start": "npm run demo & npm run watch", 20 | "jsdoc2md": "jsdoc-parse ./src/svgsaver.js | dmd > API.md", 21 | "version": "chg release -y && git add -A CHANGELOG.md", 22 | "zuul:local": "zuul --local 9966 --ui tape -- test/svgsaver-spec.js", 23 | "zuul:phantom": "zuul --phantom --ui tape -- test/svgsaver-spec.js | tap-spec", 24 | "np": "npm run build && np" 25 | }, 26 | "keywords": [ 27 | "svg", 28 | "FileSaver", 29 | "dom", 30 | "css", 31 | "png" 32 | ], 33 | "author": "J. Harshbarger", 34 | "license": "MIT", 35 | "devDependencies": { 36 | "babel": "^5.8.38", 37 | "babelify": "6", 38 | "browserify": "13.0.0", 39 | "dependency-check": "^2.5.1", 40 | "dmd": "^1.4.2", 41 | "jsdoc-parse": "^1.2.7", 42 | "live-server": "^1.1.0", 43 | "np": "^2.14.1", 44 | "phantomjs-prebuilt": "^2.1.14", 45 | "rollup": "^0.36.3", 46 | "semistandard": "^9.1.0", 47 | "tap-spec": "^4.1.0", 48 | "tape": "^4.6.3", 49 | "uglifyjs": "^2.4.10", 50 | "watch": "^1.0.1", 51 | "zuul": "^3.11.1" 52 | }, 53 | "dependencies": { 54 | "computed-styles": "^1.1.2", 55 | "file-saver": "^1.3.3" 56 | }, 57 | "directories": { 58 | "test": "test" 59 | }, 60 | "repository": { 61 | "type": "git", 62 | "url": "https://github.com/Hypercubed/svgsaver.git" 63 | }, 64 | "bugs": { 65 | "url": "https://github.com/Hypercubed/svgsaver/issues" 66 | }, 67 | "homepage": "https://github.com/Hypercubed/svgsaver#readme" 68 | } 69 | -------------------------------------------------------------------------------- /src/clonesvg.js: -------------------------------------------------------------------------------- 1 | /* Some utilities for cloning SVGs with inline styles */ 2 | import computedStyles from 'computed-styles'; 3 | import {isUndefined} from './utils'; 4 | import {inheritableAttrs} from './collection'; 5 | 6 | // Removes attributes that are not valid for SVGs 7 | function cleanAttrs (el, attrs, styles) { // attrs === false - remove all, attrs === true - allow all 8 | if (attrs === true) { return; } 9 | 10 | Array.prototype.slice.call(el.attributes) 11 | .forEach(function (attr) { 12 | // remove if it is not style nor on attrs whitelist 13 | // keeping attributes that are also styles because attributes override 14 | if (attr.specified) { 15 | if (attrs === '' || attrs === false || (isUndefined(styles[attr.name]) && attrs.indexOf(attr.name) < 0)) { 16 | el.removeAttribute(attr.name); 17 | } 18 | } 19 | }); 20 | } 21 | 22 | function cleanStyle (tgt, parentStyles) { 23 | parentStyles = parentStyles || tgt.parentNode.style; 24 | inheritableAttrs.forEach(function (key) { 25 | if (tgt.style[key] === parentStyles[key]) { 26 | tgt.style.removeProperty(key); 27 | } 28 | }); 29 | } 30 | 31 | function domWalk (src, tgt, down, up) { 32 | down(src, tgt); 33 | const children = src.childNodes; 34 | for (var i = 0; i < children.length; i++) { 35 | domWalk(children[i], tgt.childNodes[i], down, up); 36 | } 37 | up(src, tgt); 38 | } 39 | 40 | // Clones an SVGElement, copies approprate atttributes and styles. 41 | export function cloneSvg (src, attrs, styles) { 42 | const clonedSvg = src.cloneNode(true); 43 | 44 | domWalk(src, clonedSvg, (src, tgt) => { 45 | if (tgt.style) { computedStyles(src, tgt.style, styles); } 46 | }, (src, tgt) => { 47 | if (tgt.style && tgt.parentNode) { cleanStyle(tgt); } 48 | if (tgt.attributes) { cleanAttrs(tgt, attrs, styles); } 49 | }); 50 | 51 | return clonedSvg; 52 | } 53 | -------------------------------------------------------------------------------- /src/collection.js: -------------------------------------------------------------------------------- 1 | export const svgStyles = { // Whitelist of CSS styles and default values 2 | 'alignment-baseline': 'auto', 3 | 'baseline-shift': 'baseline', 4 | 'clip': 'auto', 5 | 'clip-path': 'none', 6 | 'clip-rule': 'nonzero', 7 | 'color': 'rgb(51, 51, 51)', 8 | 'color-interpolation': 'srgb', 9 | 'color-interpolation-filters': 'linearrgb', 10 | 'color-profile': 'auto', 11 | 'color-rendering': 'auto', 12 | 'cursor': 'auto', 13 | 'direction': 'ltr', 14 | 'display': 'inline', 15 | 'dominant-baseline': 'auto', 16 | 'enable-background': '', 17 | 'fill': 'rgb(0, 0, 0)', 18 | 'fill-opacity': '1', 19 | 'fill-rule': 'nonzero', 20 | 'filter': 'none', 21 | 'flood-color': 'rgb(0, 0, 0)', 22 | 'flood-opacity': '1', 23 | 'font': '', 24 | 'font-family': 'normal', 25 | 'font-size': 'medium', 26 | 'font-size-adjust': 'auto', 27 | 'font-stretch': 'normal', 28 | 'font-style': 'normal', 29 | 'font-variant': 'normal', 30 | 'font-weight': '400', 31 | 'glyph-orientation-horizontal': '0deg', 32 | 'glyph-orientation-vertical': 'auto', 33 | 'image-rendering': 'auto', 34 | 'kerning': 'auto', 35 | 'letter-spacing': '0', 36 | 'lighting-color': 'rgb(255, 255, 255)', 37 | 'marker': '', 38 | 'marker-end': 'none', 39 | 'marker-mid': 'none', 40 | 'marker-start': 'none', 41 | 'mask': 'none', 42 | 'opacity': '1', 43 | 'overflow': 'visible', 44 | 'paint-order': 'fill', 45 | 'pointer-events': 'auto', 46 | 'shape-rendering': 'auto', 47 | 'stop-color': 'rgb(0, 0, 0)', 48 | 'stop-opacity': '1', 49 | 'stroke': 'none', 50 | 'stroke-dasharray': 'none', 51 | 'stroke-dashoffset': '0', 52 | 'stroke-linecap': 'butt', 53 | 'stroke-linejoin': 'miter', 54 | 'stroke-miterlimit': '4', 55 | 'stroke-opacity': '1', 56 | 'stroke-width': '1', 57 | 'text-anchor': 'start', 58 | 'text-decoration': 'none', 59 | 'text-rendering': 'auto', 60 | 'unicode-bidi': 'normal', 61 | 'visibility': 'visible', 62 | 'word-spacing': '0px', 63 | 'writing-mode': 'lr-tb' 64 | }; 65 | 66 | export const svgAttrs = [ // white list of attributes 67 | 'id', 'xml: base', 'xml: lang', 'xml: space', // Core 68 | 'height', 'result', 'width', 'x', 'y', // Primitive 69 | 'xlink: href', // Xlink attribute 70 | 'href', 71 | 'style', 'class', 72 | 'd', 'pathLength', // Path 73 | 'x', 'y', 'dx', 'dy', 'glyphRef', 'format', 74 | 'x1', 'y1', 'x2', 'y2', 75 | 'rotate', 'textLength', 76 | 'cx', 'cy', 'r', 77 | 'rx', 'ry', 78 | 'fx', 'fy', 79 | 'width', 'height', 80 | 'refX', 'refY', 'orient', 81 | 'markerUnits', 'markerWidth', 'markerHeight', 82 | 'maskUnits', 83 | 'transform', 84 | 'viewBox', 'version', // Container 85 | 'preserveAspectRatio', 'xmlns', 86 | 'points', // Polygons 87 | 'offset', 88 | 'xlink:href' 89 | ]; 90 | 91 | // http://www.w3.org/TR/SVG/propidx.html 92 | // via https://github.com/svg/svgo/blob/master/plugins/_collections.js 93 | export const inheritableAttrs = [ 94 | 'clip-rule', 95 | 'color', 96 | 'color-interpolation', 97 | 'color-interpolation-filters', 98 | 'color-profile', 99 | 'color-rendering', 100 | 'cursor', 101 | 'direction', 102 | 'fill', 103 | 'fill-opacity', 104 | 'fill-rule', 105 | 'font', 106 | 'font-family', 107 | 'font-size', 108 | 'font-size-adjust', 109 | 'font-stretch', 110 | 'font-style', 111 | 'font-variant', 112 | 'font-weight', 113 | 'glyph-orientation-horizontal', 114 | 'glyph-orientation-vertical', 115 | 'image-rendering', 116 | 'kerning', 117 | 'letter-spacing', 118 | 'marker', 119 | 'marker-end', 120 | 'marker-mid', 121 | 'marker-start', 122 | 'pointer-events', 123 | 'shape-rendering', 124 | 'stroke', 125 | 'stroke-dasharray', 126 | 'stroke-dashoffset', 127 | 'stroke-linecap', 128 | 'stroke-linejoin', 129 | 'stroke-miterlimit', 130 | 'stroke-opacity', 131 | 'stroke-width', 132 | 'text-anchor', 133 | 'text-rendering', 134 | 'transform', 135 | 'visibility', 136 | 'white-space', 137 | 'word-spacing', 138 | 'writing-mode' 139 | ]; 140 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export {default} from './svgsaver'; 2 | -------------------------------------------------------------------------------- /src/saveuri.js: -------------------------------------------------------------------------------- 1 | /* global Image, MouseEvent */ 2 | 3 | /* Some simple utilities for saving SVGs, including an alternative to saveAs */ 4 | 5 | import {isDefined} from './utils'; 6 | import FileSaver from 'file-saver'; 7 | 8 | // detection 9 | const DownloadAttributeSupport = (typeof document !== 'undefined') && 10 | ('download' in document.createElement('a')) && 11 | (typeof MouseEvent === 'function'); 12 | 13 | export function saveUri (uri, name) { 14 | if (DownloadAttributeSupport) { 15 | const dl = document.createElement('a'); 16 | dl.setAttribute('href', uri); 17 | dl.setAttribute('download', name); 18 | // firefox doesn't support `.click()`... 19 | // from https://github.com/sindresorhus/multi-download/blob/gh-pages/index.js 20 | dl.dispatchEvent(new MouseEvent('click')); 21 | return true; 22 | } else if (typeof window !== 'undefined') { 23 | window.open(uri, '_blank', ''); 24 | return true; 25 | } 26 | 27 | return false; 28 | } 29 | 30 | export function createCanvas (uri, name, cb) { 31 | const canvas = document.createElement('canvas'); 32 | const context = canvas.getContext('2d'); 33 | 34 | const image = new Image(); 35 | image.onload = function () { 36 | canvas.width = image.width; 37 | canvas.height = image.height; 38 | context.drawImage(image, 0, 0); 39 | 40 | cb(canvas); 41 | }; 42 | image.src = uri; 43 | return true; 44 | } 45 | 46 | export function savePng (uri, name) { 47 | return createCanvas(uri, name, function (canvas) { 48 | if (isDefined(canvas.toBlob)) { 49 | canvas.toBlob(function (blob) { 50 | FileSaver.saveAs(blob, name); 51 | }); 52 | } else { 53 | saveUri(canvas.toDataURL('image/png'), name); 54 | } 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /src/svgsaver.js: -------------------------------------------------------------------------------- 1 | /* global Blob */ 2 | 3 | import {svgAttrs, svgStyles, inheritableAttrs} from './collection'; 4 | import {cloneSvg} from './clonesvg'; 5 | import {saveUri, savePng, createCanvas} from './saveuri'; 6 | import {isDefined, isFunction, isUndefined, isNode} from './utils'; 7 | import FileSaver from 'file-saver'; 8 | 9 | const isIE11 = !!window.MSInputMethodContext && !!document.documentMode; 10 | 11 | // inheritable styles may be overridden by parent, always copy for now 12 | inheritableAttrs.forEach(function (k) { 13 | if (k in svgStyles) { 14 | svgStyles[k] = true; 15 | } 16 | }); 17 | 18 | export class SvgSaver { 19 | 20 | static getSvg (el) { 21 | if (isUndefined(el) || el === '') { 22 | el = document.body.querySelector('svg'); 23 | } else if (typeof el === 'string') { 24 | el = document.body.querySelector(el); 25 | } 26 | if (el && el.tagName !== 'svg') { 27 | el = el.querySelector('svg'); 28 | } 29 | if (!isNode(el)) { 30 | throw new Error('svgsaver: Can\'t find an svg element'); 31 | } 32 | return el; 33 | } 34 | 35 | static getFilename (el, filename, ext) { 36 | if (!filename || filename === '') { 37 | filename = (el.getAttribute('title') || 'untitled') + '.' + ext; 38 | } 39 | return encodeURI(filename); 40 | } 41 | 42 | /** 43 | * SvgSaver constructor. 44 | * @constructs SvgSaver 45 | * @api public 46 | * 47 | * @example 48 | * var svgsaver = new SvgSaver(); // creates a new instance 49 | * var svg = document.querySelector('#mysvg'); // find the SVG element 50 | * svgsaver.asSvg(svg); // save as SVG 51 | */ 52 | constructor ({ attrs, styles } = {}) { 53 | this.attrs = (attrs === undefined) ? svgAttrs : attrs; 54 | this.styles = (styles === undefined) ? svgStyles : styles; 55 | } 56 | 57 | /** 58 | * Return the cloned SVG after cleaning 59 | * 60 | * @param {SVGElement} el The element to copy. 61 | * @returns {SVGElement} SVG text after cleaning 62 | * @api public 63 | */ 64 | cloneSVG (el) { 65 | el = SvgSaver.getSvg(el); 66 | const svg = cloneSvg(el, this.attrs, this.styles); 67 | 68 | svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); 69 | svg.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink'); 70 | svg.setAttribute('version', 1.1); 71 | 72 | // height and width needed to download in FireFox 73 | svg.setAttribute('width', svg.getAttribute('width') || '500'); 74 | svg.setAttribute('height', svg.getAttribute('height') || '900'); 75 | 76 | return svg; 77 | } 78 | 79 | /** 80 | * Return the SVG HTML text after cleaning 81 | * 82 | * @param {SVGElement} el The element to copy. 83 | * @returns {String} SVG text after cleaning 84 | * @api public 85 | */ 86 | getHTML (el) { 87 | const svg = this.cloneSVG(el); 88 | 89 | var html = svg.outerHTML; 90 | if (html) { 91 | return html; 92 | } 93 | 94 | // see http://stackoverflow.com/questions/19610089/unwanted-namespaces-on-svg-markup-when-using-xmlserializer-in-javascript-with-ie 95 | svg.removeAttribute('xmlns'); 96 | svg.removeAttribute('xmlns:xlink'); 97 | 98 | svg.setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns', 'http://www.w3.org/2000/svg'); 99 | svg.setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:xlink', 'http://www.w3.org/1999/xlink'); 100 | 101 | return (new window.XMLSerializer()).serializeToString(svg); 102 | } 103 | 104 | /** 105 | * Return the SVG, after cleaning, as a text/xml Blob 106 | * 107 | * @param {SVGElement} el The element to copy. 108 | * @returns {Blog} SVG as a text/xml Blob 109 | * @api public 110 | */ 111 | getBlob (el) { 112 | const html = this.getHTML(el); 113 | return new Blob([html], { type: 'text/xml' }); 114 | } 115 | 116 | /** 117 | * Return the SVG, after cleaning, as a image/svg+xml;base64 URI encoded string 118 | * 119 | * @param {SVGElement} el The element to copy. 120 | * @returns {String} SVG as image/svg+xml;base64 URI encoded string 121 | * @api public 122 | */ 123 | getUri (el) { 124 | const html = encodeURIComponent(this.getHTML(el)); 125 | if (isDefined(window.btoa)) { 126 | // see http://stackoverflow.com/questions/23223718/failed-to-execute-btoa-on-window-the-string-to-be-encoded-contains-characte 127 | return 'data:image/svg+xml;base64,' + window.btoa(unescape(html)); 128 | } 129 | return 'data:image/svg+xml,' + html; 130 | } 131 | 132 | /** 133 | * Saves the SVG as a SVG file using method compatible with the browser 134 | * 135 | * @param {SVGElement} el The element to copy. 136 | * @param {string} [filename] The filename to save, defaults to the SVG title or 'untitled.svg' 137 | * @returns {SvgSaver} The SvgSaver instance 138 | * @api public 139 | */ 140 | asSvg (el, filename) { 141 | el = SvgSaver.getSvg(el); 142 | filename = SvgSaver.getFilename(el, filename, 'svg'); 143 | if (isFunction(Blob)) { 144 | return FileSaver.saveAs(this.getBlob(el), filename); 145 | } 146 | return saveUri(this.getUri(el), filename); 147 | } 148 | 149 | /** 150 | * Gets the SVG as a PNG data URI. 151 | * 152 | * @param {SVGElement} el The element to copy. 153 | * @param {Function} cb Call back called with the PNG data uri. 154 | * @api public 155 | */ 156 | getPngUri (el, cb) { 157 | if (isIE11) { 158 | console.error('svgsaver: getPngUri not supported on IE11'); 159 | } 160 | el = SvgSaver.getSvg(el); 161 | var filename = SvgSaver.getFilename(el, null, 'png'); 162 | return createCanvas(this.getUri(el), filename, function (canvas) { 163 | cb(canvas.toDataURL('image/png')); 164 | }); 165 | } 166 | 167 | /** 168 | * Saves the SVG as a PNG file using method compatible with the browser 169 | * 170 | * @param {SVGElement} el The element to copy. 171 | * @param {string} [filename] The filename to save, defaults to the SVG title or 'untitled.png' 172 | * @returns {SvgSaver} The SvgSaver instance 173 | * @api public 174 | */ 175 | asPng (el, filename) { 176 | if (isIE11) { 177 | console.error('svgsaver: asPng not supported on IE11'); 178 | } 179 | el = SvgSaver.getSvg(el); 180 | filename = SvgSaver.getFilename(el, filename, 'png'); 181 | return savePng(this.getUri(el), filename); 182 | } 183 | 184 | } 185 | 186 | export default SvgSaver; 187 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | /* Some simple utilities */ 2 | 3 | export const isFunction = (a) => typeof a === 'function'; 4 | export const isDefined = (a) => typeof a !== 'undefined'; 5 | export const isUndefined = (a) => typeof a === 'undefined'; 6 | export const isObject = (a) => (a !== null && typeof a === 'object'); 7 | 8 | export function clone (obj) { 9 | if (obj == null || typeof obj !== 'object') { return obj; } 10 | var copy = obj.constructor(); 11 | for (var attr in obj) { 12 | if (obj.hasOwnProperty(attr)) { copy[attr] = obj[attr]; } 13 | } 14 | return copy; 15 | } 16 | 17 | // from https://github.com/npm-dom/is-dom/blob/master/index.js 18 | export function isNode (val) { 19 | if (!isObject(val)) { return false; } 20 | if (isDefined(window) && isObject(window.Node)) { return val instanceof window.Node; } 21 | return typeof val.nodeType === 'number' && typeof val.nodeName === 'string'; 22 | } 23 | -------------------------------------------------------------------------------- /test/svgsaver-spec.js: -------------------------------------------------------------------------------- 1 | /* global Blob */ 2 | 3 | import test from 'tape'; 4 | import SvgSaver from '../src/'; 5 | 6 | var html = ` 7 | 11 | 12 | 13 | 14 | 15 | `; 16 | 17 | const pg = document.createElement('div'); 18 | document.body.appendChild(pg); 19 | 20 | function toDom (html) { 21 | pg.innerHTML = html; 22 | return pg.querySelector('svg'); 23 | } 24 | 25 | var originalSvg, svgHtml, newSvgDom; 26 | 27 | function beforeEach (svgSaver) { 28 | svgSaver = svgSaver || new SvgSaver(); 29 | originalSvg = toDom(html); 30 | svgHtml = svgSaver.getHTML(originalSvg); 31 | newSvgDom = toDom(svgHtml); 32 | } 33 | 34 | test('should convert SVG element', (t) => { 35 | t.plan(1); 36 | 37 | beforeEach(); 38 | t.equal(svgHtml.slice(0, 4), ' { 42 | t.plan(1); 43 | 44 | beforeEach(); 45 | 46 | svgHtml = new SvgSaver().getHTML(pg); 47 | t.equal(svgHtml.slice(0, 4), ' { 51 | t.plan(1); 52 | 53 | beforeEach(); 54 | 55 | svgHtml = new SvgSaver().getHTML(); 56 | t.equal(svgHtml.slice(0, 4), ' { 60 | t.plan(1); 61 | 62 | beforeEach(); 63 | 64 | svgHtml = new SvgSaver().getHTML('#svg-0'); 65 | t.equal(svgHtml.slice(0, 4), ' { 69 | t.plan(1); 70 | 71 | beforeEach(); 72 | 73 | t.throws(function () { 74 | new SvgSaver().getHTML('#svg-1'); 75 | }); 76 | }); 77 | 78 | test('should convert SVG element with children', function (t) { 79 | t.plan(1); 80 | 81 | beforeEach(); 82 | t.notEqual(svgHtml.indexOf('