1

I need to convert the visible area of the SVG to a static image for printing and similar purposes. I gather from reading that the approach should be to first convert the SVG to a canvas using canvg, and then convert the canvas to an image using canvas.toDataURL. I able to accomplish these basic requirements, however my viewbox is much smaller than my SVG, thus cropping and scaling become issues and this is where I am presently stumbling. My thought was to insert an additional two steps prior to converting to an image where I use canvasContext.drawImage(...) to crop the unscaled canvas to the region defined by the SVG viewbox. This last step does not work for me: I cannot manage to crop the image and maintain the present scale. I then intend to use pica.resizeCanvas to achieve high quality scaling of the image (such that it fits the printable page).

Before getting into code the questions are:

  • Is the over all approach sound?
  • What am I doing wrong with canvasContext.drawImage(...) that the image either ends up scaled or cropped incorrectly? The objective is to apply no scaling but to crop some excess white space.

Summary of Steps

  • Copy svg to canvas using canvg. This works.
    • No scaling is applied
    • Offsets are used to move the image to the upper left corner in preparation for future cropping of remaining white space (lower right region).
  • Get a hold of newly created canvas and draw cropped region to a secondary canvas using canvasContext.drawImage. This fails. Either the canvas is the wrong size but the scaling is correct (ie none) or the canvas is the right size but the scaling is wrong (scaled way up).
  • Use pica.resizeCanvas to apply scaling with minimal loss of quality. Have not really tried this yet.
  • Use canvasContext.toDataURL(...) to convert canvas to png. This works.

Code

function MakeImage() {

    //min-x, min-y, width and height
    var viewBox = parseViewBox();   //defined below
    var vbMinX = viewBox[0];
    var vbMinY = viewBox[1];
    var vbWidth = viewBox[2];
    var vbHeight = viewBox[3];

    var svgUnitRatio = getSvgUnitRatio(vbHeight);   //defined below

    //xMin,yMin,xMax,yMax
    var boundingBox = getBounds();  //defined below
    var bbXmin = boundingBox[0];
    var bbYmin = boundingBox[1];
    var bbXmax = boundingBox[2];
    var bbYmax = boundingBox[3];

    var offsetX = (vbMinX - bbXmin) * svgUnitRatio;
    var offsetY = (vbMinY - bbYmin) * svgUnitRatio;

    var adjustedWidth = (bbXmax - bbXmin) * svgUnitRatio;
    var adjustedHeight = (bbYmax - bbYmin) * svgUnitRatio;

    var options = {
        ignoreDimensions: false,    //allow it to resize the canvas based on the svg size
        offsetX: offsetX,
        offsetY: offsetY
    };

    //first we copy the svg to a canvas w/o applying any scaling
    window.canvg("workspaceCanvas", $("#mysvg").parent().html(), options);

    //now we crop according the svg viewbox 
    var canvas = document.getElementById("canvas");
    var workspaceCanvas = document.getElementById("workspaceCanvas");

    var context = canvas.getContext('2d');
    context.drawImage(workspaceCanvas, 0, 0, adjustedWidth, adjustedHeight, 0, 0, adjustedWidth, adjustedHeight);   //something is wrong here i guess???

    //next we do a high quality scaling of the canvas 
    var pOptions = {            //maybe this has problems but i won't kow until i get the previous step right
        quality: 3,
        alpha: true,
        unsharpAmount: 50,
        unsharpRadius: 0.5,
        unsharpThreshold: 0
    };

    //holding off on trying this for now
    window.pica.resizeCanvas(workspaceCanvas, canvas, pOptions, function(err) { /*this is a mandatory argument*/ });

    var img = canvas.toDataURL("image/png,1");

    //do stuff with image data
}

function getSvgUnitRatio(viewboxHeight) {
    //shouldnt need to worry about width since the aspect ratio should be locked
    var height = parseFloat(d3.select("#mysvg").attr("height"));

    return height / viewboxHeight;
}

function getBounds() {

    //xMin,yMin,xMax,yMax
    var boundingBox = [];

    var xMin = Number.MAX_SAFE_INTEGER;
    var yMin = Number.MAX_SAFE_INTEGER;
    var xMax = Number.MIN_SAFE_INTEGER;
    var yMax = Number.MIN_SAFE_INTEGER;

    window.svg.selectAll(".elementsICareAbout").nodes().forEach(function(d) {
        var dx = parseFloat(d.getAttribute("x"));
        var dy = parseFloat(d.getAttribute("y"));
        var width = parseFloat(d.getAttribute("width"));
        var height = parseFloat(d.getAttribute("height"));

        if (dx + width > xMax) {
            xMax = dx + width;
        }

        if (dx < xMin) {
            xMin = dx;
        }

        if (dy + height > yMax) {
            yMax = dy + height;
        }

        if (dy < yMin) {
            yMin = dy;
        }
    });

    var padding = 25;   //add some fluff

    //xMin,yMin,xMax,yMax
    boundingBox = [
        xMin - padding, yMin - padding, xMax + padding, yMax + padding
    ];

    return boundingBox;
}

