How to get a high quality canvas to PNG conversion - javascript

I have a requirement to take the canvas from a webpage and convert it to PDF in the backend so it can be saved on the server and downloaded & printed at a later time at 600 DPI.
I have followed this question, and I have a working prototype of the code: AJAX call to send the canvas to backend in Base64 and then a Java function to convert it to PDF.
However, the problem is that the quality of the image is dependent on the screen/browser window size the user has when he clicks the button to trigger the image creation - a fullscreen browser will create a higher-res image than a partial window browser. Example: Both taken on my PC but on the latter the window is about half the screen size.
I was thinking of somehow creating the canvas on a headless browser with preset size, and that would at least make the quality consistent across users, but I have no idea how to dynamically change the image so I can keep it at 600 DPI no matter the paper size the user chooses to use.
Do I have to draw the canvas shapes directly onto PDF? I know that would fulfill the DPI requirement, but is that even possible to do from an AngularJS/Java stack?

You can decide the proper size for the canvas and then modify the way it's displayed via CSS. Here, the final size is set as 2000x2000 and it will be saved as such (by clicking on it), regardless of the viewport size:
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// Draw the ellipse
ctx.beginPath();
ctx.lineWidth = 10;
ctx.ellipse(1000, 1000, 500, 800, Math.PI / 4, 0, 2 * Math.PI);
ctx.stroke();
// Draw the ellipse's line of reflection
ctx.beginPath();
ctx.setLineDash([5, 5]);
ctx.moveTo(0, 2000);
ctx.lineTo(2000, 0);
ctx.stroke();
canvas.addEventListener('click', function (e) {
var dataURL = canvas.toDataURL('image/png');
var link = document.createElement("a");
link.download = "finalsize.png";
link.href = dataURL;
link.click();
});
<body style="display:flexbox">
<canvas id="canvas" width="2000" height="2000" style="width:100%; "></canvas>
</body>

Related

Cropped image displayed with a weird 1px black border

