Skip to content

Commit

Permalink
Allow both DOM Element and string as SVG input, and work with SVGs hi…
Browse files Browse the repository at this point in the history
…dden in DOM
  • Loading branch information
sharonchoong committed Dec 14, 2020
1 parent bce38c5 commit 416f4a1
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 72 deletions.
35 changes: 18 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
# svg-exportJS [![Codacy Badge](https://api.codacy.com/project/badge/Grade/a2677830f9d2432d8061a8151e03fd23)](https://app.codacy.com/gh/sharonchoong/svg-exportJS?utm_source=github.com&utm_medium=referral&utm_content=sharonchoong/svg-exportJS&utm_campaign=Badge_Grade)

An easy-to-use client-side Javascript library to export svg graphics from web pages and download them as an SVG file, PDF, or raster image (JPEG, PNG) format. Written in plain vanilla javascript. Originally created to export D3.js charts.
An easy-to-use client-side Javascript library to export SVG graphics from web pages and download them as an SVG file, PDF, or raster image (JPEG, PNG) format. Written in plain vanilla javascript. Originally created to export D3.js charts.

This library handles
This library features:

- Custom size for exported image or graphic
- Exporting SVG DOM Element objects or serialized SVG string to SVG file, PNG, JPEG, PDF
- Setting custom size for exported image or graphic
- High resolution raster image, using `scale`
- External CSS styles in SVG
- Custom embedded fonts
- Transparent background for JPEG format conversion
- Including external CSS styles in SVG
- Exporting text in custom embedded fonts
- Handling transparent background for JPEG format conversion
- Exporting SVGs that are hidden on the DOM (`display: none`, SVGs in hidden modals, dropdowns or tabs, etc.)

