├── .npmignore ├── assets ├── linked.css ├── imported.css ├── test.png └── d3-save-svg.min.js ├── .gitignore ├── index.js ├── src ├── namespaces.js ├── download.js ├── convertRaster.js ├── preprocess.js ├── d3-save-svg.js └── setInlineStyles.js ├── CONTRIBUTING.md ├── LICENSE ├── package.json ├── test └── save-svg-test.js ├── README.md └── index.html /.npmignore: -------------------------------------------------------------------------------- 1 | build/*.zip 2 | test/ 3 | -------------------------------------------------------------------------------- /assets/linked.css: -------------------------------------------------------------------------------- 1 | .linked { 2 | fill: #f0f; 3 | } -------------------------------------------------------------------------------- /assets/imported.css: -------------------------------------------------------------------------------- 1 | .imported { 2 | fill: #f0f; 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | build/ 3 | node_modules 4 | npm-debug.log 5 | -------------------------------------------------------------------------------- /assets/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edeno/d3-save-svg/HEAD/assets/test.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | export { 2 | save, 3 | embedRasterImages, 4 | } from './src/d3-save-svg'; 5 | -------------------------------------------------------------------------------- /src/namespaces.js: -------------------------------------------------------------------------------- 1 | export default { 2 | svg: 'http://www.w3.org/2000/svg', 3 | xhtml: 'http://www.w3.org/1999/xhtml', 4 | xlink: 'http://www.w3.org/1999/xlink', 5 | xml: 'http://www.w3.org/XML/1998/namespace', 6 | xmlns: 'http://www.w3.org/2000/xmlns/', 7 | }; 8 | -------------------------------------------------------------------------------- /src/download.js: -------------------------------------------------------------------------------- 1 | export default function (svgInfo, filename) { 2 | window.URL = (window.URL || window.webkitURL); 3 | var blob = new Blob(svgInfo.source, {type: 'text\/xml'}); 4 | var url = window.URL.createObjectURL(blob); 5 | var body = document.body; 6 | var a = document.createElement('a'); 7 | 8 | body.appendChild(a); 9 | a.setAttribute('download', filename + '.svg'); 10 | a.setAttribute('href', url); 11 | a.style.display = 'none'; 12 | a.click(); 13 | a.parentNode.removeChild(a); 14 | 15 | setTimeout(function() { 16 | window.URL.revokeObjectURL(url); 17 | }, 10); 18 | } 19 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | Fork, then clone the repo: 3 | 4 | git clone git@github.com:your-username/d3-save-svg.git 5 | 6 | Set up your machine: 7 | 8 | npm install 9 | 10 | If the tests pass, then the library should build in the `/build` folder. 11 | Make your change. Add tests for your change. Make the tests pass: 12 | 13 | npm test 14 | 15 | Push to your fork and [submit a pull request](https://github.com/edeno/d3-save-svg/compare/) to the develop branch. 16 | 17 | ### Additional Notes 18 | Testing is via [Tape](https://github.com/substack/tape) and [jsdom](https://github.com/tmpvar/jsdom). Right now the tests are pretty rudimentary. Also `index.html` serves as a good check on whether things are working. 19 | 20 | Development is done using the [git-flow](http://nvie.com/posts/a-successful-git-branching-model/) workflow. Please merge changes into the `develop` branch. 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 The New York Times 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /src/convertRaster.js: -------------------------------------------------------------------------------- 1 | export {converterEngine, getImageBase64, isDataURL}; 2 | 3 | function converterEngine(input) { // fn BLOB => Binary => Base64 ? 4 | var uInt8Array = new Uint8Array(input); 5 | var i = uInt8Array.length; 6 | var biStr = []; //new Array(i); 7 | while (i--) { 8 | biStr[i] = String.fromCharCode(uInt8Array[i]); 9 | } 10 | 11 | var base64 = window.btoa(biStr.join('')); 12 | return base64; 13 | }; 14 | 15 | function getImageBase64(url, callback) { 16 | var xhr = new XMLHttpRequest(url); 17 | var img64; 18 | xhr.open('GET', url, true); // url is the url of a PNG/JPG image. 19 | xhr.responseType = 'arraybuffer'; 20 | xhr.callback = callback; 21 | xhr.onload = function() { 22 | img64 = converterEngine(this.response); // convert BLOB to base64 23 | this.callback(null, img64); // callback : err, data 24 | }; 25 | 26 | xhr.onerror = function() { 27 | callback('B64 ERROR', null); 28 | }; 29 | 30 | xhr.send(); 31 | }; 32 | 33 | function isDataURL(str) { 34 | var uriPattern = /^\s*data:([a-z]+\/[a-z0-9\-]+(;[a-z\-]+\=[a-z\-]+)?)?(;base64)?,[a-z0-9\!\$\&\'\,\(\)\*\+\,\;\=\-\.\_\~\:\@\/\?\%\s]*\s*$/i; 35 | return !!str.match(uriPattern); 36 | } 37 | -------------------------------------------------------------------------------- /src/preprocess.js: -------------------------------------------------------------------------------- 1 | import setInlineStyles from './setInlineStyles'; 2 | import prefix from './namespaces'; 3 | 4 | export default function (svg) { 5 | svg.setAttribute('version', '1.1'); 6 | 7 | // removing attributes so they aren't doubled up 8 | svg.removeAttribute('xmlns'); 9 | svg.removeAttribute('xlink'); 10 | 11 | // These are needed for the svg 12 | if (!svg.hasAttributeNS(prefix.xmlns, 'xmlns')) { 13 | svg.setAttributeNS(prefix.xmlns, 'xmlns', prefix.svg); 14 | } 15 | 16 | if (!svg.hasAttributeNS(prefix.xmlns, 'xmlns:xlink')) { 17 | svg.setAttributeNS(prefix.xmlns, 'xmlns:xlink', prefix.xlink); 18 | } 19 | 20 | setInlineStyles(svg); 21 | 22 | var xmls = new XMLSerializer(); 23 | var source = xmls.serializeToString(svg); 24 | var doctype = ''; 25 | var rect = svg.getBoundingClientRect(); 26 | var svgInfo = { 27 | top: rect.top, 28 | left: rect.left, 29 | width: rect.width, 30 | height: rect.height, 31 | class: svg.getAttribute('class'), 32 | id: svg.getAttribute('id'), 33 | childElementCount: svg.childElementCount, 34 | source: [doctype + source], 35 | }; 36 | 37 | return svgInfo; 38 | } 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "d3-save-svg", 3 | "version": "0.0.2", 4 | "description": "D3 plugin for exporting SVGs. Based on svg-crowbar by Shan Carter", 5 | "keywords": [ 6 | "d3", 7 | "svg", 8 | "svg-crowbar" 9 | ], 10 | "license": "BSD-3-Clause", 11 | "main": "build/d3-save-svg.js", 12 | "jsnext:main": "index", 13 | "homepage": "https://github.com/edeno/d3-save-svg", 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/edeno/d3-save-svg" 17 | }, 18 | "scripts": { 19 | "pretest": "mkdir -p build && node -e 'process.stdout.write(\"var version = \\\"\" + require(\"./package.json\").version + \"\\\"; export * from \\\"../index\\\"; export {version};\");' > build/bundle.js && rollup -f umd -u d3-save-svg -n d3_save_svg -o build/d3-save-svg.js -- build/bundle.js", 20 | "test": "faucet `find test -name '*-test.js'`", 21 | "prepublish": "npm run test && uglifyjs build/d3-save-svg.js -c -m -o build/d3-save-svg.min.js && rm -f build/d3-save-svg.zip && zip -j build/d3-save-svg.zip -- LICENSE README.md build/d3-save-svg.js build/d3-save-svg.min.js" 22 | }, 23 | "devDependencies": { 24 | "faucet": "0.0", 25 | "jsdom": "^7.1.0", 26 | "rollup": "0.20.5", 27 | "tape": "4", 28 | "uglify-js": "2", 29 | "w3c-blob": "0.0.1", 30 | "xmlserializer": "^0.3.3" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/d3-save-svg.js: -------------------------------------------------------------------------------- 1 | import download from './download'; 2 | import preprocess from './preprocess'; 3 | import prefix from './namespaces'; 4 | import {converterEngine, getImageBase64, isDataURL} from './convertRaster'; 5 | 6 | export function save(svgElement, config) { 7 | if (svgElement.nodeName !== 'svg' || svgElement.nodeType !== 1) { 8 | throw 'Need an svg element input'; 9 | } 10 | 11 | var config = config || {}; 12 | var svgInfo = preprocess(svgElement, config); 13 | var defaultFileName = getDefaultFileName(svgInfo); 14 | var filename = config.filename || defaultFileName; 15 | var svgInfo = preprocess(svgElement); 16 | download(svgInfo, filename); 17 | } 18 | 19 | export function embedRasterImages(svg) { 20 | 21 | var images = svg.querySelectorAll('image'); 22 | [].forEach.call(images, function(image) { 23 | var url = image.getAttribute('href'); 24 | 25 | // Check if it is already a data URL 26 | if (!isDataURL(url)) { 27 | // convert to base64 image and embed. 28 | getImageBase64(url, function(err, d) { 29 | image.setAttributeNS(prefix.xlink, 'href', 'data:image/png;base64,' + d); 30 | }); 31 | } 32 | 33 | }); 34 | 35 | } 36 | 37 | function getDefaultFileName(svgInfo) { 38 | var defaultFileName = 'untitled'; 39 | if (svgInfo.id) { 40 | defaultFileName = svgInfo.id; 41 | } else if (svgInfo.class) { 42 | defaultFileName = svgInfo.class; 43 | } else if (window.document.title) { 44 | defaultFileName = window.document.title.replace(/[^a-z0-9]/gi, '-').toLowerCase(); 45 | } 46 | 47 | return defaultFileName; 48 | } 49 | -------------------------------------------------------------------------------- /test/save-svg-test.js: -------------------------------------------------------------------------------- 1 | var tape = require('tape'); 2 | var d3SaveSvg = require('../'); 3 | var jsdom = require('jsdom'); 4 | var xmls = require('xmlserializer'); 5 | var Blob = require('w3c-blob'); 6 | 7 | var revokeObjectURLShim = function() {}; 8 | 9 | var createObjectURLShim = function() {}; 10 | 11 | var xmlsShim = function() { 12 | this.serializeToString = xmls.serializeToString; 13 | }; 14 | 15 | tape('save() throws an error if element is not an svg element.', function(test) { 16 | jsdom.env({ 17 | html: '
A fork of the nytimes svg-crowbar bookmarklet that extracts SVG nodes and accompanying styles from an HTML document and downloads them as an SVG file—A file which you could open and edit in Adobe Illustrator, for instance. Because SVGs are resolution independent, it’s great for when you want to use web technologies to create documents that are meant to be printed (like, maybe on newsprint). It was created with d3.js in mind, but it should work fine no matter how you choose to generate your SVG.
78 | 79 |Code is on github here.
80 | 81 |Can use to download specific svg, which can be incorporated into buttons for exporting images. See example below.
83 |Allows specification of custom file names.
84 |Handles embedded raster images.
85 |Has convenience method for embedding raster images.
86 | 87 |Pixels will map to points when opening in Illustrator.
89 |Dimensions of the document will be the same as the dimensions of your SVG element.
90 |All colors are RGB, which is not ideal for print documents, but CMYK is not supported in SVG 1.1.
91 | 92 |It only works in Chrome.
94 |Currently the https version of the script is being served from raw.github.com, which might break in the future. If the script stops running on https pages, check back here—you might have to re-install the bookmarklet at that time.
95 |Descendent ">" CSS selectors will crash Adobe Illustrator, therefore those styles are stripped out. Be warned.
96 |Adobe Illustrator also chokes on fonts that it doesn’t recognize. Font-family assigments like “sans-serif” (or if you're using webfonts like “nyt-franklin”) will cause Illustrator to give this error when opening the file: “The operation cannot complete because of an unknown error. [CANT]”. This is fixed in Illustrator version 17.1. Other SVG viewers are pretty okay with them though.
97 |Some styles won’t propogate down if they depend on markup outside of the svg element. For instance, if you use CSS that targets an SVG path element by an id on the div surrounding the SVG ("#map svg path") then those styles won’t show up in the resulting file.
98 | 99 |