EDIT: I've made a video showing the current issue. Recorded it on an old macbook so it took a while haha. Anyway, In the video you can see me uploading an image, cropping it and generating a pdf with it. The preview does not have a black border whatsoever, the pdf DOES have a black border. Hope this helped.
So I got a webapplication that lets the user upload a profile picture and later generate a pdf with the profile picture. The problem is that I would like to have the profile picture displayed with rounded corners / in a circle.
I am using jspdf to generate the pdf. With that library I can add an image with the addImage method, that takes a base64 encoded DataUrl. Unfortunately, there isn't a native method to display the image with rounded corners / in a circle, so I decided to let the user crop their image before encoding the url to base64. That's when the weird behavior starts... If the cropped image is displayed in a img tag, it's all good, a nice circle profile picture is displayed. When that dataUrl is used to generate a pdf, for some reason a 1px black border is generated around the image... If I inspect the dataurl in the browser or with a online base64 previewer, no black border is visible, only when it gets generated as a pdf...
For demonstration purposes I made a codesandbox. Images with a white background show the problem best. For example, use this image: profilepicture
This method is most likely what is causing the problem (I took this one directly from the cropperjs examples on github):
function getRoundedCanvas(sourceCanvas) {
var canvas = document.createElement("canvas");
var context = canvas.getContext("2d");
var width = sourceCanvas.width;
var height = sourceCanvas.height;
canvas.width = width;
canvas.height = height;
context.imageSmoothingEnabled = true;
context.drawImage(sourceCanvas, 0, 0, width, height);
context.globalCompositeOperation = "destination-in";
context.beginPath();
context.arc(
width / 2,
height / 2,
Math.min(width, height) / 2,
0,
2 * Math.PI,
true
);
context.fill();
return canvas;
}
This method is used in the crop method like this: roundedCanvas = getRoundedCanvas(croppedCanvas); If I take out that method and just use roundedCanvas = croppedCanvas; the image gets cropped to a square and is displayed without the black borders.
I have no idea how I can resolve this issue, and if it is even possible to resolve. Any help in the right direction is very much appreciated. Even alternative methods to display rounded / circle images on a pdf are welcome (I already tried html2canvas and that didn't work).

How to remove a drawn arch from canvas

I have been trying to print arc in the html page. How can i remove the already drawn arch from the page?. i have written the below code.
<!DOCTYPE html>
<html>
<body>
<canvas id="myCanvas" width="1200" height="1000"
style="border:1px solid #d3d3d3;">
Your browser does not support the HTML5 canvas tag.</canvas>
<script>
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
/*ctx.beginPath();
ctx.arc(600,500,20, 0.5*Math.PI,2*Math.PI);
ctx.stroke();
ctx.beginPath();
ctx.arc(600,500,40, 0.5*Math.PI,2*Math.PI);
ctx.stroke();
*/
var radius=20;
for (i=0; i<10; i++)
{
var ctx = c.getContext("2d");
ctx.beginPath();
ctx.arc(600,500,radius, 0.5*Math.PI, 2*Math.PI);
ctx.stroke();
radius= radius+30;
}
</script>
</body>
</html>
How can i achieve this?.
Call clearRect method:
ctx.clearRect(0, 0, 1200, 1000)
The four arguments are:
axis-X of left top corner of the area to wipe
axis-Y of left top corner of the area to wipe
width of the area to wipe
height of the area to wipe
So with this method, you could wipe either the whole canvas or just a certain part of it.
If you want to remove the whole previously drawn image please take a look at the other anwers. In the comments OP made it clear that this is not what he was trying to achieve. So instead I will answer the intended question:
How do I un-stroke a path?
A bitmap is not a vector graphic. You cannot simply remove or modify things you've drawn previously. By drawing on a canvas you modify the pixel color values of its image data. If you need to undo things you have to maintain a separate data structure with the relevant data yourself.
For example you could create a copy of the image data before drawing something. Then you could return to this snapshot afterwards. HTMLCanvasElement#toDataURL returns the complete image as an url which you can use as the src of an image. Later you can draw this image on the canvas to revert all subsequent changes. HTMLCanvasElement#toBlob does about the same but it returns a blob. This might consume less memory but it's a little more inconvenient to use. The most convenient method is CanvasRenderingContext2D#getImageData. You can even use it to copy only a small part of the image. This is useful if you have a big canvas but only modify pixels in a small region.
Another way to make modifications undoable is by maintaining a detailed list of your steps. For example whenever you draw an arc you store the exact parameters as one entry in the list. steps.push({type: 'stroke', style: 'rgb(0,0,0)', shapes: [{type: 'arc', x: 600, y: 500, radius: radius, from: 0.5 * Math.PI, to: 2 * Math.PI}]}) You can remove, rearrange or modify the elements in this list any way you like and have all necessary information to draw the resulting image from scratch. Basically you've implemented just another vector graphic library.

Image to base64 in fixed resolution

In my webapplication the user can take a photo with the camera on his mobile device. The photo is then displayed in a canvas.
<canvas id="photo" ></canvas>
Now I want to send the photo to a backend server. The best way to do this seems to be encoding the image to a base64 string.
However, I want the photo to always be the same resolution, for example 100 x 100 pixels. Otherwise the base64 string is too big.
I am currently using the second parameter of toDataURL to determine the export quality of the picture. (here 2%).
var base64 = document.getElementById("photo").toDataURL("image/jpeg", 0.02);
This does not seem to be a good way because I don't know the initial resolution of the photo. So I would like to always get the photo in the same resolution. How to achieve this?
You could use an invisible background canvas with a size of 100x100 and copy the image from the photo-canvas to it. The drawImage function allows you to specify a size for the destination rectangle. When it's not the same as the source rectangle, the content will be scaled:
// Create a temporary, invisible 100x100 canvas
var tmpCanvas = document.createElement('canvas');
tmpCanvas.width = 100;
tmpCanvas.height = 100;
var tmpContext = canvas.getContext('2d');
// copy the content of the photo-canvas to the temp canvas rescaled
var photoCanvas = document.getElementById("photo");
tmpContext.drawImage(photoCanvas, // source
0, 0, photoCanvas.width, photoCanvas.height, // source rect
0, 0, 100, 100 // destination rect
);
// get data-url of temporary canvas
var base64 = tmpCanvas.toDataURL("image/jpeg");
But keep in mind that when the source-canvas isn't rectangular, the aspect ratio won't be preserved. When you don't want this, there are different ways to deal with this issue, like cropping or adding a border. Most of these would be implemented by choosing different source- or destination rectangles in the drawImage call.

Zooming on HTML5 <canvas> and no pixelation for text?

Let's put some text on a HTML5 <canvas> with
var canvas = document.getElementById('myCanvas'),
ctx = canvas.getContext('2d'),
ctx.textBaseline = 'top';
ctx.textAlign = 'left';
ctx.font = '14px sans-serif';
ctx.fillText('Bonjour', 10, 10);
When zooming the canvas on text, one can see pixelation.
Is there a way of zooming on a canvas without having pixelation on text ?
When you fillText on the canvas, it stops being letters and starts being a letter-shaped collection of pixels. When you zoom in on it, the pixels become bigger. That's how a canvas works.
When you want the text to scale as a vector-based font and not as pixels, don't draw them on the canvas. You could create <span> HTML elements instead and place them on top of the canvas using CSS positioning. That way the rendering engine will render the fonts in a higher resolution when you zoom in and they will stay sharp. But anything you draw on the canvas will zoom accordingly.
Alternatively, you could override the browsers zoom feature and create your own zooming algorithm, but this will be some work.
When the user zooms in or out of the window, the window.onresize event handler is triggered. You can use this trigger to adjust the width and the height of the canvas css styling accordingly (not the properties of the canvas. That's the internal rendering resolution. Change the width and height attributes of the style which is the resolution it is scaled to on the website).
Now you effectively disabled the users web browser from resizing the canvas, and also have a place where you can react on the scaling input events. You can use this to adjust the context.scale of your canvas to change the size of everything you draw, including fonts.
Here is an example:
<!DOCTYPE html>
<html>
<head>
<script type="application/javascript">
"use strict"
var canvas;
var context;
function redraw() {
// clears the canvas and draws a text label
context.clearRect(0, 0, context.canvas.width, context.canvas.height);
context.font = "60pt sans-serif";
context.fillText("Hello World!", 100, 100);
}
function adjustSize() {
var width = window.innerWidth;
var height = window.innerHeight;
// resize the canvas to fill the whole screen
var style = canvas.style;
style.width = width + "px";
style.height = height + "px";
// backup the old current scaling factor
context.save();
// change the scaling according to the new zoom factor
context.scale(1000 / width, 1000 / height);
// redraw the canvas
redraw();
// restore the original scaling (important because multiple calls to scale are relative to the current scale factor)
context.restore();
}
window.onload = function() {
canvas = document.getElementById("myCanvas");
context = canvas.getContext("2d");
adjustSize();
}
window.onresize = adjustSize;
</script>
</head>
<body>
<canvas id ="myCanvas" width = 1000 height = 1000 ></canvas>
</body>
</html>
If you only need to scale text you can simply scale the font size.
A couple of notes on that however: fonts, or typefaces, are not just straight forward to scale meaning you will not get a smooth progress. This is because fonts are often optimized for certain sizes so the sizes in between so to speak are a result of the previous and next size. This can make the font look like it's moving around a little when scaled up and is normal and expected.
The approach here uses a simply size scale. If you need an absolute smooth scale for animation purposes you will have to use a very different technique.
The simple way is:
ctx.font = (fontSize * scale).toFixed(0) + 'px sans-serif';
An online demo here.
For animation purposes you would need to do the following:
Render a bigger size to an off-screen canvas which is then used to draw the different sizes
When the difference is too big and you get problems with interpolation you will have to render several of these cached text images at key sizes so you can switch between them when scaling factor exceeds a certain threshold.
In this demo you can see that at small sizes the pixels gets a bit "clumpy" but otherwise is much smoother than a pure text approach.
This is because the browser uses bi-linear interpolation rather than bi-cubic with canvas (this may or may not change in the future) so it's not able to interpolate properly when the difference gets to big (see below for solution with this issue).
The opposite happens at big sizes as the text gets blurry also due to interpolation.
This is where we would have to switch to a smaller (or bigger) cached version which we then scale within a certain range before we again switch.
The demo is simplified to show only a single cached version. You can see halfway through that this works fine. The principle would be in a full solution (sizes being just examples):
(Update Here is a demo of a switched image during scale).
-- Cached image (100px)
-- Draw cached image above scaled based on zoom between 51-100 pixels
-- Cached image (50px) generated from 100px version / 2
-- Draw cached image above scaled based on zoom between 26-50 pixels
-- Cached image (25px) generated from 50px version / 2
-- Draw cached image above scaled based on zoom between 1-25 pixels
Then use a "sweet spot" (which you find by experiment a little) to toggle between the cached versions before drawing them to screen.
var ctx = canvas.getContext('2d'),
scale = 1, /// initial scale
initialFactor = 6, /// fixed reduction scale of cached image
sweetSpot = 1, /// threshold to switch the cached images
/// create two off-screen canvases
ocanvas = document.createElement('canvas'),
octx = ocanvas.getContext('2d'),
ocanvas2 = document.createElement('canvas'),
octx2 = ocanvas2.getContext('2d');
ocanvas.width = 800;
ocanvas.height = 150;
ocanvas2.width = 400; /// 50% here, but maybe 75% in your case
ocanvas2.height = 75; /// experiment to find ideal size..
/// draw a big version of text to first off-screen canvas
octx.textBaseline = 'top';
octx.font = '140px sans-serif';
octx.fillText('Cached text on canvas', 10, 10);
/// draw a reduced version of that to second (50%)
octx2.drawImage(ocanvas, 0, 0, 400, 75);
Now we only need to check the sweet spot value to find out when to switch between these versions:
function draw() {
/// calc dimensions
var w = ocanvas.width / initialFactor * scale,
h = ocanvas.height / initialFactor * scale;
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (scale >= sweetSpot) {
ctx.drawImage(ocanvas, 10, 10, w, h); /// use cached image 1
} else {
ctx.drawImage(ocanvas2, 10, 10, w, h); /// use cached image 2
}
}
So why not just draw the second cached image with a font? You can do that but then you are back to the issue with fonts being optimized for certain sizes and it would generate a small jump when scaling. If you can live with that then use this as it will provide a little better quality (specially at small sizes). If you need smooth animation you will have to reduce a larger cached version in order to keep the size 100% proportional.
You can see this answer on how to get a large image resized without interpolation problems.
Hope this helps.

Hit detection on non-transparent pixel

Given a PNG in a web context with some transparent pixels and some non-transparent pixels, is there a way in Javascript to determine if a user has clicked on a non-transparent pixel? A webkit-only solution would be perfectly acceptable.
1) Create HTML5 canvas of same size as your image
2) Get Canvas's context, drawImage(yourImage, 0, 0)
3) d = context.getImageData(0, 0, w of img, h of img)
4) d.data[(y*width+x)*4+3] for the alpha
canvas = document.createElement("canvas"); //Create HTML5 canvas: supported in latest firefox/chrome/safari/konquerer. Support in IE9
canvas.width = img.width; //Set width of your rendertarget
canvas.height = img.height; // \ height \ \ \
ctx = canvas.getContext("2d"); //Get the 2d context [the thing you draw on]
ctx.drawImage(img, 0, 0); //Draw the picture on it.
id = ctx.getImageData(0,0, img.width, img.height); //Get the pixelData of image
//id.data[(y*width+x)*4+3] for the alpha value of pixel at x,y, 0->255
I know these things are out of fashion these days, but HTML image maps are still valid, and can accomplish adding hit targets to nearly-arbitrary shapes within an image. If you don't actually want to reload another page on click, you could probably change the anchor in the URL with this technique and pick up the change with a Javascript interval.
Canvas is the way to go for this purpose. But also remember that older internet explorer versions will not be capable of the getImageData() function. Even if you include excanvas.
I made a small jquery plugin exactly for this purpose, maybe it will help you solving your problem without to completely reinvent the wheel. http://www.cw-internetdienste.de/pixelselection/

Categories