Smarter ideas worth writing about.

Exporting an SVG image to a SharePoint Document Library via JavaScript

In a recent project, we created visualizations of a process workflow into an SVG image.  One of the project requirements included saving this visualization as an image to a SharePoint document library.  This article will discuss and demonstrate how to export an SVG image in a SharePoint 2013 app to a SharePoint document library in the host web.


Creating a Sample SVG Image

In our project, we had significant additional code to generate the SVG image based on data in the application.  For the purposes of this article, I created a simple SVG image using the Google svg-edit tool at http://svg-edit.googlecode.com/svn/branches/textedit/editor/svg-editor.html.  Details on the project with source code can be found at https://code.google.com/p/svg-edit/.

The Basic App Page
Start with the basic “App for SharePoint” project template in Visual Studio 2013, there are several tags added to main content area.  The first is the svgWrapper div. This div contains the svg image code (either the code from Google’s svg-edit tool or the custom generated svg).  Next is a button which will call our JavaScript and export the image.  Finally, is a canvas (with an id of exportCanvas) which is hidden that will be used during the conversion process.  The basic code with my sample SVG image looks like the following:


    <div id="svgWrapper">
        <svg id="svgImage" width="640" height="480" xmlns="http://www.w3.org/2000/svg">
         <g>
          <title>Layer 1</title>
          <rect id="svg_1" height="43" width="86" y="57" x="121" stroke-width="5" stroke="#000000" fill="#FF0000"/>
          <ellipse ry="57" rx="87.5" id="svg_2" cy="100" cx="332.5" stroke-width="5" stroke="#000000" fill="#FF0000"/>
          <path id="svg_3" d="m29,171c-14,26 99,22 99,22c0,0 -4,41 -8,43c-4,2 -39,51 -39,51c0,0 -62,-58 -62,-59c0,-1 10,-57 10,-57z" stroke-linecap="null" stroke-linejoin="null" stroke-dasharray="null" stroke-width="5" stroke="#000000" fill="#FF0000"/>
          <path id="svg_4" d="m292.42999,252.03l0,-31.6875l0,0c0,-1.3461 1.0914,-2.4375 2.43869,-2.4375l29.25,0c1.3461,0 2.43631,1.0914 2.43631,2.4375c0,1.34669 -1.09021,2.4375 -2.43631,2.4375l-2.43869,0l0,31.6875c0,1.34669 -1.0914,2.4375 -2.4375,2.4375l-29.25,0l0,0c-1.3461,0 -2.4375,-1.09081 -2.4375,-2.4375c0,-1.3461 1.0914,-2.4375 2.4375,-2.4375l2.4375,0zm4.875,-34.125l0,0c1.3461,0 2.4375,1.0914 2.4375,2.4375c0,1.34669 -1.0914,2.4375 -2.4375,2.4375c-0.67245,0 -1.21875,-0.5457 -1.21875,-1.21875c0,-0.67305 0.5463,-1.21875 1.21875,-1.21875l2.4375,0m24.37619,2.4375l-26.81369,0m-4.875,29.25l0,0c0.67365,0 1.21875,0.5457 1.21875,1.21875c0,0.67305 -0.5451,1.21875 -1.21875,1.21875l2.43869,0m-2.43869,2.4375l0,0c1.3461,0 2.43869,-1.09081 2.43869,-2.4375l0,-2.4375" stroke-linecap="null" stroke-linejoin="null" stroke-dasharray="null" stroke-width="5" stroke="#000000" fill="#FF0000"/>
          <text xml:space="preserve" text-anchor="start" id="svg_5" y="146" x="110" stroke-linecap="null" stroke-linejoin="null" stroke-dasharray="null" stroke-width="0" stroke="#000000" fill="#FF0000">
           <tspan x="110" dy="0" font-weight="normal" font-style="normal" font-size="24" font-family="DroidSerif" fill="#000000" id="svg_25" xml:space="preserve">Test SVG</tspan>
          </text>
         </g>
        </svg>
    </div>
    <button type="button" onclick="saveImage();">Export to Doc Lib</button>
    <canvas id="exportCanvas" style="display:none;"></canvas>