Demo available [here](https://sharonchoong.github.io/svg-exportJS/index.html).

Expand Down Expand Up @@ -53,26 +55,26 @@ In Javascript:

```javascript
svgExport.downloadSvg(
"#mysvg", // css selector of svg element to be exported
svgElement, // SVG DOM Element object to be exported. Alternatively, a string of the serialized SVG can be passed
"chart title name", // chart title: file name of exported image
{ width: 200, height: 200 } // options (optional)
{ width: 200, height: 200 } // options (optional, please see below for a list of option properties)
);
svgExport.downloadPng("#mysvg", "chart title name", {
svgExport.downloadPng(svgString, "chart title name", {
width: 200,
height: 200,
});
svgExport.downloadJpeg("#mysvg", "chart title name");
svgExport.downloadPdf("#mysvg", "chart title name");
svgExport.downloadJpeg(svgElement, "chart title name");
svgExport.downloadPdf(svgElement, "chart title name");
```

See `index.html` for an example of how to use.

## Options

- **width** (number) : _the width of the resulting image exported, in pixels. Default is the svg's width on the DOM_
- **height** (number) : _the height of the resulting image exported, in pixels. Default is the svg's height on the DOM_
- **scale** (number) : _a multiple by which the svg can be increased or decreased in size. For PNG and JPEG exports, if width, height and scale are not specified, scale is set to `10` for a 10x enlargement to ensure that a higher resolution image is produced. Otherwise, the default scale is `1`_
- **useCSS** (bool): _if SVG styles are specified in stylesheet rather than inline, setting `true` will add references to such styles from the styles computed by the browser. If useCSS is `false`, `currentColor` will be changed to `black`. Default is `true`_
- **width** (number) : _the width of the resulting image exported, in pixels. Default is the SVG's width on the DOM_
- **height** (number) : _the height of the resulting image exported, in pixels. Default is the SVG's height on the DOM_
- **scale** (number) : _a multiple by which the SVG can be increased or decreased in size. For PNG and JPEG exports, if width, height and scale are not specified, scale is set to `10` for a 10x enlargement to ensure that a higher resolution image is produced. Otherwise, the default scale is `1`_
- **useCSS** (bool): _if SVG styles are specified in stylesheet externally rather than inline, setting `true` will add references to such styles from the styles computed by the browser. If useCSS is `false`, `currentColor` will be changed to `black`. This setting only applies if the SVG is passed as a DOM Element object, not as a string. Default is `true`_
- **transparentBackgroundReplace** (string): _the color to be used to replace a transparent background in JPEG format export. Default is `white`_
- **pdfOptions**
- **pageLayout** (object): _e.g. `{ margin: 50, layout: "landscape" }`. This is provided to PDFKit's `addPage`. When the options **width** and **height** are not specified, a minimum size of 300x300 is used for the PDF page size; otherwise the page size wraps around the SVG size. Please see the [PDFKit documentation](https://pdfkit.org/docs/getting_started.html#adding_pages) for more info_
Expand All @@ -98,8 +100,7 @@ Need to add SVG graphics to Office Word, Excel or Powerpoint presentations? [SVG
## Roadmap

- [ ] Test external images within SVGs
- [ ] Allow serialized SVG string or external SVG document as input
- [ ] Set up package.json and publish to npm
- [ ] Set up package.json and publish to npm (jsdom for Node?)

## Contributing

Expand Down
25 changes: 16 additions & 9 deletions index.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/canvg@3.0.1/lib/umd.js"></script>
<script src="https://cdn.jsdelivr.net/npm/pdfkit@0.11.0/js/pdfkit.standalone.js"></script>
<script src="https://github.com/devongovett/blob-stream/releases/download/v0.1.3/blob-stream.js"></script>
<script src="https://cdn.jsdelivr.net/npm/svg-to-pdfkit@0.1.8/source.min.js"></script>
<script src="svg-export.js"></script>
<style>
text {
fill: blue;
Expand All @@ -10,11 +15,6 @@
src: url('fonts/Segan/Segan-Light.ttf') format('truetype');
}
</style>
<script src="https://unpkg.com/canvg@3.0.1/lib/umd.js"></script>
<script src="https://cdn.jsdelivr.net/npm/pdfkit@0.11.0/js/pdfkit.standalone.js"></script>
<script src="https://github.com/devongovett/blob-stream/releases/download/v0.1.3/blob-stream.js"></script>
<script src="https://cdn.jsdelivr.net/npm/svg-to-pdfkit@0.1.8/source.min.js"></script>
<script src="svg-export.min.js"></script>
</head>
<body>
<div>
Expand All @@ -41,21 +41,28 @@ <h3>Demo</h3>
<button id="btn_export_svg">SVG</button>
<button id="btn_export_jpeg">JPEG</button>
<button id="btn_export_png">PNG (5000 x 5000 custom size)</button>
<button id="btn_export_png_string">PNG (svg string)</button>
<button id="btn_export_pdf">PDF (with captions)</button>
</div>
<p>Note: for the PNG (svg string) export, the red circle does not render as red because the serialized string does not contain the "color: red" style for "currentColor", which is actually found on the SVG's container</div>
</body>
<script>
document.querySelector("#btn_export_svg").onclick = function(){
svgExport.downloadSvg("#mysvg", "Circles and rectangles chart");
svgExport.downloadSvg(document.querySelector("#mysvg"), "Circles and rectangles chart");
};
document.querySelector("#btn_export_jpeg").onclick = function(){
svgExport.downloadJpeg("#mysvg", "Circles and rectangles chart");
svgExport.downloadJpeg(document.querySelector("#mysvg"), "Circles and rectangles chart");
};
document.querySelector("#btn_export_png").onclick = function(){
svgExport.downloadPng("#mysvg", "Circles and rectangles chart", { width: 5000, height: 5000 });
svgExport.downloadPng(document.querySelector("#mysvg"), "Circles and rectangles chart", { width: 5000, height: 5000 });
};
document.querySelector("#btn_export_png_string").onclick = function(){
var svg_string = document.querySelector("#mysvg").outerHTML;
svg_string = svg_string.replace(">", ">" + document.getElementsByTagName("style")[0].outerHTML);
svgExport.downloadPng(svg_string, "Circles and rectangles chart");
};
document.querySelector("#btn_export_pdf").onclick = function(){
svgExport.downloadPdf("#mysvg", "Circles and rectangles chart", {
svgExport.downloadPdf(document.querySelector("#mysvg"), "Circles and rectangles chart", {
pdfOptions: {
chartCaption: "Hi there. This is a test chart caption. ",
pdfTextFontFamily: "Segan",
Expand Down
129 changes: 84 additions & 45 deletions svg-export.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,31 @@
var version = "1.0.0";
var _options = {};

function getSvgElement(svg) {
var div = document.createElement("div");
div.className = "tempdiv-svg-exportJS";

if (typeof svg === "string") {
div.innerHTML = svg.trim();
svg = div.firstChild;
}

if (!svg.nodeType || svg.nodeType !== 1) {
console.log("Error svg-export: The input svg was not recognized");
return null;
}

var svgClone = svg.cloneNode(true);
svgClone.style.display = null;
div.appendChild(svgClone);
div.style.visibility = "hidden";
div.style.display = "table";
div.style.position = "absolute";
document.body.appendChild(div);

return svgClone;
}

function setPdfOptions(options) {
if (options && options.pdfOptions)
{
Expand Down Expand Up @@ -48,8 +73,8 @@
];
}
}

function setOptions(svgSelector, options) {
function setOptions(svgElement, options) {
//initialize options
_options = {
originalWidth: 100,
Expand All @@ -71,14 +96,14 @@
};

//original size
_options.originalHeight = document.querySelector(svgSelector).style.getPropertyValue("height").indexOf("%") !== -1
|| document.querySelector(svgSelector).getAttribute("height").indexOf("%") !== -1
? document.querySelector(svgSelector).getBBox().height * _options.scale
: document.querySelector(svgSelector).getBoundingClientRect().height * _options.scale;
_options.originalWidth = document.querySelector(svgSelector).style.getPropertyValue("width").indexOf("%") !== -1
|| document.querySelector(svgSelector).getAttribute("width").indexOf("%") !== -1
? document.querySelector(svgSelector).getBBox().width * _options.scale
: document.querySelector(svgSelector).getBoundingClientRect().width * _options.scale;
_options.originalHeight = svgElement.style.getPropertyValue("height").indexOf("%") !== -1
|| (svgElement.getAttribute("height") && svgElement.getAttribute("height").indexOf("%") !== -1 )
? svgElement.getBBox().height * _options.scale
: svgElement.getBoundingClientRect().height * _options.scale;
_options.originalWidth = svgElement.style.getPropertyValue("width").indexOf("%") !== -1
|| (svgElement.getAttribute("width") && svgElement.getAttribute("width").indexOf("%") !== -1 )
? svgElement.getBBox().width * _options.scale
: svgElement.getBoundingClientRect().width * _options.scale;

//custom options
if (options && options.scale && typeof options.scale === "number") {
Expand Down Expand Up @@ -108,11 +133,13 @@

function useCSSfromComputedStyles(element, elementClone) {
if (typeof getComputedStyle !== "function"){
alert("Warning svg-export: this browser is not able to get computed styles");
console.log("Warning svg-export: this browser is not able to get computed styles");
return;
}
element.children.forEach(function(child, index){
useCSSfromComputedStyles(child, elementClone.children[parseInt(index)]);
element.childNodes.forEach(function(child, index){
if (child.nodeType === 1/*Node.ELEMENT_NODE*/) {
useCSSfromComputedStyles(child, elementClone.childNodes[parseInt(index)]);
}
});

var compStyles = window.getComputedStyle(element);
Expand All @@ -123,26 +150,32 @@
});
}

function getSvg(svgSelector, asString = true)
function setupSvg(svgElement, originalSvg, asString)
{
var svg = document.querySelector(svgSelector).cloneNode(true);
if (_options.useCSS) {
useCSSfromComputedStyles(document.querySelector(svgSelector), svg);
if (asString === undefined) { asString = true; }
if (_options.useCSS && typeof originalSvg === "object") {
useCSSfromComputedStyles(originalSvg, svgElement);
svgElement.style.display = null;
}

svg.style.width = null;
svg.style.height = null;
svg.setAttribute("width", _options.width);
svg.setAttribute("height", _options.height);
svg.setAttribute("preserveAspectRatio", "none");
svg.setAttribute("viewBox", "0 0 " + (_options.originalWidth) + " " + (_options.originalHeight));
svgElement.style.width = null;
svgElement.style.height = null;
svgElement.setAttribute("width", _options.width);
svgElement.setAttribute("height", _options.height);
svgElement.setAttribute("preserveAspectRatio", "none");
svgElement.setAttribute("viewBox", "0 0 " + (_options.originalWidth) + " " + (_options.originalHeight));

var elements = document.getElementsByClassName("tempdiv-svg-exportJS");
while(elements.length > 0){
elements[0].parentNode.removeChild(elements[0]);
}

//get svg string
if (asString)
{
var serializer = new XMLSerializer();
//setting currentColor to black matters if computed styles are not used
var svgString = serializer.serializeToString(svg).replace(/currentColor/g, "black");
var svgString = serializer.serializeToString(svgElement).replace(/currentColor/g, "black");

//add namespaces
if (!svgString.match(/^<svg[^>]+xmlns="http\:\/\/www\.w3\.org\/2000\/svg"/)) {
Expand All @@ -154,7 +187,7 @@

return svgString;
}
return svg;
return svgElement;
}

function getCustomFonts(fontUrls) {
Expand Down Expand Up @@ -201,14 +234,16 @@
}
}

function downloadSvg(svgSelector, svgName, options) {
function downloadSvg(svg, svgName, options) {
var svgElement = getSvgElement(svg);
if (!svgElement) { return; }
if (svgName == null) {
svgName = "chart";
}

//get svg element
setOptions(svgSelector, options);
var svgString = getSvg(svgSelector);
setOptions(svgElement, options);
var svgString = setupSvg(svgElement, svg);

//add xml declaration
svgString = "<?xml version=\"1.0\" standalone=\"no\"?>\r\n" + svgString;
Expand All @@ -219,18 +254,19 @@
triggerDownload(url, svgName + ".svg");
}

function downloadRaster(svgSelector, svgName, options, imageType) {
function downloadRaster(svg, svgName, options, imageType) {
//check dependency and values
if (typeof canvg !== "object")
{
alert("Error svg-export: PNG/JPEG export requires Canvg.js");
console.log("Error svg-export: PNG/JPEG export requires Canvg.js");
return;
}
imageType = imageType.toLowerCase().replace("jpg", "jpeg");
if (imageType !== "png" && imageType !== "jpeg") {
imageType = "png";
}

var svgElement = getSvgElement(svg);
if (!svgElement) { return; }
if (svgName == null) {
svgName = "chart";
}
Expand All @@ -243,9 +279,10 @@
}
options.scale = 10;
}
setOptions(svgSelector, options);
var svgString = getSvg(svgSelector);

setOptions(svgElement, options);
var svgString = setupSvg(svgElement, svg);
console.log(svgElement)
console.log(_options.width)
if (imageType === "jpeg")
{
//change transparent background to white
Expand All @@ -259,11 +296,11 @@
var image = canvas.toDataURL("image/" + imageType);
triggerDownload(image, svgName + "." + imageType, canvas);
}
function downloadPng(svgSelector, svgName, options) {
downloadRaster(svgSelector, svgName, options, "png");
function downloadPng(svg, svgName, options) {
downloadRaster(svg, svgName, options, "png");
}
function downloadJpeg(svgSelector, svgName, options) {
downloadRaster(svgSelector, svgName, options, "jpeg");
function downloadJpeg(svg, svgName, options) {
downloadRaster(svg, svgName, options, "jpeg");
}

function fillPDFDoc(doc, svgName, svg) {
Expand Down Expand Up @@ -291,20 +328,22 @@
});
}
}
function downloadPdf(svgSelector, svgName, options) {
function downloadPdf(svg, svgName, options) {
//check dependency and values
if (typeof PDFDocument !== "function" || typeof SVGtoPDF !== "function" || typeof blobStream !== "function")
{
alert("Error svg-export: PDF export requires PDFKit.js, blob-stream and SVG-to-PDFKit");
console.log("Error svg-export: PDF export requires PDFKit.js, blob-stream and SVG-to-PDFKit");
return;
}
var svgElement = getSvgElement(svg);
if (!svgElement) { return; }
if (svgName == null) {
svgName = "chart";
}

//get svg element
setOptions(svgSelector, options);
var svg = getSvg(svgSelector, false);
setOptions(svgElement, options);
var svgCloned = setupSvg(svgElement, svg, false);

//create PDF doc
var doc = new PDFDocument(_options.pdfOptions.pageLayout);
Expand All @@ -317,7 +356,7 @@
fonts.forEach(function(font, index) {
var thisPdfOptions = _options.pdfOptions.customFonts[parseInt(index)];
//this ensures that the font fallbacks are removed from inline CSS that contain custom fonts, as fonts with fallbacks are not parsed correctly by SVG-to-PDFKit
var fontStyledElements = svg.querySelectorAll("[style*=\"" +thisPdfOptions.fontName + "\"]");
var fontStyledElements = svgCloned.querySelectorAll("[style*=\"" +thisPdfOptions.fontName + "\"]");
fontStyledElements.forEach(function(element) {
element.style.fontFamily = thisPdfOptions.fontName;
});
Expand All @@ -328,11 +367,11 @@
doc.registerFont(thisPdfOptions.fontName, font);
}
});
fillPDFDoc(doc, svgName, svg);
fillPDFDoc(doc, svgName, svgCloned);
doc.end();
});
} else {
fillPDFDoc(doc, svgName, svg);
fillPDFDoc(doc, svgName, svgCloned);
doc.end();
}

Expand Down

0 comments on commit 416f4a1

Please sign in to comment.