function parseViewBox() {
    var str = d3.select("#mysvg").attr("viewBox");

    var parts = str.split(" ");

    var parsed = [];

    parts.forEach(function(p) {
        parsed.push(parseFloat(p));
    });

    return parsed;
}
Brian Sweeney
  • 6,693
  • 14
  • 54
  • 69
  • 1
    You could always apply a clip or clipPath to the viewBox. – Robert Longson Feb 20 '17 at 19:15
  • 1
    If you want to provide a full size inline svg for printing, then you could use the events window.onbeforeprint , window.onafterprint, and window.matchmedia (Chrome). This provides the ability to manipulate the svg to fill the window, then reset it after print. (This does not need the canvas). I can provide an example is you want to see this approach. – Francis Hemsher Feb 20 '17 at 22:04
  • @FrancisHemsher if you have an example handy i'd love to see it. – Brian Sweeney Feb 21 '17 at 22:57

1 Answers1

1

If you want to provide a full size inline svg for printing, then you could use the events window.onbeforeprint , window.onafterprint, and window.matchmedia (Chrome). This provides the ability to manipulate the svg to fill the window, then reset it after print. (This does not need the canvas). Below is an example of this approach.

NOTE: to test this, the below must be copied into a HTML file on your computer and called into your browser.

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <title>Print SVG Full Size</title>

</head>
<body style='padding:10px;font-family:arial'>
<center>
<h4>Print SVG Full Size</h4>
<div style='width:90%;background-color:gainsboro;text-align:justify;padding:10px;border-radius:6px;'>
You can print the inline SVG segment of your web page as full size. This uses the browser's 'Print..' feature, the window events <b>onbeforeprint</b>, <b>onafterprint</b>, plus <b>window.matchMedia</b>.
</div>
<table><tr>
<td>
<div style="padding:10px;width:400px;text-align:justify">

<b>Scenerio:</b><br />
Select the browser's <b>Print..</b><br> <br>
The function <b>beforePrint</b> hides all elements except the DIV containing the inline SVG, plus the DIV is postioned to top/left at 0 px. The SVG and the DIV are sized at 100%.
<br><br>
The function <b>afterPrint</b> returns the elements to their original visibility and locatons.<br> <br>
The event <b>window.matchMedia</b> automatically calls the above functions for Chrome.<br>
Both IE and FF use the window events <b>onbeforeprint</b> and <b>onafterprint</b>.
<p></p>
Note: A Print 'Save as PDF' has the nice feature called 'Snapshot' that can be used to clip just the SVG portion of the PDF and save it, via any image editor, as a .png file.
</div>
</td>
<td>
<div id="svgDiv" style='width:400px;height:400px;'>
<svg id="mySVG" width="400" height="400">
<rect x=0 y=0 width="400" height="400" stroke="none" fill="red" />
<circle cx=200 cy=200 fill=yellow r=150 stroke=none />
</svg>
</div>

</td>
</tr></table>
<script>
function beforePrint()
{
    document.body.style.visibility="hidden"
    svgDiv.style.visibility='visible'
    svgDiv.style.position="absolute"
    svgDiv.style.top="0px"
    svgDiv.style.left="0px"
    svgDiv.style.width="100%"
    svgDiv.style.height="100%"

    var bb=mySVG.getBBox()
    var bbx=bb.x
    var bby=bb.y
    var bbw=bb.width
    var bbh=bb.height

    mySVG.setAttribute("viewBox",bbx+" "+bby+" "+bbw+" "+bbh )
    mySVG.setAttribute("width","100%")
    mySVG.setAttribute("height","100%")
}

function afterPrint()
{
    document.body.style.visibility=""
    svgDiv.style.visibility=''
    svgDiv.style.position=""
    svgDiv.style.top=""
    svgDiv.style.left=""
    mySVG.removeAttribute("viewBox")
    mySVG.setAttribute("width","400")
    mySVG.setAttribute("height","400")
}
//---Chrome Browser---
 if (window.matchMedia)
 {
        var mediaQueryList = window.matchMedia('print');
        mediaQueryList.addListener(function(mql)
            {
                if (mql.matches)
                {
                    beforePrint();
                }
                else
                {
                    afterPrint();
                }
            }
        );
 }

     //---IE & FF---
window.onbeforeprint = beforePrint
window.onafterprint = afterPrint;
</script>



</body>

</html>
Francis Hemsher
  • 3,478
  • 2
  • 13
  • 15