JavaScript Libraries to Include

Within the solution, we will use multiple third-party JavaScript libraries to simply our development. 

First, we start with the canvg library.  This library allows us to parse the SVG image and render it onto the Canvas.  To use, download the canvg.js, stackblur.js, and rgbcolor.js files from https://github.com/gabelerner/canvg.

Next, we create a JavaScript file called Base64ToArrayBuffer.js based on the code under solution #2 – rewriting atob() and btoa() using TypedArrays and UTF-8 on the Mozilla Developer Network at https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding.  This library helps us convert the canvas into the binary data we need to pass to SharePoint.

Finally, we add a reference to the SharePoint cross-domain library (SP.RequestExecutor.js).  This will allow us to connect to the host web to save the image.  The reference should look as follows:

    <script type="text/javascript" src="/_layouts/15/SP.RequestExecutor.js"></script>


Converting the SVG image to Canvas

To start, we clone the svgWrapper div (the div that contains the SVG image) using jQuery.  This allows us to manipulate the code without affecting the display of the SVG.

    var svg = $("#svgWrapper").clone();

Next, we take the inner HTML of the div.  This gives us the code for the SVG (this is the same as what would be produced by the Google svg-edit tool).

    svg = svg[0].innerHTML;

After we have the SVG code, we need to manipulate it slightly to make it into a format that canvg can parse.  The first line replaces all new line characters and the second trims the svg code (canvg seems to have issues if the <svg> tag isn’t at the beginning of the string).

    svg = svg.replace(/\n/g, '');
    svg = svg.trim();

Note that if you are using an SVG image generated by the Raphael JavaScript library, you will need to add an additional line to clear up the multiple entries of the xmlns attribute in the SVG.  This can be done using the following code:

    svg = svg.replace("xmlns=\"http://www.w3.org/2000/svg\" ", " ");

Finally, we will call the canvg library and import the SVG data into the hidden canvas tag on our page.


    canvg(document.getElementById('exportCanvas'), svg, { ignoreMouse: true, ignoreAnimation: true });

Converting the Canvas to a Binary Image To convert the image in the canvas tag to binary, we create a new JavaScript function using the functions within the Base64ToArrayBuffer.js file we created earlier. This function converts the base 64 code in the canvas to a DataURL, then converts that DataURL to an array buffer using the base64DecToArr function. We then use the Uint8Array to convert the array buffer into bytes. Finally, we loop through the bytes and convert the character codes into string format.


function toBinary(canvas) {
    var ab = base64DecToArr(canvas.toDataURL().replace('data:image/png;base64,', ""));
    var binary = '';
    var bytes = new Uint8Array(ab);
    var len = bytes.byteLength;
    for (var i = 0; i < len; i++) {
        binary += String.fromCharCode(bytes[i]);
    }
    return binary;
}

We can now call the toBinary function from our code to generate the image data we will need to post to SharePoint.

    var binaryImage = toBinary(canvas);

Saving the Binary Image to SharePoint 

Note that in this sample, we are saving the image to the Documents library in the host web and not a document library in the app web. The permissions of the SharePoint app must be set to allow write permissions to the Documents library. We create a new function called createImage that we will use to create the image in SharePoint. This function has the following signature:


function createImage(listTitle, listPath, fileName, fileContents)

We start the process of creating the image by retrieving the host web url and app web url. This gets both values from the query string values passed from the host web to the SharePoint app.


    var hostweburl = decodeURIComponent(getQueryStringParameter("SPHostUrl"));
    var appweburl = decodeURIComponent(getQueryStringParameter("SPAppWebUrl"));

The getQueryStringParameter function (which is readily available multiple places on the internet) has the following definition:

