11

I'm using Raphael to draw an object, then transferring it to an HTML canvas element with canvg so that I can use toDataURL to save it as a PNG. But when I use canvg, the resulting image is blurry. The code below, for example, produces this (raphael on top, canvg on bottom):

enter image description here

<html>
    <head>
        <script src="lib/raphael-min.js"></script>
        <script type="text/javascript" src="http://canvg.googlecode.com/svn/trunk/rgbcolor.js"></script> 
        <script type="text/javascript" src="http://canvg.googlecode.com/svn/trunk/StackBlur.js"></script>
        <script type="text/javascript" src="http://canvg.googlecode.com/svn/trunk/canvg.js"></script> 
        <script src="lib/raphael.export.js"></script>
    </head>
    <body>

    <div id="raph_canvas"></div><br> 
    <canvas id="html_canvas" width="50px" height="50px"></canvas>

    <script language="JavaScript">
    var test=Raphael("raph_canvas",50,50);
    var rect=test.rect(0,0,50,50);
    rect.attr({fill: '#fff000', 'fill-opacity':1, 'stroke-width':1})

    window.onload = function() {
        var canvas_svg = test.toSVG();
        canvg('html_canvas',canvas_svg);
        var canvas_html = document.getElementById("html_canvas");
    }

    </script>
    </body>
</html>

The blurriness is evident in the png created by toDataURL as well. Any idea what is going on here? I don't think this has anything to do with re-sizing. I've tried setting ignoreDimensions: True and some other things.

Another datapoint. If I use raphael to output some text and then use canvg, it is not only blurry but the wrong font!

enter image description here

And here is the test.rect(0.5,0.5,50,50) suggested. Still blurry:

enter image description here

royhowie
  • 11,075
  • 14
  • 50
  • 67
AustinC
  • 826
  • 1
  • 8
  • 23
  • It seems to be much less blurry if the rectangle is quite large. Other than that I haven't found out much. – AustinC Jun 24 '14 at 22:03
  • Without doing any tests I wonder what the result is when the stroke width is 2px. I bet you're dealing with a half pixel issue. – ericjbasti Jun 25 '14 at 01:35
  • What's the toDataURL code look like? Make sure it's not set to JPEG. JPEGs suck at line art. – ericjbasti Jun 25 '14 at 01:41
  • 1
    I can't help you with this canvg lib (never heard of it before this question).. but here you can see that its normally not an issue. http://jsbin.com/liquxiyi/2/ – ericjbasti Jun 25 '14 at 01:56
  • It's harder to notice the blurriness for thicker line widths but definitely still there. Using png: window.location = canvas_html.toDataURL("image/png"). – AustinC Jun 25 '14 at 01:57
  • do this test.rect(0.5,0.5,50,50); does that fix it? the line is drawn half on each side of the point. you should be able to see it in the jsbin. – ericjbasti Jun 25 '14 at 01:59
  • I'm starting to wonder if it is specific to Raphael. – AustinC Jun 25 '14 at 01:59
  • 1
    Hey are you on a retina device? – ericjbasti Jun 25 '14 at 02:36
  • Yes, and you win. Can't believe it was that stupid. Looks fine on my wife's air.As a followup, how can I fix this for retina devices? I tried drawing the initial raphael object at 2x size and then using canvg's width and height elements to constraint it to 50x50, but that didn't work. – AustinC Jun 25 '14 at 03:30
  • I posted a more involved answer that hopefully explains everything for you. Raphael isn't the issue, so don't change how you're doing anything there. – ericjbasti Jun 25 '14 at 03:35

2 Answers2

16

So it took me a while, but then it dawned on me. All your example images are twice the size the code claims they should be. So you're most likely on some sort of HDPI device (Retina MacBook Pro ect...) SVG is great because its resolution independent, canvas on the other hand is not. The issue you're seeing has to do with how canvas renders. To fix this, you need to prep the canvas so that your drawing will be done at the resolution of your screen.

http://jsbin.com/liquxiyi/3/edit?html,js,output

This jsbin example should look great on any screen.

The trick:

var cv = document.getElementById('box');
var ctx = cv.getContext("2d");

// SVG is resolution independent. Canvas is not. We need to make our canvas 
// High Resolution.

// lets get the resolution of our device.
var pixelRatio = window.devicePixelRatio || 1;

// lets scale the canvas and change its CSS width/height to make it high res.
cv.style.width = cv.width +'px';
cv.style.height = cv.height +'px';
cv.width *= pixelRatio;
cv.height *= pixelRatio;

// Now that its high res we need to compensate so our images can be drawn as 
//normal, by scaling everything up by the pixelRatio.
ctx.setTransform(pixelRatio,0,0,pixelRatio,0,0);


// lets draw a box
// or in your case some parsed SVG
ctx.strokeRect(20.5,20.5,80,80);

// lets convert that into a dataURL
var ur = cv.toDataURL();

// result should look exactly like the canvas when using PNG (default)
var result = document.getElementById('result');
result.src=ur;

// we need our image to match the resolution of the canvas
result.style.width = cv.style.width;
result.style.height = cv.style.height;

This should explain the issue you're having, and hopefully point you in a good direction to fix it.

ericjbasti
  • 2,085
  • 17
  • 19
7

Another solution described in this article, similar to the one posted here, except it's using scale() and it's taking into account the pixel ratio of the backing store (browser underlying storage of the canvas):

var devicePixelRatio = window.devicePixelRatio || 1,
    backingStoreRatio = context.webkitBackingStorePixelRatio ||
                        context.mozBackingStorePixelRatio ||
                        context.msBackingStorePixelRatio ||
                        context.oBackingStorePixelRatio ||
                        context.backingStorePixelRatio || 1,

    ratio = devicePixelRatio / backingStoreRatio;

// upscale the canvas if the two ratios don't match
if(devicePixelRatio !== backingStoreRatio){

   // adjust the original width and height of the canvas
   canvas.width = originalWidth * ratio;
   canvas.height = originalHeight * ratio;

   // scale the context to reflect the changes above
   context.scale(ratio, ratio);
}

// ...do the drawing here...

// use CSS to bring the entire thing back to the original size
canvas.style.width = originalWidth + 'px';
canvas.style.height = originalHeight + 'px';
nice ass
  • 16,471
  • 7
  • 50
  • 89