Is it possible to capture or print what's displayed in an HTML canvas as an image or PDF?
I'd like to generate an image via canvas and be able to generate a PNG from that image.
Original answer was specific to a similar question. This has been revised:
const canvas = document.getElementById('mycanvas')
const img = canvas.toDataURL('image/png')
With the value in img you can write it out as a new image like so:
document.getElementById('existing-image-id').src = img
or
document.write('<img src="'+img+'"/>');
HTML5 provides Canvas.toDataURL(mimetype) which is implemented in Opera, Firefox, and Safari 4 beta. There are a number of security restrictions, however (mostly to do with drawing content from another origin onto the canvas).
So you don't need an additional library.
e.g.
<canvas id=canvas width=200 height=200></canvas>
<script>
window.onload = function() {
var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");
context.fillStyle = "green";
context.fillRect(50, 50, 100, 100);
// no argument defaults to image/png; image/jpeg, etc also work on some
// implementations -- image/png is the only one that must be supported per spec.
window.location = canvas.toDataURL("image/png");
}
</script>
Theoretically this should create and then navigate to an image with a green square in the middle of it, but I haven't tested.
I thought I'd extend the scope of this question a bit, with some useful tidbits on the matter.
In order to get the canvas as an image, you should do the following:
var canvas = document.getElementById("mycanvas");
var image = canvas.toDataURL("image/png");
You can use this to write the image to the page:
document.write('<img src="'+image+'"/>');
Where "image/png" is a mime type (png is the only one that must be supported). If you would like an array of the supported types you can do something along the lines of this:
var imageMimes = ['image/png', 'image/bmp', 'image/gif', 'image/jpeg', 'image/tiff']; //Extend as necessary
var acceptedMimes = new Array();
for(i = 0; i < imageMimes.length; i++) {
if(canvas.toDataURL(imageMimes[i]).search(imageMimes[i])>=0) {
acceptedMimes[acceptedMimes.length] = imageMimes[i];
}
}
You only need to run this once per page - it should never change through a page's lifecycle.
If you wish to make the user download the file as it is saved you can do the following:
var canvas = document.getElementById("mycanvas");
var image = canvas.toDataURL("image/png").replace("image/png", "image/octet-stream"); //Convert image to 'octet-stream' (Just a download, really)
window.location.href = image;
If you're using that with different mime types, be sure to change both instances of image/png, but not the image/octet-stream.
It is also worth mentioning that if you use any cross-domain resources in rendering your canvas, you will encounter a security error when you try to use the toDataUrl method.
function exportCanvasAsPNG(id, fileName) {
var canvasElement = document.getElementById(id);
var MIME_TYPE = "image/png";
var imgURL = canvasElement.toDataURL(MIME_TYPE);
var dlLink = document.createElement('a');
dlLink.download = fileName;
dlLink.href = imgURL;
dlLink.dataset.downloadurl = [MIME_TYPE, dlLink.download, dlLink.href].join(':');
document.body.appendChild(dlLink);
dlLink.click();
document.body.removeChild(dlLink);
}
I would use "wkhtmltopdf". It just work great. It uses webkit engine (used in Chrome, Safari, etc.), and it is very easy to use:
wkhtmltopdf stackoverflow.com/questions/923885/ this_question.pdf
That's it!
Try it
Here is some help if you do the download through a server (this way you can name/convert/post-process/etc your file):
-Post data using toDataURL
-Set the headers
$filename = "test.jpg"; //or png
header('Content-Description: File Transfer');
if($msie = !strstr($_SERVER["HTTP_USER_AGENT"],"MSIE")==false)
header("Content-type: application/force-download");else
header("Content-type: application/octet-stream");
header("Content-Disposition: attachment; filename=\"$filename\"");
header("Content-Transfer-Encoding: binary");
header("Expires: 0"); header("Cache-Control: must-revalidate");
header("Pragma: public");
-create image
$data = $_POST['data'];
$img = imagecreatefromstring(base64_decode(substr($data,strpos($data,',')+1)));
-export image as JPEG
$width = imagesx($img);
$height = imagesy($img);
$output = imagecreatetruecolor($width, $height);
$white = imagecolorallocate($output, 255, 255, 255);
imagefilledrectangle($output, 0, 0, $width, $height, $white);
imagecopy($output, $img, 0, 0, 0, 0, $width, $height);
imagejpeg($output);
exit();
-or as transparent PNG
imagesavealpha($img, true);
imagepng($img);
die($img);
This is the other way, without strings although I don't really know if it's faster or not. Instead of toDataURL (as all questions here propose). In my case want to prevent dataUrl/base64 since I need a Array buffer or view. So the other method in HTMLCanvasElement is toBlob. (TypeScript function):
export function canvasToArrayBuffer(canvas: HTMLCanvasElement, mime: string): Promise<ArrayBuffer> {
return new Promise((resolve, reject) => canvas.toBlob(async (d) => {
if (d) {
const r = new FileReader();
r.addEventListener('loadend', e => {
const ab = r.result;
if (ab) {
resolve(ab as ArrayBuffer);
}
else {
reject(new Error('Expected FileReader result'));
}
}); r.addEventListener('error', e => {
reject(e)
});
r.readAsArrayBuffer(d);
}
else {
reject(new Error('Expected toBlob() to be defined'));
}
}, mime));
}
Another advantage of blobs is you can create ObjectUrls to represent data as files, similar to HTMLInputFile's 'files' member. More info:
https://developer.mozilla.org/en/docs/Web/API/HTMLCanvasElement/toBlob
Another interesting solution is PhantomJS.
It's a headless WebKit scriptable with JavaScript or CoffeeScript.
One of the use case is screen capture : you can programmatically capture web contents, including SVG and Canvas and/or Create web site screenshots with thumbnail preview.
The best entry point is the screen capture wiki page.
Here is a good example for polar clock (from RaphaelJS):
>phantomjs rasterize.js http://raphaeljs.com/polar-clock.html clock.png
Do you want to render a page to a PDF ?
> phantomjs rasterize.js 'http://en.wikipedia.org/w/index.php?title=Jakarta&printable=yes' jakarta.pdf
If you are using jQuery, which quite a lot of people do, then you would implement the accepted answer like so:
var canvas = $("#mycanvas")[0];
var img = canvas.toDataURL("image/png");
$("#elememt-to-write-to").html('<img src="'+img+'"/>');
The key point is
canvas.toDataURL(type, quality)
And I want to provide an example for someone like me who wants to save SVG to PNG(also can add some text if you wish), which may be from an Online source or font-awesome icon, etc.
Example
100% javascript and no other 3-rd library.
<script>
(() => {
window.onload = () => {
// Test 1: SVG from Online
const canvas = new Canvas(650, 500)
// canvas.DrawGrid() // If you want to show grid, you can use it.
const svg2img = new SVG2IMG(canvas.canvas, "https://upload.wikimedia.org/wikipedia/commons/b/bd/Test.svg")
svg2img.AddText("Hello", 100, 250, {mode: "fill", color: "yellow", alpha: 0.8})
svg2img.AddText("world", 200, 250, {mode: "stroke", color: "red"})
svg2img.AddText("!", 280, 250, {color: "#f700ff", size: "72px"})
svg2img.Build("Test.png")
// Test 2: URI.data
const canvas2 = new Canvas(180, 180)
const uriData = "data:image/svg+xml;base64,PHN2ZyBjbGFzcz0ic3ZnLWlubGluZS0tZmEgZmEtc21pbGUtd2luayBmYS13LTE2IiBhcmlhLWhpZGRlbj0idHJ1ZSIgZm9jdXNhYmxlPSJmYWxzZSIgZGF0YS1wcmVmaXg9ImZhciIgZGF0YS1pY29uPSJzbWlsZS13aW5rIiByb2xlPSJpbWciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDQ5NiA1MTIiIGRhdGEtZmEtaTJzdmc9IiI+PHBhdGggZmlsbD0iY3VycmVudENvbG9yIiBkPSJNMjQ4IDhDMTExIDggMCAxMTkgMCAyNTZzMTExIDI0OCAyNDggMjQ4IDI0OC0xMTEgMjQ4LTI0OFMzODUgOCAyNDggOHptMCA0NDhjLTExMC4zIDAtMjAwLTg5LjctMjAwLTIwMFMxMzcuNyA1NiAyNDggNTZzMjAwIDg5LjcgMjAwIDIwMC04OS43IDIwMC0yMDAgMjAwem0xMTcuOC0xNDYuNGMtMTAuMi04LjUtMjUuMy03LjEtMzMuOCAzLjEtMjAuOCAyNS01MS41IDM5LjQtODQgMzkuNHMtNjMuMi0xNC4zLTg0LTM5LjRjLTguNS0xMC4yLTIzLjctMTEuNS0zMy44LTMuMS0xMC4yIDguNS0xMS41IDIzLjYtMy4xIDMzLjggMzAgMzYgNzQuMSA1Ni42IDEyMC45IDU2LjZzOTAuOS0yMC42IDEyMC45LTU2LjZjOC41LTEwLjIgNy4xLTI1LjMtMy4xLTMzLjh6TTE2OCAyNDBjMTcuNyAwIDMyLTE0LjMgMzItMzJzLTE0LjMtMzItMzItMzItMzIgMTQuMy0zMiAzMiAxNC4zIDMyIDMyIDMyem0xNjAtNjBjLTI1LjcgMC01NS45IDE2LjktNTkuOSA0Mi4xLTEuNyAxMS4yIDExLjUgMTguMiAxOS44IDEwLjhsOS41LTguNWMxNC44LTEzLjIgNDYuMi0xMy4yIDYxIDBsOS41IDguNWM4LjUgNy40IDIxLjYuMyAxOS44LTEwLjgtMy44LTI1LjItMzQtNDIuMS01OS43LTQyLjF6Ij48L3BhdGg+PC9zdmc+"
const svg2img2 = new SVG2IMG(canvas2.canvas, uriData)
svg2img2.Build("SmileWink.png")
// Test 3: Exists SVG
ImportFontAwesome()
const range = document.createRange()
const fragSmile = range.createContextualFragment(`<i class="far fa-smile" style="background-color:black;color:yellow"></i>`)
document.querySelector(`body`).append(fragSmile)
// use MutationObserver wait the fontawesome convert ``<i class="far fa-smile"></i>`` to SVG. If you write the element in the HTML, then you can skip this hassle way.
const observer = new MutationObserver((mutationRecordList, observer) => {
for (const mutation of mutationRecordList) {
switch (mutation.type) {
case "childList":
const targetSVG = mutation.target.querySelector(`svg`)
if (targetSVG !== null) {
const canvas3 = new Canvas(64, 64) // 👈 Focus here. The part of the observer is not important.
const svg2img3 = new SVG2IMG(canvas3.canvas, SVG2IMG.Convert2URIData(targetSVG))
svg2img3.Build("Smile.png")
targetSVG.remove() // This SVG is created by font-awesome, and it's an extra element. I don't want to see it.
observer.disconnect()
return
}
}
}
})
observer.observe(document.querySelector(`body`), {childList: true})
}
})()
class SVG2IMG {
/**
* #param {HTMLCanvasElement} canvas
* #param {string} src "http://.../xxx.svg" or "data:image/svg+xml;base64,${base64}"
* */
constructor(canvas, src) {
this.canvas = canvas;
this.context = this.canvas.getContext("2d")
this.src = src
this.addTextList = []
}
/**
* #param {HTMLElement} node
* #param {string} mediaType: https://en.wikipedia.org/wiki/Media_type#Common_examples_%5B10%5D
* #see https://en.wikipedia.org/wiki/List_of_URI_schemes
* */
static Convert2URIData(node, mediaType = 'data:image/svg+xml') {
const base64 = btoa(node.outerHTML)
return `${mediaType};base64,${base64}`
}
/**
* #param {string} text
* #param {int} x
* #param {int} y
* #param {"stroke"|"fill"} mode
* #param {string} size, "30px"
* #param {string} font, example: "Arial"
* #param {string} color, example: "#3ae016" or "yellow"
* #param {int} alpha, 0.0 (fully transparent) to 1.0 (fully opaque) // https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Applying_styles_and_colors#transparency
* */
AddText(text, x, y, {mode = "fill", size = "32px", font = "Arial", color = "black", alpha = 1.0}) {
const drawFunc = (text, x, y, mode, font) => {
return () => {
// https://www.w3schools.com/graphics/canvas_text.asp
// https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillText
const context = this.context
const originAlpha = context.globalAlpha
context.globalAlpha = alpha
context.font = `${size} ${font}`
switch (mode) {
case "fill":
context.fillStyle = color
context.fillText(text, x, y)
break
case "stroke":
context.strokeStyle = color
context.strokeText(text, x, y)
break
default:
throw Error(`Unknown mode:${mode}`)
}
context.globalAlpha = originAlpha
}
}
this.addTextList.push(drawFunc(text, x, y, mode, font))
}
/**
* #description When the build is finished, you can click the filename to download the PNG or mouse enters to copy PNG to the clipboard.
* */
Build(filename = "download.png") {
const img = new Image()
img.src = this.src
img.crossOrigin = "anonymous" // Fixes: Tainted canvases may not be exported
img.onload = (event) => {
this.context.drawImage(event.target, 0, 0)
for (const drawTextFunc of this.addTextList) {
drawTextFunc()
}
// create a "a" node for download
const a = document.createElement('a')
document.querySelector('body').append(a)
a.innerText = filename
a.download = filename
const quality = 1.0
// a.target = "_blank"
a.href = this.canvas.toDataURL("image/png", quality)
a.append(this.canvas)
}
this.canvas.onmouseenter = (event) => {
// set background to white. Otherwise, background-color is black.
this.context.globalCompositeOperation = "destination-over" // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation // https://www.w3schools.com/tags/canvas_globalcompositeoperation.asp
this.context.fillStyle = "rgb(255,255,255)"
this.context.fillRect(0, 0, this.canvas.width, this.canvas.height)
this.canvas.toBlob(blob => navigator.clipboard.write([new ClipboardItem({'image/png': blob})])) // copy to clipboard
}
}
}
class Canvas {
/**
* #description for do something like that: ``<canvas width="" height=""></>canvas>``
**/
constructor(w, h) {
const canvas = document.createElement("canvas")
document.querySelector(`body`).append(canvas)
this.canvas = canvas;
[this.canvas.width, this.canvas.height] = [w, h]
}
/**
* #description If your SVG is large, you may want to know which part is what you wanted.
* */
DrawGrid(step = 100) {
const ctx = this.canvas.getContext('2d')
const w = this.canvas.width
const h = this.canvas.height
// Draw the vertical line.
ctx.beginPath();
for (let x = 0; x <= w; x += step) {
ctx.moveTo(x, 0);
ctx.lineTo(x, h);
}
// set the color of the line
ctx.strokeStyle = 'rgba(255,0,0, 0.5)'
ctx.lineWidth = 1
ctx.stroke();
// Draw the horizontal line.
ctx.beginPath();
for (let y = 0; y <= h; y += step) {
ctx.moveTo(0, y)
ctx.lineTo(w, y)
}
ctx.strokeStyle = 'rgba(128, 128, 128, 0.5)'
ctx.lineWidth = 5
ctx.stroke()
}
}
function ImportFontAwesome() {
const range = document.createRange()
const frag = range.createContextualFragment(`
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.2/css/all.min.css" integrity="sha512-HK5fgLBL+xu6dm/Ii3z4xhlSUyZgTT9tuc/hSrtw6uzJOvgRr2a9jyxxT1ely+B+xFAmJKVSTbpM/CuL7qxO8w==" crossorigin="anonymous" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.2/js/all.min.js" integrity="sha512-UwcC/iaz5ziHX7V6LjSKaXgCuRRqbTp1QHpbOJ4l1nw2/boCfZ2KlFIqBUA/uRVF0onbREnY9do8rM/uT/ilqw==" crossorigin="anonymous"/>
`)
document.querySelector("head").append(frag)
}
</script>
if you want to run on stackoverflow and move your mouse on the picture may get error
DOMException: The Clipboard API has been blocked because of a permissions policy applied to the current document
You can copy the code on your local machine and run it again, will be fine.
upload image from <canvas />:
async function canvasToBlob(canvas) {
if (canvas.toBlob) {
return new Promise(function (resolve) {
canvas.toBlob(resolve)
})
} else {
throw new Error('canvas.toBlob Invalid')
}
}
await canvasToBlob(yourCanvasEl)
On some versions of Chrome, you can:
Use the draw image function ctx.drawImage(image1, 0, 0, w, h);Right-click on the canvas
You can use jspdf to capture a canvas into an image or pdf like this:
var imgData = canvas.toDataURL('image/png');
var doc = new jsPDF('p', 'mm');
doc.addImage(imgData, 'PNG', 10, 10);
doc.save('sample-file.pdf');
More info: https://github.com/MrRio/jsPDF
The simple answer is just to take the blob of it and set the img src to a new object URL of that blob, then add that image to a PDF using some library, like
var ok = document.createElement("canvas")
ok.width = 400
ok.height = 140
var ctx = ok.getContext("2d");
for(let k = 0; k < ok.height; k++)
(
k
%
Math.floor(
(
Math.random()
) *
10
)
==
0
) && (y => {
for(var i = 0; i < ok.width; i++) {
if(i % 25 == 0) {
ctx.globalAlpha = Math.random()
ctx.fillStyle = (
"rgb(" +
Math.random() * 255 + "," +
Math.random() * 255 + "," +
Math.random() * 255 + ")"
);
(wdth =>
ctx.fillRect(
Math.sin(
i * Math.PI / 180
) *
Math.random() *
ok.width,
Math.cos(
i * Math.PI / 180,
) * wdth + y,
wdth,
wdth
)
)(15)
}
}
})(k)
ok.toBlob(blob => {
k.src = URL.createObjectURL(blob)
})
<img id=k>
Alternatively, if you wanted to work with low-level byte data, you can get the raw bytes of the canvas, then, depending on the file spec, write the raw image data into the necessary bytes of the data. you just need to call ctx.getImageData(0, 0, ctx.canvas.widht, ctx.canvas.height) to get the raw image data, then based on the file specification, write it to that
if you want to emebed the canvas you can use this snippet
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<canvas id=canvas width=200 height=200></canvas>
<iframe id='img' width=200 height=200></iframe>
<script>
window.onload = function() {
var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");
context.fillStyle = "green";
context.fillRect(50, 50, 100, 100);
document.getElementById('img').src = canvas.toDataURL("image/jpeg");
console.log(canvas.toDataURL("image/jpeg"));
}
</script>
</body>
</html>
Related
TL;DR;
Is there a way to compress an image (mostly jpeg, png and gif) directly browser-side, before uploading it ? I'm pretty sure JavaScript can do this, but I can't find a way to achieve it.
Here's the full scenario I would like to implement:
the user goes to my website, and choose an image via an input type="file" element,
this image is retrieved via JavaScript, we do some verification such as correct file format, maximum file size etc,
if every thing is OK, a preview of the image is displayed on the page,
the user can do some basic operations such as rotate the image by 90°/-90°, crop it following a pre-defined ratio, etc, or the user can upload another image and return to step 1,
when the user is satisfied, the edited image is then compressed and "saved" locally (not saved to a file, but in the browser memory/page),-
the user fill a form with data like name, age etc,
the user click on the "Finish" button, then the form containing datas + compressed image is sent to the server (without AJAX),
The full process up to the last step should be done client side, and should be compatible on latest Chrome and Firefox, Safari 5+ and IE 8+. If possible, only JavaScript should be used (but I'm pretty sure this is not possible).
I've not code anything right now, but I've thought about it already. File reading locally is possible via File API, image previewing and editing could be done using Canvas element, but I can't find a way to do the image compression part.
According to html5please.com and caniuse.com, supporting those browser is quite hard (thanks to IE), but could be done using polyfill such as FlashCanvas and FileReader.
Actually, the goal is to reduce file size, so I see image compression as a solution. But, I know that uploaded images are going to be displayed on my website, every time at the same place, and I know the dimension of this display area (eg. 200x400). So, I could resize the image to fit those dimensions, thus reducing file size. I have no idea what would be the compression ratio for this technique.
What do you think ? Do you have any advice to tell me ? Do you know any way to compress an image browser-side in JavaScript ? Thanks for your replies.
In short:
Read the files using the HTML5 FileReader API with .readAsArrayBuffer
Create a Blob with the file data and get its url with window.URL.createObjectURL(blob)
Create new Image element and set it's src to the file blob url
Send the image to the canvas. The canvas size is set to desired output size
Get the scaled-down data back from canvas via canvas.toDataURL("image/jpeg",0.7) (set your own output format and quality)
Attach new hidden inputs to the original form and transfer the dataURI images basically as normal text
On backend, read the dataURI, decode from Base64, and save it
Source: code.
I see two things missing from the other answers:
canvas.toBlob (when available) is more performant than canvas.toDataURL, and also async.
the file -> image -> canvas -> file conversion loses EXIF data; in particular, data about image rotation commonly set by modern phones/tablets.
The following script deals with both points:
// From https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob, needed for Safari:
if (!HTMLCanvasElement.prototype.toBlob) {
Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {
value: function(callback, type, quality) {
var binStr = atob(this.toDataURL(type, quality).split(',')[1]),
len = binStr.length,
arr = new Uint8Array(len);
for (var i = 0; i < len; i++) {
arr[i] = binStr.charCodeAt(i);
}
callback(new Blob([arr], {type: type || 'image/png'}));
}
});
}
window.URL = window.URL || window.webkitURL;
// Modified from https://stackoverflow.com/a/32490603, cc by-sa 3.0
// -2 = not jpeg, -1 = no data, 1..8 = orientations
function getExifOrientation(file, callback) {
// Suggestion from http://code.flickr.net/2012/06/01/parsing-exif-client-side-using-javascript-2/:
if (file.slice) {
file = file.slice(0, 131072);
} else if (file.webkitSlice) {
file = file.webkitSlice(0, 131072);
}
var reader = new FileReader();
reader.onload = function(e) {
var view = new DataView(e.target.result);
if (view.getUint16(0, false) != 0xFFD8) {
callback(-2);
return;
}
var length = view.byteLength, offset = 2;
while (offset < length) {
var marker = view.getUint16(offset, false);
offset += 2;
if (marker == 0xFFE1) {
if (view.getUint32(offset += 2, false) != 0x45786966) {
callback(-1);
return;
}
var little = view.getUint16(offset += 6, false) == 0x4949;
offset += view.getUint32(offset + 4, little);
var tags = view.getUint16(offset, little);
offset += 2;
for (var i = 0; i < tags; i++)
if (view.getUint16(offset + (i * 12), little) == 0x0112) {
callback(view.getUint16(offset + (i * 12) + 8, little));
return;
}
}
else if ((marker & 0xFF00) != 0xFF00) break;
else offset += view.getUint16(offset, false);
}
callback(-1);
};
reader.readAsArrayBuffer(file);
}
// Derived from https://stackoverflow.com/a/40867559, cc by-sa
function imgToCanvasWithOrientation(img, rawWidth, rawHeight, orientation) {
var canvas = document.createElement('canvas');
if (orientation > 4) {
canvas.width = rawHeight;
canvas.height = rawWidth;
} else {
canvas.width = rawWidth;
canvas.height = rawHeight;
}
if (orientation > 1) {
console.log("EXIF orientation = " + orientation + ", rotating picture");
}
var ctx = canvas.getContext('2d');
switch (orientation) {
case 2: ctx.transform(-1, 0, 0, 1, rawWidth, 0); break;
case 3: ctx.transform(-1, 0, 0, -1, rawWidth, rawHeight); break;
case 4: ctx.transform(1, 0, 0, -1, 0, rawHeight); break;
case 5: ctx.transform(0, 1, 1, 0, 0, 0); break;
case 6: ctx.transform(0, 1, -1, 0, rawHeight, 0); break;
case 7: ctx.transform(0, -1, -1, 0, rawHeight, rawWidth); break;
case 8: ctx.transform(0, -1, 1, 0, 0, rawWidth); break;
}
ctx.drawImage(img, 0, 0, rawWidth, rawHeight);
return canvas;
}
function reduceFileSize(file, acceptFileSize, maxWidth, maxHeight, quality, callback) {
if (file.size <= acceptFileSize) {
callback(file);
return;
}
var img = new Image();
img.onerror = function() {
URL.revokeObjectURL(this.src);
callback(file);
};
img.onload = function() {
URL.revokeObjectURL(this.src);
getExifOrientation(file, function(orientation) {
var w = img.width, h = img.height;
var scale = (orientation > 4 ?
Math.min(maxHeight / w, maxWidth / h, 1) :
Math.min(maxWidth / w, maxHeight / h, 1));
h = Math.round(h * scale);
w = Math.round(w * scale);
var canvas = imgToCanvasWithOrientation(img, w, h, orientation);
canvas.toBlob(function(blob) {
console.log("Resized image to " + w + "x" + h + ", " + (blob.size >> 10) + "kB");
callback(blob);
}, 'image/jpeg', quality);
});
};
img.src = URL.createObjectURL(file);
}
Example usage:
inputfile.onchange = function() {
// If file size > 500kB, resize such that width <= 1000, quality = 0.9
reduceFileSize(this.files[0], 500*1024, 1000, Infinity, 0.9, blob => {
let body = new FormData();
body.set('file', blob, blob.name || "file.jpg");
fetch('/upload-image', {method: 'POST', body}).then(...);
});
};
#PsychoWoods' answer is good. I would like to offer my own solution. This Javascript function takes an image data URL and a width, scales it to the new width, and returns a new data URL.
// Take an image URL, downscale it to the given width, and return a new image URL.
function downscaleImage(dataUrl, newWidth, imageType, imageArguments) {
"use strict";
var image, oldWidth, oldHeight, newHeight, canvas, ctx, newDataUrl;
// Provide default values
imageType = imageType || "image/jpeg";
imageArguments = imageArguments || 0.7;
// Create a temporary image so that we can compute the height of the downscaled image.
image = new Image();
image.src = dataUrl;
oldWidth = image.width;
oldHeight = image.height;
newHeight = Math.floor(oldHeight / oldWidth * newWidth)
// Create a temporary canvas to draw the downscaled image on.
canvas = document.createElement("canvas");
canvas.width = newWidth;
canvas.height = newHeight;
// Draw the downscaled image on the canvas and return the new data URL.
ctx = canvas.getContext("2d");
ctx.drawImage(image, 0, 0, newWidth, newHeight);
newDataUrl = canvas.toDataURL(imageType, imageArguments);
return newDataUrl;
}
This code can be used anywhere you have a data URL and want a data URL for a downscaled image.
You can take a look at image-conversion,Try it here --> demo page
Try this Customizable Pure JS Sample - Compress over 90% :
<div id="root">
<p>Upload an image and see the result</p>
<input id="img-input" type="file" accept="image/*" style="display:block" />
</div>
<script>
const MAX_WIDTH = 320;
const MAX_HEIGHT = 180;
const MIME_TYPE = "image/jpeg";
const QUALITY = 0.7;
const input = document.getElementById("img-input");
input.onchange = function (ev) {
const file = ev.target.files[0]; // get the file
const blobURL = URL.createObjectURL(file);
const img = new Image();
img.src = blobURL;
img.onerror = function () {
URL.revokeObjectURL(this.src);
// Handle the failure properly
console.log("Cannot load image");
};
img.onload = function () {
URL.revokeObjectURL(this.src);
const [newWidth, newHeight] = calculateSize(img, MAX_WIDTH, MAX_HEIGHT);
const canvas = document.createElement("canvas");
canvas.width = newWidth;
canvas.height = newHeight;
const ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0, newWidth, newHeight);
canvas.toBlob(
(blob) => {
// Handle the compressed image. es. upload or save in local state
displayInfo('Original file', file);
displayInfo('Compressed file', blob);
},
MIME_TYPE,
QUALITY
);
document.getElementById("root").append(canvas);
};
};
function calculateSize(img, maxWidth, maxHeight) {
let width = img.width;
let height = img.height;
// calculate the width and height, constraining the proportions
if (width > height) {
if (width > maxWidth) {
height = Math.round((height * maxWidth) / width);
width = maxWidth;
}
} else {
if (height > maxHeight) {
width = Math.round((width * maxHeight) / height);
height = maxHeight;
}
}
return [width, height];
}
// Utility functions for demo purpose
function displayInfo(label, file) {
const p = document.createElement('p');
p.innerText = `${label} - ${readableBytes(file.size)}`;
document.getElementById('root').append(p);
}
function readableBytes(bytes) {
const i = Math.floor(Math.log(bytes) / Math.log(1024)),
sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + sizes[i];
}
</script>
I find that there's simpler solution compared to the accepted answer.
Read the files using the HTML5 FileReader API with .readAsArrayBuffer
Create a Blob with the file data and get its url with window.URL.createObjectURL(blob)
Create new Image element and set it's src to the file blob url
Send the image to the canvas. The canvas size is set to desired output size
Get the scaled-down data back from canvas via canvas.toDataURL("image/jpeg",0.7) (set your own output format and quality)
Attach new hidden inputs to the original form and transfer the dataURI images basically as normal text
On backend, read the dataURI, decode from Base64, and save it
As per your question:
Is there a way to compress an image (mostly jpeg, png and gif)
directly browser-side, before uploading it
My solution:
Create a blob with the file directly with URL.createObjectURL(inputFileElement.files[0]).
Same as accepted answer.
Same as accepted answer. Worth mentioning that, canvas size is necessary and use img.width and img.height to set canvas.width and canvas.height. Not img.clientWidth.
Get the scale-down image by canvas.toBlob(callbackfunction(blob){}, 'image/jpeg', 0.5). Setting 'image/jpg' has no effect. image/png is also supported. Make a new File object inside the callbackfunction body with let compressedImageBlob = new File([blob]).
Add new hidden inputs or send via javascript . Server doesn't have to decode anything.
Check https://javascript.info/binary for all information. I came up the solution after reading this chapter.
Code:
<!DOCTYPE html>
<html>
<body>
<form action="upload.php" method="post" enctype="multipart/form-data">
Select image to upload:
<input type="file" name="fileToUpload" id="fileToUpload" multiple>
<input type="submit" value="Upload Image" name="submit">
</form>
</body>
</html>
This code looks far less scary than the other answers..
Update:
One has to put everything inside img.onload. Otherwise canvas will not be able to get the image's width and height correctly as the time canvas is assigned.
function upload(){
var f = fileToUpload.files[0];
var fileName = f.name.split('.')[0];
var img = new Image();
img.src = URL.createObjectURL(f);
img.onload = function(){
var canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
var ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
canvas.toBlob(function(blob){
console.info(blob.size);
var f2 = new File([blob], fileName + ".jpeg");
var xhr = new XMLHttpRequest();
var form = new FormData();
form.append("fileToUpload", f2);
xhr.open("POST", "upload.php");
xhr.send(form);
}, 'image/jpeg', 0.5);
}
}
3.4MB .png file compression test with image/jpeg argument set.
|0.9| 777KB |
|0.8| 383KB |
|0.7| 301KB |
|0.6| 251KB |
|0.5| 219kB |
I had an issue with the downscaleImage() function posted above by #daniel-allen-langdon in that the image.width and image.height properties are not available immediately because the image load is asynchronous.
Please see updated TypeScript example below that takes this into account, uses async functions, and resizes the image based on the longest dimension rather than just the width
function getImage(dataUrl: string): Promise<HTMLImageElement>
{
return new Promise((resolve, reject) => {
const image = new Image();
image.src = dataUrl;
image.onload = () => {
resolve(image);
};
image.onerror = (el: any, err: ErrorEvent) => {
reject(err.error);
};
});
}
export async function downscaleImage(
dataUrl: string,
imageType: string, // e.g. 'image/jpeg'
resolution: number, // max width/height in pixels
quality: number // e.g. 0.9 = 90% quality
): Promise<string> {
// Create a temporary image so that we can compute the height of the image.
const image = await getImage(dataUrl);
const oldWidth = image.naturalWidth;
const oldHeight = image.naturalHeight;
console.log('dims', oldWidth, oldHeight);
const longestDimension = oldWidth > oldHeight ? 'width' : 'height';
const currentRes = longestDimension == 'width' ? oldWidth : oldHeight;
console.log('longest dim', longestDimension, currentRes);
if (currentRes > resolution) {
console.log('need to resize...');
// Calculate new dimensions
const newSize = longestDimension == 'width'
? Math.floor(oldHeight / oldWidth * resolution)
: Math.floor(oldWidth / oldHeight * resolution);
const newWidth = longestDimension == 'width' ? resolution : newSize;
const newHeight = longestDimension == 'height' ? resolution : newSize;
console.log('new width / height', newWidth, newHeight);
// Create a temporary canvas to draw the downscaled image on.
const canvas = document.createElement('canvas');
canvas.width = newWidth;
canvas.height = newHeight;
// Draw the downscaled image on the canvas and return the new data URL.
const ctx = canvas.getContext('2d')!;
ctx.drawImage(image, 0, 0, newWidth, newHeight);
const newDataUrl = canvas.toDataURL(imageType, quality);
return newDataUrl;
}
else {
return dataUrl;
}
}
For Moderm browser use createImageBitmap() instead of img.onload
async function compressImage(blobImg, percent) {
let bitmap = await createImageBitmap(blobImg);
let canvas = document.createElement("canvas");
let ctx = canvas.getContext("2d");
canvas.width = bitmap.width;
canvas.height = bitmap.height;
ctx.drawImage(bitmap, 0, 0);
let dataUrl = canvas.toDataURL("image/jpeg", percent/100);
return dataUrl;
}
inputImg.addEventListener('change', async(e) => {
let img = e.target.files[0];
console.log('File Name: ', img.name)
console.log('Original Size: ', img.size.toLocaleString())
let imgCompressed = await compressImage(img, 75) // set to 75%
let compSize = atob(imgCompressed.split(",")[1]).length;
console.log('Compressed Size: ', compSize.toLocaleString())
//console.log(imgCompressed)
})
<input type="file" id="inputImg">
Edit: As per the Mr Me comment on this answer, it looks like compression is now available for JPG/WebP formats ( see https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL ).
As far as I know, you cannot compress images using canvas, instead, you can resize it. Using canvas.toDataURL will not let you choose the compression ratio to use. You can take a look at canimage that does exactly what you want : https://github.com/nfroidure/CanImage/blob/master/chrome/canimage/content/canimage.js
In fact, it's often sufficient to just resize the image to decrease it's size but if you want to go further, you'll have to use newly introduced method file.readAsArrayBuffer to get a buffer containing the image data.
Then, just use a DataView to read it's content according to the image format specification (http://en.wikipedia.org/wiki/JPEG or http://en.wikipedia.org/wiki/Portable_Network_Graphics).
It'll be hard to deal with image data compression, but it is worse a try. On the other hand, you can try to delete the PNG headers or the JPEG exif data to make your image smaller, it should be easier to do so.
You'll have to create another DataWiew on another buffer and fill it with the filtered image content. Then, you'll just have to encode you're image content to DataURI using window.btoa.
Let me know if you implement something similar, will be interesting to go through the code.
Compressor.js
https://github.com/fengyuanchen/compressorjs
import axios from 'axios';
import Compressor from 'compressorjs';
document.getElementById('file').addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) {
return;
}
new Compressor(file, {
quality: 0.6,
// The compression process is asynchronous,
// which means you have to access the `result` in the `success` hook function.
success(result) {
const formData = new FormData();
// The third parameter is required for server
formData.append('file', result, result.name);
// Send the compressed image file to server with XMLHttpRequest.
axios.post('/path/to/upload', formData).then(() => {
console.log('Upload success');
});
},
error(err) {
console.log(err.message);
},
});
});
I used the following package:
https://www.npmjs.com/package/browser-image-compression
npm install browser-image-compression
or
yarn add browser-image-compression
Then just following the docs:
import imageCompression from 'browser-image-compression';
const options = {
maxSizeMB: 0.5, // pretty much self-explanatory
maxWidthOrHeight: 500, // apparently px
}
imageCompression(file, options)
.then(function(compressedFile) {
console.log(
"compressedFile instanceof Blob",
compressedFile instanceof Blob
); // true
console.log(
`compressedFile size ${compressedFile.size /
1024 /
1024} MB`
); // smaller than maxSizeMB
return uploader(compressedFile); // code to actual upload, in my case uploader() is a function to upload to Firebase storage.
})
Just in case if you were curios about the uploader(), here's the code of it:
import { initializeApp } from "firebase/app";
const firebaseConfig = {
// your config
};
initializeApp(firebaseConfig);
import { getStorage, ref, uploadBytes, getDownloadURL } from "firebase/storage";
const storage = getStorage();
const sRef = ref(storage);
const uploader = async (file) => {
/* uploads to root */
// const imageRef = ref(sRef, file.name);
// console.log(imageRef);
// await uploadBytes(imageRef, file).then((snapshot) => {
// console.log("Uploaded a blob or file!", snapshot);
// });
/* upload to folder 'techs/' */
const folderRef = ref(sRef, "techs/" + file.name);
await uploadBytes(folderRef, file);
// get URL
const url = await getDownloadURL(ref(storage, folderRef));
console.log("url: ", url);
return url;
};
You can compress an image using the HTML <canvas> element:
function compressImage(imgToCompress, resizingFactor, quality) {
// resizing the image
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
const originalWidth = imgToCompress.width;
const originalHeight = imgToCompress.height;
const canvasWidth = originalWidth * resizingFactor;
const canvasHeight = originalHeight * resizingFactor;
canvas.width = canvasWidth;
canvas.height = canvasHeight;
context.drawImage(
imgToCompress,
0,
0,
originalWidth * resizingFactor,
originalHeight * resizingFactor
);
// reducing the quality of the image
canvas.toBlob(
(blob) => {
if (blob) {
// showing the compressed image
resizedImage.src = URL.createObjectURL(resizedImageBlob);
}
},
"image/jpeg",
quality
);
}
See this blog post for an in depth explanation: https://img.ly/blog/how-to-compress-an-image-before-uploading-it-in-javascript/
i improved the function a head to be this :
var minifyImg = function(dataUrl,newWidth,imageType="image/jpeg",resolve,imageArguments=0.7){
var image, oldWidth, oldHeight, newHeight, canvas, ctx, newDataUrl;
(new Promise(function(resolve){
image = new Image(); image.src = dataUrl;
log(image);
resolve('Done : ');
})).then((d)=>{
oldWidth = image.width; oldHeight = image.height;
log([oldWidth,oldHeight]);
newHeight = Math.floor(oldHeight / oldWidth * newWidth);
log(d+' '+newHeight);
canvas = document.createElement("canvas");
canvas.width = newWidth; canvas.height = newHeight;
log(canvas);
ctx = canvas.getContext("2d");
ctx.drawImage(image, 0, 0, newWidth, newHeight);
//log(ctx);
newDataUrl = canvas.toDataURL(imageType, imageArguments);
resolve(newDataUrl);
});
};
the use of it :
minifyImg(<--DATAURL_HERE-->,<--new width-->,<--type like image/jpeg-->,(data)=>{
console.log(data); // the new DATAURL
});
enjoy ;)
For JPG Image compression you can use the best compression technique called JIC
(Javascript Image Compression)This will definitely help you -->https://github.com/brunobar79/J-I-C
I need some help with finding a memory leak in a small, Browser / WebWorker JavaScript. I tracked it down into this little piece of code:
/**
* Resizes an Image
*
* #function scaleImage
* #param {object} oImageBlob blob of image
* #param {int} iHeight New height of Image
* #return {ImageBitmap} ImageBitmap Object
*/
async function scaleImage(oImageBlob, iHeight) {
var img = await self.createImageBitmap(oImageBlob);
var iWidth = Math.round( ( img.width / img.height ) * iHeight);
var canvas = new OffscreenCanvas(iWidth,iHeight);
var ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
return(canvas.transferToImageBitmap());
}
It's called from:
[inside a web worker: Some looping that calls this about 1200 times while parsind files from a s3 bucket ...]
var oImageBlob = await new Response(oS3Object.Body, {}).blob();
var oThumbnail = await scaleImage(oImageBlob, 100);
await IDBputData(oInput.sFileKey, oImageBlob, oInput.sStore, oThumbnail)
[... end of the loop]
The other interior function is
/**
* Put information to IndexedDB
*
* #function IDBputData
* #param {string} sKey key of information
* #param {string} sValue information to upload
* #param {string} sStore name of object store
* #param {object} oThumbnail optrional, default: null, thumbnail image
* #return {object} - SUCCESS: array, IndexedDB Identifyer "key"
* - FAIL: Error Message
*/
async function IDBputData(sKey, sValue, sStore, oThumbnail=null) {
var oGeneratedKeys = {};
if(sStore=="panelStore"){
oGeneratedKeys = await getKeyfromSKey(sKey);
}
return new Promise((resolve, reject) => {
const tx = oConn.transaction(sStore, 'readwrite');
const store = tx.objectStore(sStore);
var request = {}
request = store.put({panelkey: oGeneratedKeys.panelkey, layerkey: oGeneratedKeys.layerkey, countrycode: oGeneratedKeys.countrycode, value: sValue, LastModified: new Date(), oThumbnail: oThumbnail});
request.onsuccess = () => (oThumbnail.close(),resolve(request.result));
request.onerror = () => (oThumbnail.close(),reject(request.error));
});
}
When I run it this way in Chrome, it will consume every bit of RAM I've got free (around 8 GB) and then crash. (Laptop with shared RAM for CPU/GPU).
When I change
var oThumbnail = await scaleImage(oImageBlob, 100);
to
var oThumbnail = null;
RAM consumption of Chrome stays rather fixed around 800 MB, so there must be something with the topmost function "scaleImage".
I tried tweaking it, but with no success.
/**
* Resizes an Image
*
* #function scaleImage
* #param {object} oImageBlob blob of image
* #param {int} iHeight New height of Image
* #return {ImageBitmap} ImageBitmap Object
*/
async function scaleImage(oImageBlob, iHeight) {
var img = await self.createImageBitmap(oImageBlob);
var iWidth = Math.round( ( img.width / img.height ) * iHeight);
var canvas = new OffscreenCanvas(iWidth,iHeight);
var ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
var oImageBitmap = canvas.transferToImageBitmap();
ctx = null;
canvas = null;
iWidth = null;
img = null;
return(oImageBitmap);
}
Any help is very much appreciated.
For the ImageBitmap to release its bitmap data the most efficient way, you have to call its .close() method once you're done with it.
But actually, you don't need this scaleImage function. createImageBitmap() has a resizeHeight option, and if you use it without the resizeWidth one, you'll resize your image by keeping the aspect-ratio exacty like you are doing in your function, except that it won't need to assign the bitmap twice.
Once you have this resized ImageBitmap, you can transfer it to a BitmapRenderingContext (which will internally close() the original ImageBitmap) and call the transferToBlob() from that renderer.
This should be lighter for your computer.
async function worker_script() {
const blob = await fetch( "https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png" ).then( (resp) => resp.blob() );
// resize from createImageBitmap directly
const img = await createImageBitmap( blob , { resizeHeight: 100 } );
const canvas = new OffscreenCanvas( img.width, img.height );
canvas.getContext( "bitmaprenderer" )
.transferFromImageBitmap( img ); // this internally closes the ImageBitmap
const resized_blob = await canvas.convertToBlob();
// putInIDB( resized_blob );
// for the demo we pass that Blob to main, but one shouldn't have to do that
// show the canvas instead ;)
postMessage( { blob: resized_blob, width: img.width } );
// to be safe, we can even resize our canvas to 0x0 to free its bitmap entirely
canvas.width = canvas.height = 0;
}
// back to main
const worker = new Worker( getWorkerURL() );
worker.onmessage = ({ data: { blob, width } }) => {
const img = new Image();
img.src = URL.createObjectURL( blob );
img.onload = (evt) => URL.revokeObjectURL( img.src );
document.body.append( img );
console.log( "ImageBitmap got detached?", width === 0 );
};
function getWorkerURL() {
const content = "!" + worker_script.toString() + "();";
const blob = new Blob( [ content ], { type: "text/javascript" } );
return URL.createObjectURL( blob );
}
Problem:
I am learning p5.js and I am following a tutorial from Coding Train YouTube channel. Everything was fine until I had to call a library function on an Image object. The problem is that I have instantiated the library in an object p and I'm using it's variables through p object. I don't know why it isn't recognizing the loadPixels() function. In the tutorial, the function works fine.
Error Message:
🌸 p5.js says: There's an error as "loadPixels" could not be called as a function
(on line 17 in help.html [file:///G:/Android/help.html:17:11]).
Verify whether "img" has "loadPixels" in it and check the spelling, letter-casing (Javacript is case-sensitive) and its type.
For more: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/Not_a_function#What_went_wrong
Code:
<!DOCTYPE html>
<head>
<script src='p5/p5.js'></script>
</head>
<body>
<div id='container'></div>
<script>
let sketch = function(p) {
p.setup = function(){
p.createCanvas(56, 56);
img = new Image();
img.src = "scott.jpg";
}
p.draw = function() {
// p.drawingContext.drawImage(img, 0, 0);
p.loadPixels();
img.loadPixels();
for (var x=0; x<p.width; x++) {
for (var y=0; y<p.height; y++) {
// var d = p.dist(x, y, p.width/2, p.height/2);
var loc = x+y+p.width;
// p.pixels[loc] = p.color(d);
p.pixels[loc] = img.pixels[loc];
}
}
}
p.updatePixels();
};
new p5(sketch, 'container');
</script>
</body>
</html>
Edit:
As someone pointed out that the problem is that I'm using Image() which is the default Image class for javascript. I did some changes to my code but now it gives me this error.
Error :-
Uncaught DOMException: The operation is insecure. help.html:18
openWindow file:///G:/Android/help.html:18
onclick file:///G:/Android/help.html:1
Code :-
<!DOCTYPE html>
<head>
<script src='p5/p5.js'></script>
</head>
<body>
<button onclick="openWindow()">click me to open a new window.</button>
<div id='container'></div>
<script>
function openWindow() {
var newWindow = window.open("", "Import Image", "height=56,width=56,status=yes,toolbar=no,menubar=no,location=no");
newWindow.document.write("<canvas id='imagePlaceholder'>Canvas not supported!</canvas>");
var canvas = newWindow.document.getElementById("imagePlaceholder");
var ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0);
// console.log(ctx.getImageData(0, 0, 56, 56).data);
dest = ctx.getImageData(0, 0, 56, 56).data;
}
let sketch = function(p) {
p.setup = function(){
p.createCanvas(56, 56);
img = new Image();
img.src = "scott.jpg";
let dest = p.createImage(56, 56);
console.log(img);
}
p.draw = function() {
// p.drawingContext.drawImage(img, 0, 0);
// p.loadPixels();
img.loadPixels();
for (var x=0; x<p.width; x++) {
for (var y=0; y<p.height; y++) {
// var d = p.dist(x, y, p.width/2, p.height/2);
var loc = x+y+p.width;
// p.pixels[loc] = p.color(d);
p.pixels[loc] = img.pixels[loc];
}
}
}
p.updatePixels();
};
new p5(sketch, 'container');
</script>
</body>
</html>
Because you are specifically trying to load an image from a local computer as opposed to a publicly accessible URL, a file input with user interaction is going to be the only way to do this. This is a deliberate constraint put in place by web browsers to prevent a malicious webpage from illicitly reading data from your local files. However there is a much simpler way to get the image data from the file input onto your p5js canvas. In fact this exact use case can be seen in the documentation for the createFileInput function.
let input;
let img;
function setup() {
input = createFileInput(handleFile);
input.position(0, 0);
}
function draw() {
background(255);
if (img) {
image(img, 0, 0, width, height);
}
}
function handleFile(file) {
if (file.type === 'image') {
img = createImg(file.data, '');
img.hide();
} else {
img = null;
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/p5.min.js"></script>
</head>
<body>
</body>
</html>
One obvious problem here is that you are using the builtin Image() constructor, which creates an HTMLImageTag (see the MDN Article) instead of creating a p5js p5.Image object (see the p5js Reference). However there are several other issues. In p5js you need to load images in the preload function to ensure they are available when you start drawing (this is an asynchronous operation). You'd have a much easier time drawing images in p5js using the built in image function. If you are going to use pixel arrays, you need to understand the structure of these arrays. They don't store Color objects, they store 4 separate numbers for each color channel (red, green, blue, and alpha). So the indices in the array are not (x + y * width), but ((x + y * width) * 4 + channel) where channel is a number from 0 to 3. Also you need to account for the fact that the canvas may have a pixel density > 1, whereas the image will have a pixel density of 1. I strongly suggest you read all of the documentation for the Image related p5js functions.
let sketch = function(p) {
let img;
p.preload = function() {
img = p.loadImage("https://s3-ap-southeast-1.amazonaws.com/investingnote-production-webbucket/attachments/41645da792aef1c5054c33de240a52e2c32d205e.png");
};
p.setup = function() {
p.createCanvas(200, 200);
};
p.draw = function() {
// this would be a lot simpler way to draw the image:
// p.image(img, 0, 0);
p.loadPixels();
img.loadPixels();
// Handle high pixel density displays. This code effectively scale the image up so that each 1 pixel in the source image is density * density pixels in the display, thus preserving the size of the image but leading to visible pixelation.
let density = p.pixelDensity();
for (var x = 0; x < p.width && x < img.width; x++) {
for (var y = 0; y < p.height && y < img.height; y++) {
// There are 4 values per pixel in the pixels array:
var srcLoc = (x + y * img.width) * 4;
for (var xd = 0; xd < density; xd++) {
for (var yd = 0; yd < density; yd++) {
var destLoc =
(x * density + xd + (y * density + yd) * p.width * density) * 4;
for (var i = 0; i < 4; i++) {
p.pixels[destLoc + i] = img.pixels[srcLoc + i];
}
}
}
}
}
p.updatePixels();
};
};
new p5(sketch, 'container');
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/p5.min.js"></script>
</head>
<body>
<div id='container'></div>
</body>
</html>
Here is an alternate version of the snippet that handles pixelDensity differently:
let sketch = function(p) {
let img;
p.preload = function() {
img = p.loadImage("https://s3-ap-southeast-1.amazonaws.com/investingnote-production-webbucket/attachments/41645da792aef1c5054c33de240a52e2c32d205e.png");
};
p.setup = function() {
p.createCanvas(200, 200);
};
p.draw = function() {
// this would be a lot simpler way to draw the image:
// p.image(img, 0, 0);
p.loadPixels();
img.loadPixels();
// Handle high pixel density displays. This code shrinks the image down by mapping one pixel in the source image to 1 / (density ^ 2) actual pixels in the canvas.
let density = p.pixelDensity();
for (var x = 0; x < p.width * density && x < img.width; x++) {
for (var y = 0; y < p.height * density && y < img.height; y++) {
// There are 4 values per pixel in the pixels array:
var srcLoc = (x + y * img.width) * 4;
var destLoc = (x + y * p.width * density) * 4;
for (var i = 0; i < 4; i++) {
p.pixels[destLoc + i] = img.pixels[srcLoc + i];
}
}
}
p.updatePixels();
};
};
new p5(sketch, 'container');
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/p5.min.js"></script>
</head>
<body>
<div id='container'></div>
</body>
</html>
I tried a lot of things and almost giveup, but at the end I had to change the code a bit and this worked for me. Although what I got was base64 url as Alice in the comment suggested and I converted it into Uint8ClampedArray. Now if anyone wants a full image or all the pixels of an image then they can follow this link :- https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Pixel_manipulation_with_canvas . I hope it will be helpful for anyone who wants to make an offline webcanvas based application and don't want to dabble with CORS.
var fileSelector = document.createElement('input');
fileSelector.setAttribute('type', 'file');
fileSelector.setAttribute('accept', 'image/gif, image/jpeg, image/png');
fileSelector.click();
fileSelector.onchange = function(e) {
img = new Image();
var file = e.target.files[0];
var reader = new FileReader();
reader.onloadend = function() {
img.src = reader.result;
}
reader.readAsDataURL(file);
var newWindow = window.open("", "_blank", "height=56,width=56,status=yes,toolbar=no,menubar=no,location=no");
newWindow.document.write("<canvas id='imagePlaceholder'>Canvas not supported!</canvas>");
var canvas = newWindow.document.getElementById("imagePlaceholder");
var ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0);
// console.log(ctx.getImageData(0, 0, 56, 56).data);
var dest = ctx.getImageData(0, 0, img.width, img.height).data;
console.log(dest);
newWindow.close();
}
I am trying to produce a wave form (https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Visualizations_with_Web_Audio_API) with howler.js . I see the dataArray looping through the draw function. However it only draws a straight line because the v variable always returns 1. I based the code off a pretty common MDN example, this leads me to believe maybe the way I am getting the howler data is incorrect.
HTML
<div id="play">play</div>
<canvas id="canvas"></canvas>
JS
let playing = false
const playBtn = document.getElementById('play')
const canvas = document.getElementById('canvas')
const canvasCtx = canvas.getContext('2d')
const WIDTH = canvas.width
const HEIGHT = canvas.height
let drawVisual = null
/*
files
https://s3-us-west-2.amazonaws.com/s.cdpn.io/481938/Find_My_Way_Home.mp3
*/
/*
streams
'http://rfcmedia.streamguys1.com/MusicPulse.mp3'
*/
let analyser = null
let bufferLength = null
let dataArray = null
const howler = new Howl({
html5: true,
format: ['mp3', 'aac'],
src:
'https://s3-us-west-2.amazonaws.com/s.cdpn.io/481938/Find_My_Way_Home.mp3',
onplay: () => {
analyser = Howler.ctx.createAnalyser()
Howler.masterGain.connect(analyser)
analyser.connect(Howler.ctx.destination)
analyser.fftSize = 2048
analyser.minDecibels = -90
analyser.maxDecibels = -10
analyser.smoothingTimeConstant = 0.85
bufferLength = analyser.frequencyBinCount
dataArray = new Uint8Array(bufferLength)
canvasCtx.clearRect(0, 0, WIDTH, HEIGHT)
const draw = () => {
drawVisual = requestAnimationFrame(draw)
analyser.getByteTimeDomainData(dataArray)
canvasCtx.fillStyle = '#000'
canvasCtx.fillRect(0, 0, WIDTH, HEIGHT)
canvasCtx.lineWidth = 2
canvasCtx.strokeStyle = 'limegreen'
canvasCtx.beginPath()
let sliceWidth = (WIDTH * 1.0) / bufferLength
let x = 0
for (let i = 0; i < bufferLength; i++) {
let v = dataArray[i] / 128.0
let y = (v * HEIGHT) / 2
if (i === 0) {
canvasCtx.moveTo(x, y)
} else {
canvasCtx.lineTo(x, y)
}
x += sliceWidth
}
canvasCtx.lineTo(canvas.width, canvas.height / 2)
canvasCtx.stroke()
}
draw()
}
})
playBtn.addEventListener('click', () => {
if (!playing) {
howler.play()
playing = true
}
})
To get it working:
Remove html5: true
There is a CORS setup isssue with your audio source. What are your bucket CORS settings? Access to XMLHttpRequest at 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/481938/Find_My_Way_Home.mp3' from origin 'null' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
The CORS issue leads to your dataArray being full of 128 basically meaning no sound even though the music is playing.
With that I got your visualizer to work. (You can bypass CORS on chrome "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" --disable-web-security)
Here is the code for the waveform:
const data = audioBuffer.getChannelData(0)
context.beginPath()
const last = data.length - 1
for (let i = 0; i <= last; i++) {
context.lineTo(i / last * width, height / 2 - height * data[i])
}
context.strokeStyle = 'white'
context.stroke()
How to get this audioBuffer from howler? I'm not suggesting to try it because howler may not use web audio api. And no way in doc, only digging source code. Instead, here is the code to load this buffer directly:
const url = 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/481938/Find_My_Way_Home.mp3'
const getAudioBuffer = async (url) => {
const context = new AudioContext
result = await new Promise(resolve => {
request = new XMLHttpRequest
request.open "GET", url, true
request.responseType = 'arraybuffer'
request.onload = () => resolve(request.response)
request.send()
}
return await context.decodeAudioData(result)
}
audioBuffer = getAudioBuffer(url)
audioBuffer.getChannelData(0) // it can have multiple channels, each channel is Float32Array
But! This is waveform without animation, track is downloaded and waveform draw.
In your example you are trying to make something animated, using that code above it's possible to make something like window moving from start to end according to playback position.
So my answer is not answer, no animation, no howler, but hope it helps :)
TL;DR;
Is there a way to compress an image (mostly jpeg, png and gif) directly browser-side, before uploading it ? I'm pretty sure JavaScript can do this, but I can't find a way to achieve it.
Here's the full scenario I would like to implement:
the user goes to my website, and choose an image via an input type="file" element,
this image is retrieved via JavaScript, we do some verification such as correct file format, maximum file size etc,
if every thing is OK, a preview of the image is displayed on the page,
the user can do some basic operations such as rotate the image by 90°/-90°, crop it following a pre-defined ratio, etc, or the user can upload another image and return to step 1,
when the user is satisfied, the edited image is then compressed and "saved" locally (not saved to a file, but in the browser memory/page),-
the user fill a form with data like name, age etc,
the user click on the "Finish" button, then the form containing datas + compressed image is sent to the server (without AJAX),
The full process up to the last step should be done client side, and should be compatible on latest Chrome and Firefox, Safari 5+ and IE 8+. If possible, only JavaScript should be used (but I'm pretty sure this is not possible).
I've not code anything right now, but I've thought about it already. File reading locally is possible via File API, image previewing and editing could be done using Canvas element, but I can't find a way to do the image compression part.
According to html5please.com and caniuse.com, supporting those browser is quite hard (thanks to IE), but could be done using polyfill such as FlashCanvas and FileReader.
Actually, the goal is to reduce file size, so I see image compression as a solution. But, I know that uploaded images are going to be displayed on my website, every time at the same place, and I know the dimension of this display area (eg. 200x400). So, I could resize the image to fit those dimensions, thus reducing file size. I have no idea what would be the compression ratio for this technique.
What do you think ? Do you have any advice to tell me ? Do you know any way to compress an image browser-side in JavaScript ? Thanks for your replies.
In short:
Read the files using the HTML5 FileReader API with .readAsArrayBuffer
Create a Blob with the file data and get its url with window.URL.createObjectURL(blob)
Create new Image element and set it's src to the file blob url
Send the image to the canvas. The canvas size is set to desired output size
Get the scaled-down data back from canvas via canvas.toDataURL("image/jpeg",0.7) (set your own output format and quality)
Attach new hidden inputs to the original form and transfer the dataURI images basically as normal text
On backend, read the dataURI, decode from Base64, and save it
Source: code.
I see two things missing from the other answers:
canvas.toBlob (when available) is more performant than canvas.toDataURL, and also async.
the file -> image -> canvas -> file conversion loses EXIF data; in particular, data about image rotation commonly set by modern phones/tablets.
The following script deals with both points:
// From https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob, needed for Safari:
if (!HTMLCanvasElement.prototype.toBlob) {
Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {
value: function(callback, type, quality) {
var binStr = atob(this.toDataURL(type, quality).split(',')[1]),
len = binStr.length,
arr = new Uint8Array(len);
for (var i = 0; i < len; i++) {
arr[i] = binStr.charCodeAt(i);
}
callback(new Blob([arr], {type: type || 'image/png'}));
}
});
}
window.URL = window.URL || window.webkitURL;
// Modified from https://stackoverflow.com/a/32490603, cc by-sa 3.0
// -2 = not jpeg, -1 = no data, 1..8 = orientations
function getExifOrientation(file, callback) {
// Suggestion from http://code.flickr.net/2012/06/01/parsing-exif-client-side-using-javascript-2/:
if (file.slice) {
file = file.slice(0, 131072);
} else if (file.webkitSlice) {
file = file.webkitSlice(0, 131072);
}
var reader = new FileReader();
reader.onload = function(e) {
var view = new DataView(e.target.result);
if (view.getUint16(0, false) != 0xFFD8) {
callback(-2);
return;
}
var length = view.byteLength, offset = 2;
while (offset < length) {
var marker = view.getUint16(offset, false);
offset += 2;
if (marker == 0xFFE1) {
if (view.getUint32(offset += 2, false) != 0x45786966) {
callback(-1);
return;
}
var little = view.getUint16(offset += 6, false) == 0x4949;
offset += view.getUint32(offset + 4, little);
var tags = view.getUint16(offset, little);
offset += 2;
for (var i = 0; i < tags; i++)
if (view.getUint16(offset + (i * 12), little) == 0x0112) {
callback(view.getUint16(offset + (i * 12) + 8, little));
return;
}
}
else if ((marker & 0xFF00) != 0xFF00) break;
else offset += view.getUint16(offset, false);
}
callback(-1);
};
reader.readAsArrayBuffer(file);
}
// Derived from https://stackoverflow.com/a/40867559, cc by-sa
function imgToCanvasWithOrientation(img, rawWidth, rawHeight, orientation) {
var canvas = document.createElement('canvas');
if (orientation > 4) {
canvas.width = rawHeight;
canvas.height = rawWidth;
} else {
canvas.width = rawWidth;
canvas.height = rawHeight;
}
if (orientation > 1) {
console.log("EXIF orientation = " + orientation + ", rotating picture");
}
var ctx = canvas.getContext('2d');
switch (orientation) {
case 2: ctx.transform(-1, 0, 0, 1, rawWidth, 0); break;
case 3: ctx.transform(-1, 0, 0, -1, rawWidth, rawHeight); break;
case 4: ctx.transform(1, 0, 0, -1, 0, rawHeight); break;
case 5: ctx.transform(0, 1, 1, 0, 0, 0); break;
case 6: ctx.transform(0, 1, -1, 0, rawHeight, 0); break;
case 7: ctx.transform(0, -1, -1, 0, rawHeight, rawWidth); break;
case 8: ctx.transform(0, -1, 1, 0, 0, rawWidth); break;
}
ctx.drawImage(img, 0, 0, rawWidth, rawHeight);
return canvas;
}
function reduceFileSize(file, acceptFileSize, maxWidth, maxHeight, quality, callback) {
if (file.size <= acceptFileSize) {
callback(file);
return;
}
var img = new Image();
img.onerror = function() {
URL.revokeObjectURL(this.src);
callback(file);
};
img.onload = function() {
URL.revokeObjectURL(this.src);
getExifOrientation(file, function(orientation) {
var w = img.width, h = img.height;
var scale = (orientation > 4 ?
Math.min(maxHeight / w, maxWidth / h, 1) :
Math.min(maxWidth / w, maxHeight / h, 1));
h = Math.round(h * scale);
w = Math.round(w * scale);
var canvas = imgToCanvasWithOrientation(img, w, h, orientation);
canvas.toBlob(function(blob) {
console.log("Resized image to " + w + "x" + h + ", " + (blob.size >> 10) + "kB");
callback(blob);
}, 'image/jpeg', quality);
});
};
img.src = URL.createObjectURL(file);
}
Example usage:
inputfile.onchange = function() {
// If file size > 500kB, resize such that width <= 1000, quality = 0.9
reduceFileSize(this.files[0], 500*1024, 1000, Infinity, 0.9, blob => {
let body = new FormData();
body.set('file', blob, blob.name || "file.jpg");
fetch('/upload-image', {method: 'POST', body}).then(...);
});
};
#PsychoWoods' answer is good. I would like to offer my own solution. This Javascript function takes an image data URL and a width, scales it to the new width, and returns a new data URL.
// Take an image URL, downscale it to the given width, and return a new image URL.
function downscaleImage(dataUrl, newWidth, imageType, imageArguments) {
"use strict";
var image, oldWidth, oldHeight, newHeight, canvas, ctx, newDataUrl;
// Provide default values
imageType = imageType || "image/jpeg";
imageArguments = imageArguments || 0.7;
// Create a temporary image so that we can compute the height of the downscaled image.
image = new Image();
image.src = dataUrl;
oldWidth = image.width;
oldHeight = image.height;
newHeight = Math.floor(oldHeight / oldWidth * newWidth)
// Create a temporary canvas to draw the downscaled image on.
canvas = document.createElement("canvas");
canvas.width = newWidth;
canvas.height = newHeight;
// Draw the downscaled image on the canvas and return the new data URL.
ctx = canvas.getContext("2d");
ctx.drawImage(image, 0, 0, newWidth, newHeight);
newDataUrl = canvas.toDataURL(imageType, imageArguments);
return newDataUrl;
}
This code can be used anywhere you have a data URL and want a data URL for a downscaled image.
You can take a look at image-conversion,Try it here --> demo page
Try this Customizable Pure JS Sample - Compress over 90% :
<div id="root">
<p>Upload an image and see the result</p>
<input id="img-input" type="file" accept="image/*" style="display:block" />
</div>
<script>
const MAX_WIDTH = 320;
const MAX_HEIGHT = 180;
const MIME_TYPE = "image/jpeg";
const QUALITY = 0.7;
const input = document.getElementById("img-input");
input.onchange = function (ev) {
const file = ev.target.files[0]; // get the file
const blobURL = URL.createObjectURL(file);
const img = new Image();
img.src = blobURL;
img.onerror = function () {
URL.revokeObjectURL(this.src);
// Handle the failure properly
console.log("Cannot load image");
};
img.onload = function () {
URL.revokeObjectURL(this.src);
const [newWidth, newHeight] = calculateSize(img, MAX_WIDTH, MAX_HEIGHT);
const canvas = document.createElement("canvas");
canvas.width = newWidth;
canvas.height = newHeight;
const ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0, newWidth, newHeight);
canvas.toBlob(
(blob) => {
// Handle the compressed image. es. upload or save in local state
displayInfo('Original file', file);
displayInfo('Compressed file', blob);
},
MIME_TYPE,
QUALITY
);
document.getElementById("root").append(canvas);
};
};
function calculateSize(img, maxWidth, maxHeight) {
let width = img.width;
let height = img.height;
// calculate the width and height, constraining the proportions
if (width > height) {
if (width > maxWidth) {
height = Math.round((height * maxWidth) / width);
width = maxWidth;
}
} else {
if (height > maxHeight) {
width = Math.round((width * maxHeight) / height);
height = maxHeight;
}
}
return [width, height];
}
// Utility functions for demo purpose
function displayInfo(label, file) {
const p = document.createElement('p');
p.innerText = `${label} - ${readableBytes(file.size)}`;
document.getElementById('root').append(p);
}
function readableBytes(bytes) {
const i = Math.floor(Math.log(bytes) / Math.log(1024)),
sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + sizes[i];
}
</script>
I find that there's simpler solution compared to the accepted answer.
Read the files using the HTML5 FileReader API with .readAsArrayBuffer
Create a Blob with the file data and get its url with window.URL.createObjectURL(blob)
Create new Image element and set it's src to the file blob url
Send the image to the canvas. The canvas size is set to desired output size
Get the scaled-down data back from canvas via canvas.toDataURL("image/jpeg",0.7) (set your own output format and quality)
Attach new hidden inputs to the original form and transfer the dataURI images basically as normal text
On backend, read the dataURI, decode from Base64, and save it
As per your question:
Is there a way to compress an image (mostly jpeg, png and gif)
directly browser-side, before uploading it
My solution:
Create a blob with the file directly with URL.createObjectURL(inputFileElement.files[0]).
Same as accepted answer.
Same as accepted answer. Worth mentioning that, canvas size is necessary and use img.width and img.height to set canvas.width and canvas.height. Not img.clientWidth.
Get the scale-down image by canvas.toBlob(callbackfunction(blob){}, 'image/jpeg', 0.5). Setting 'image/jpg' has no effect. image/png is also supported. Make a new File object inside the callbackfunction body with let compressedImageBlob = new File([blob]).
Add new hidden inputs or send via javascript . Server doesn't have to decode anything.
Check https://javascript.info/binary for all information. I came up the solution after reading this chapter.
Code:
<!DOCTYPE html>
<html>
<body>
<form action="upload.php" method="post" enctype="multipart/form-data">
Select image to upload:
<input type="file" name="fileToUpload" id="fileToUpload" multiple>
<input type="submit" value="Upload Image" name="submit">
</form>
</body>
</html>
This code looks far less scary than the other answers..
Update:
One has to put everything inside img.onload. Otherwise canvas will not be able to get the image's width and height correctly as the time canvas is assigned.
function upload(){
var f = fileToUpload.files[0];
var fileName = f.name.split('.')[0];
var img = new Image();
img.src = URL.createObjectURL(f);
img.onload = function(){
var canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
var ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
canvas.toBlob(function(blob){
console.info(blob.size);
var f2 = new File([blob], fileName + ".jpeg");
var xhr = new XMLHttpRequest();
var form = new FormData();
form.append("fileToUpload", f2);
xhr.open("POST", "upload.php");
xhr.send(form);
}, 'image/jpeg', 0.5);
}
}
3.4MB .png file compression test with image/jpeg argument set.
|0.9| 777KB |
|0.8| 383KB |
|0.7| 301KB |
|0.6| 251KB |
|0.5| 219kB |
I had an issue with the downscaleImage() function posted above by #daniel-allen-langdon in that the image.width and image.height properties are not available immediately because the image load is asynchronous.
Please see updated TypeScript example below that takes this into account, uses async functions, and resizes the image based on the longest dimension rather than just the width
function getImage(dataUrl: string): Promise<HTMLImageElement>
{
return new Promise((resolve, reject) => {
const image = new Image();
image.src = dataUrl;
image.onload = () => {
resolve(image);
};
image.onerror = (el: any, err: ErrorEvent) => {
reject(err.error);
};
});
}
export async function downscaleImage(
dataUrl: string,
imageType: string, // e.g. 'image/jpeg'
resolution: number, // max width/height in pixels
quality: number // e.g. 0.9 = 90% quality
): Promise<string> {
// Create a temporary image so that we can compute the height of the image.
const image = await getImage(dataUrl);
const oldWidth = image.naturalWidth;
const oldHeight = image.naturalHeight;
console.log('dims', oldWidth, oldHeight);
const longestDimension = oldWidth > oldHeight ? 'width' : 'height';
const currentRes = longestDimension == 'width' ? oldWidth : oldHeight;
console.log('longest dim', longestDimension, currentRes);
if (currentRes > resolution) {
console.log('need to resize...');
// Calculate new dimensions
const newSize = longestDimension == 'width'
? Math.floor(oldHeight / oldWidth * resolution)
: Math.floor(oldWidth / oldHeight * resolution);
const newWidth = longestDimension == 'width' ? resolution : newSize;
const newHeight = longestDimension == 'height' ? resolution : newSize;
console.log('new width / height', newWidth, newHeight);
// Create a temporary canvas to draw the downscaled image on.
const canvas = document.createElement('canvas');
canvas.width = newWidth;
canvas.height = newHeight;
// Draw the downscaled image on the canvas and return the new data URL.
const ctx = canvas.getContext('2d')!;
ctx.drawImage(image, 0, 0, newWidth, newHeight);
const newDataUrl = canvas.toDataURL(imageType, quality);
return newDataUrl;
}
else {
return dataUrl;
}
}
For Moderm browser use createImageBitmap() instead of img.onload
async function compressImage(blobImg, percent) {
let bitmap = await createImageBitmap(blobImg);
let canvas = document.createElement("canvas");
let ctx = canvas.getContext("2d");
canvas.width = bitmap.width;
canvas.height = bitmap.height;
ctx.drawImage(bitmap, 0, 0);
let dataUrl = canvas.toDataURL("image/jpeg", percent/100);
return dataUrl;
}
inputImg.addEventListener('change', async(e) => {
let img = e.target.files[0];
console.log('File Name: ', img.name)
console.log('Original Size: ', img.size.toLocaleString())
let imgCompressed = await compressImage(img, 75) // set to 75%
let compSize = atob(imgCompressed.split(",")[1]).length;
console.log('Compressed Size: ', compSize.toLocaleString())
//console.log(imgCompressed)
})
<input type="file" id="inputImg">
Edit: As per the Mr Me comment on this answer, it looks like compression is now available for JPG/WebP formats ( see https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL ).
As far as I know, you cannot compress images using canvas, instead, you can resize it. Using canvas.toDataURL will not let you choose the compression ratio to use. You can take a look at canimage that does exactly what you want : https://github.com/nfroidure/CanImage/blob/master/chrome/canimage/content/canimage.js
In fact, it's often sufficient to just resize the image to decrease it's size but if you want to go further, you'll have to use newly introduced method file.readAsArrayBuffer to get a buffer containing the image data.
Then, just use a DataView to read it's content according to the image format specification (http://en.wikipedia.org/wiki/JPEG or http://en.wikipedia.org/wiki/Portable_Network_Graphics).
It'll be hard to deal with image data compression, but it is worse a try. On the other hand, you can try to delete the PNG headers or the JPEG exif data to make your image smaller, it should be easier to do so.
You'll have to create another DataWiew on another buffer and fill it with the filtered image content. Then, you'll just have to encode you're image content to DataURI using window.btoa.
Let me know if you implement something similar, will be interesting to go through the code.
Compressor.js
https://github.com/fengyuanchen/compressorjs
import axios from 'axios';
import Compressor from 'compressorjs';
document.getElementById('file').addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) {
return;
}
new Compressor(file, {
quality: 0.6,
// The compression process is asynchronous,
// which means you have to access the `result` in the `success` hook function.
success(result) {
const formData = new FormData();
// The third parameter is required for server
formData.append('file', result, result.name);
// Send the compressed image file to server with XMLHttpRequest.
axios.post('/path/to/upload', formData).then(() => {
console.log('Upload success');
});
},
error(err) {
console.log(err.message);
},
});
});
I used the following package:
https://www.npmjs.com/package/browser-image-compression
npm install browser-image-compression
or
yarn add browser-image-compression
Then just following the docs:
import imageCompression from 'browser-image-compression';
const options = {
maxSizeMB: 0.5, // pretty much self-explanatory
maxWidthOrHeight: 500, // apparently px
}
imageCompression(file, options)
.then(function(compressedFile) {
console.log(
"compressedFile instanceof Blob",
compressedFile instanceof Blob
); // true
console.log(
`compressedFile size ${compressedFile.size /
1024 /
1024} MB`
); // smaller than maxSizeMB
return uploader(compressedFile); // code to actual upload, in my case uploader() is a function to upload to Firebase storage.
})
Just in case if you were curios about the uploader(), here's the code of it:
import { initializeApp } from "firebase/app";
const firebaseConfig = {
// your config
};
initializeApp(firebaseConfig);
import { getStorage, ref, uploadBytes, getDownloadURL } from "firebase/storage";
const storage = getStorage();
const sRef = ref(storage);
const uploader = async (file) => {
/* uploads to root */
// const imageRef = ref(sRef, file.name);
// console.log(imageRef);
// await uploadBytes(imageRef, file).then((snapshot) => {
// console.log("Uploaded a blob or file!", snapshot);
// });
/* upload to folder 'techs/' */
const folderRef = ref(sRef, "techs/" + file.name);
await uploadBytes(folderRef, file);
// get URL
const url = await getDownloadURL(ref(storage, folderRef));
console.log("url: ", url);
return url;
};
You can compress an image using the HTML <canvas> element:
function compressImage(imgToCompress, resizingFactor, quality) {
// resizing the image
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
const originalWidth = imgToCompress.width;
const originalHeight = imgToCompress.height;
const canvasWidth = originalWidth * resizingFactor;
const canvasHeight = originalHeight * resizingFactor;
canvas.width = canvasWidth;
canvas.height = canvasHeight;
context.drawImage(
imgToCompress,
0,
0,
originalWidth * resizingFactor,
originalHeight * resizingFactor
);
// reducing the quality of the image
canvas.toBlob(
(blob) => {
if (blob) {
// showing the compressed image
resizedImage.src = URL.createObjectURL(resizedImageBlob);
}
},
"image/jpeg",
quality
);
}
See this blog post for an in depth explanation: https://img.ly/blog/how-to-compress-an-image-before-uploading-it-in-javascript/
i improved the function a head to be this :
var minifyImg = function(dataUrl,newWidth,imageType="image/jpeg",resolve,imageArguments=0.7){
var image, oldWidth, oldHeight, newHeight, canvas, ctx, newDataUrl;
(new Promise(function(resolve){
image = new Image(); image.src = dataUrl;
log(image);
resolve('Done : ');
})).then((d)=>{
oldWidth = image.width; oldHeight = image.height;
log([oldWidth,oldHeight]);
newHeight = Math.floor(oldHeight / oldWidth * newWidth);
log(d+' '+newHeight);
canvas = document.createElement("canvas");
canvas.width = newWidth; canvas.height = newHeight;
log(canvas);
ctx = canvas.getContext("2d");
ctx.drawImage(image, 0, 0, newWidth, newHeight);
//log(ctx);
newDataUrl = canvas.toDataURL(imageType, imageArguments);
resolve(newDataUrl);
});
};
the use of it :
minifyImg(<--DATAURL_HERE-->,<--new width-->,<--type like image/jpeg-->,(data)=>{
console.log(data); // the new DATAURL
});
enjoy ;)
For JPG Image compression you can use the best compression technique called JIC
(Javascript Image Compression)This will definitely help you -->https://github.com/brunobar79/J-I-C