function getQueryStringParameter(paramToRetrieve) {
    var params =
        document.URL.split("?")[1].split("&");
    for (var i = 0; i < params.length; i = i + 1) {
        var singleParam = params[i].split("=");
        if (singleParam[0] == paramToRetrieve)
            return singleParam[1];
    }
}

Next, we set the URL that we will use to post the image into the document library. Note that the paths are different based on whether the document will be created in the root folder or a subfolder of the document library (if the listPath variable is an empty string, the image will be in the root folder).


    var addUrl;
    if (listPath == "") {
        addUrl = appweburl + "/_api/SP.AppContextSite(@target)/web/lists/getByTitle('" + listTitle + "')/RootFolder/Files/Add(url='" + fileName + "', overwrite=true)?@target='" + hostweburl + "'";
    }
    else {
        addUrl = appweburl + "/_api/SP.AppContextSite(@target)/web/getfolderbyserverrelativeurl('" + listTitle + "/" + listPath + "')/Files/Add(url='" + fileName + "', overwrite=true)?@target='" + hostweburl + "'";
    }

Because we want to keep our application responsive and prevent locking of the UI while the REST call is being made to SharePoint, we use the Deferred object as our method of implementing promises in our application. To do this, we add


    var deferred = $.Deferred();

at the beginning of the createImage function. We then add


    return deferred;

at the end of the function. We use deferred.resolve() to complete and resolve a successful call to SharePoint and deferred.reject() to reject a failed call. We use the SP.RequestExecutor to set up the call to SharePoint and the executeAsync method to execute the request asynchronously. The method parameter must be set to “POST” as we are sending the binary data for the image to SharePoint. Also, the binaryStringRequestBody must be set to true in order to specify that the image data is a binary string. The code to make the request to SharePoint looks like the following:


function createImage(listTitle, listPath, fileName, fileContents) {
    var deferred = $.Deferred();

    var hostweburl = decodeURIComponent(getQueryStringParameter("SPHostUrl"));
    var appweburl = decodeURIComponent(getQueryStringParameter("SPAppWebUrl"));
    var addUrl;
    if (listPath == "") {
        addUrl = appweburl + "/_api/SP.AppContextSite(@target)/web/lists/getByTitle('" + listTitle + "')/RootFolder/Files/Add(url='" + fileName + "', overwrite=true)?@target='" + hostweburl + "'";
    }
    else {
        addUrl = appweburl + "/_api/SP.AppContextSite(@target)/web/getfolderbyserverrelativeurl('" + listTitle + "/" + listPath + "')/Files/Add(url='" + fileName + "', overwrite=true)?@target='" + hostweburl + "'";
    }

    var executor = new SP.RequestExecutor(appweburl);
    executor.executeAsync({
        url: addUrl,
        method: "POST",
        headers: {
            "Accept": "application/json; odata=verbose"
        },
        contentType: "application/json;odata=verbose",
        binaryStringRequestBody: true,
        body: fileContents,
        success: function (data, textStatus, xhr) {
            deferred.resolve(fileName);
        },
        error: function (xhr, textStats, errorThrown) {
            deferred.reject(JSON.stringify(xhr));
        }
    });

Now, in order to call this function, we use the following code.


    var imgTitle = "exportedSVG.png";
    $.when(createImage("Documents", "", imgTitle, binaryImage)).done(function (result) {
        alert('Export Image ' + result + ' Completed!');
    }).fail(function (err) {
        alert('Error: ' + JSON.stringify(err));
    });

Note the use of the when, done, and fail statements to handle the deferred object. If the request to SharePoint is successful, the done path will be followed, whereas, if the request fails, the fail path will be followed. Summary and Final JavaScript This article has shown how to save an SVG image within a SharePoint app to a document library in the host web. Here is the final code for the Javascript app.js file.


'use strict';

function saveImage() {
    //Clone the SVG 
    var svg = $("#svgWrapper").clone();
    svg = svg[0].innerHTML;
    svg = svg.replace("xmlns=\"http://www.w3.org/2000/svg\" ", " ");
    //Needed to replace return characters
    svg = svg.replace(/\n/g, '');
    //The <svg> tag must be the first item in the code for canvg
    svg = svg.trim();
    canvg(document.getElementById('exportCanvas'), svg, { ignoreMouse: true, ignoreAnimation: true });
    var canvas = document.getElementById('exportCanvas');
    var binaryImage = toBinary(canvas);
    var imgTitle = "exportedSVG.png";
    $.when(createImage("Documents", "", imgTitle, binaryImage)).done(function (result) {
        alert('Export Image ' + result + ' Completed!');
    }).fail(function (err) {
        alert('Error: ' + JSON.stringify(err));
    });
}

function toBinary(canvas) {
    var ab = base64DecToArr(canvas.toDataURL().replace('data:image/png;base64,', ""));
    var binary = '';
    var bytes = new Uint8Array(ab);
    var len = bytes.byteLength;
    for (var i = 0; i < len; i++) {
        binary += String.fromCharCode(bytes[i]);
    }
    return binary;
}

function createImage(listTitle, listPath, fileName, fileContents) {
    var deferred = $.Deferred();

    var hostweburl = decodeURIComponent(getQueryStringParameter("SPHostUrl"));
    var appweburl = decodeURIComponent(getQueryStringParameter("SPAppWebUrl"));
    var addUrl;
    if (listPath == "") {
        addUrl = appweburl + "/_api/SP.AppContextSite(@target)/web/lists/getByTitle('" + listTitle + "')/RootFolder/Files/Add(url='" + fileName + "', overwrite=true)?@target='" + hostweburl + "'";
    }
    else {
        addUrl = appweburl + "/_api/SP.AppContextSite(@target)/web/getfolderbyserverrelativeurl('" + listTitle + "/" + listPath + "')/Files/Add(url='" + fileName + "', overwrite=true)?@target='" + hostweburl + "'";
    }

    var executor = new SP.RequestExecutor(appweburl);
    executor.executeAsync({
        url: addUrl,
        method: "POST",
        headers: {
            "Accept": "application/json; odata=verbose"
        },
        contentType: "application/json;odata=verbose",
        binaryStringRequestBody: true,
        body: fileContents,
        success: function (data, textStatus, xhr) {
            deferred.resolve(fileName);
        },
        error: function (xhr, textStats, errorThrown) {
            deferred.reject(JSON.stringify(xhr));
        }
    });

    return deferred;
}

function getQueryStringParameter(paramToRetrieve) {
    var params =
        document.URL.split("?")[1].split("&");
    for (var i = 0; i < params.length; i = i + 1) {
        var singleParam = params[i].split("=");
        if (singleParam[0] == paramToRetrieve)
            return singleParam[1];
    }
}

And here is the final code from the Default.aspx page for the SharePoint app.


 <%@ Page Inherits="Microsoft.SharePoint.WebPartPages.WebPartPage, Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" MasterPageFile="~masterurl/default.master" Language="C#" %>

<%@ Register TagPrefix="Utilities" Namespace="Microsoft.SharePoint.Utilities" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register TagPrefix="WebPartPages" Namespace="Microsoft.SharePoint.WebPartPages" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register TagPrefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>

<%-- The markup and script in the following Content element will be placed in the <head> of the page --%>
<asp:Content ContentPlaceHolderID="PlaceHolderAdditionalPageHead" runat="server">
    <script type="text/javascript" src="../Scripts/jquery-1.9.1.min.js"></script>
    <script type="text/javascript" src="/_layouts/15/sp.runtime.js"></script>
    <script type="text/javascript" src="/_layouts/15/sp.js"></script>
    <script type="text/javascript" src="/_layouts/15/SP.RequestExecutor.js"></script>
    <script src="../Scripts/rgbcolor.js"></script>
    <script src="../Scripts/StackBlur.js"></script>
    <script src="../Scripts/canvg.js"></script>
    <script src="../Scripts/Base64ToArrayBuffer.js"></script>
    <meta name="WebPartPageExpansion" content="full" />

    <!-- Add your CSS styles to the following file -->
    <link rel="Stylesheet" type="text/css" href="../Content/App.css" />

    <!-- Add your JavaScript to the following file -->
    <script type="text/javascript" src="../Scripts/App.js"></script>
</asp:Content>

<%-- The markup in the following Content element will be placed in the TitleArea of the page --%>
<asp:Content ContentPlaceHolderID="PlaceHolderPageTitleInTitleArea" runat="server">
    Page Title
</asp:Content>

<%-- The markup and script in the following Content element will be placed in the <body> of the page --%>
<asp:Content ContentPlaceHolderID="PlaceHolderMain" runat="server">

    <div id="svgWrapper">
        <svg id="svgImage" width="640" height="480" xmlns="http://www.w3.org/2000/svg">
         <g>
          <title>Layer 1</title>
          <rect id="svg_1" height="43" width="86" y="57" x="121" stroke-width="5" stroke="#000000" fill="#FF0000"/>
          <ellipse ry="57" rx="87.5" id="svg_2" cy="100" cx="332.5" stroke-width="5" stroke="#000000" fill="#FF0000"/>
          <path id="svg_3" d="m29,171c-14,26 99,22 99,22c0,0 -4,41 -8,43c-4,2 -39,51 -39,51c0,0 -62,-58 -62,-59c0,-1 10,-57 10,-57z" stroke-linecap="null" stroke-linejoin="null" stroke-dasharray="null" stroke-width="5" stroke="#000000" fill="#FF0000"/>
          <path id="svg_4" d="m292.42999,252.03l0,-31.6875l0,0c0,-1.3461 1.0914,-2.4375 2.43869,-2.4375l29.25,0c1.3461,0 2.43631,1.0914 2.43631,2.4375c0,1.34669 -1.09021,2.4375 -2.43631,2.4375l-2.43869,0l0,31.6875c0,1.34669 -1.0914,2.4375 -2.4375,2.4375l-29.25,0l0,0c-1.3461,0 -2.4375,-1.09081 -2.4375,-2.4375c0,-1.3461 1.0914,-2.4375 2.4375,-2.4375l2.4375,0zm4.875,-34.125l0,0c1.3461,0 2.4375,1.0914 2.4375,2.4375c0,1.34669 -1.0914,2.4375 -2.4375,2.4375c-0.67245,0 -1.21875,-0.5457 -1.21875,-1.21875c0,-0.67305 0.5463,-1.21875 1.21875,-1.21875l2.4375,0m24.37619,2.4375l-26.81369,0m-4.875,29.25l0,0c0.67365,0 1.21875,0.5457 1.21875,1.21875c0,0.67305 -0.5451,1.21875 -1.21875,1.21875l2.43869,0m-2.43869,2.4375l0,0c1.3461,0 2.43869,-1.09081 2.43869,-2.4375l0,-2.4375" stroke-linecap="null" stroke-linejoin="null" stroke-dasharray="null" stroke-width="5" stroke="#000000" fill="#FF0000"/>
          <text xml:space="preserve" text-anchor="start" id="svg_5" y="146" x="110" stroke-linecap="null" stroke-linejoin="null" stroke-dasharray="null" stroke-width="0" stroke="#000000" fill="#FF0000">
           <tspan x="110" dy="0" font-weight="normal" font-style="normal" font-size="24" font-family="DroidSerif" fill="#000000" id="svg_25" xml:space="preserve">Test SVG</tspan>
          </text>
         </g>
        </svg>
    </div>
    <button type="button" onclick="saveImage();">Export to Doc Lib</button>
    <canvas id="exportCanvas" style="display:none;"></canvas>
</asp:Content>

Share:

About The Author

Managing Consultant

Bart is a Managing Consultant in the Application Development practice of Cardinal’s Raleigh/Durham office.