
input file: how to manipulate an image before uploading
UPDATE 2017 July 2st
I’ve fixed the source code in order to solve a couple of bug:
1) Files with no exif data crashes the app
2) new Files() on iphone brakes the upload, formData contains empty files
——
I built an angular2+ component that allows selecting and previewing single images before uploading them through an api.
The component works fine with angular forms.
When the user selects a picture, it often happens that it is not correctly oriented, especially if the picture is taken on the fly from the mobile camera.
The solution is made of three steps:
- Read the exif data and get the orientation
- Accordingly with the orientation, rotate the image by using canvas
- Update the “file” object
In this article I am going to focus on these steps, in a further one, I’d like to discuss about the whole component (that works with angular form by implementing ControlValueAccessor)
For the steps 1 and 2 I’ve written a little angular provider in order to have a base on which I can add other canvas methods:
(Read the comments in the code below)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 |
import { Injectable } from '@angular/core'; //this is the lib I chose for reading exif data https://github.com/hMatoba/piexifjs import piexif from 'piexifjs'; @Injectable() export class ImagemanProvider { public imageObj; public imageSrc; public exifObj; public canvas; public ctx; constructor() { this.imageObj = new Image(); } readImg(src) { //loading images is an async operation so it returns a promise return new Promise((resolve,reject) => { if (src) { this.imageSrc = src; this.imageObj.src = src; this.imageObj.onload = () => { resolve(); } } else reject(); }); } readExif() { try { this.exifObj = piexif.load(this.imageSrc); return true; } catch(e) { return false; } } getExifOrientation() { if (this.exifObj) return this.exifObj["0th"][piexif.ImageIFD.Orientation]; else return null; } _createCanvas() { this.canvas = document.createElement("canvas"); this.canvas.width = this.imageObj.width; this.canvas.height = this.imageObj.height; this.ctx = this.canvas.getContext("2d"); this.ctx.save(); this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); } //I found these cases here https://github.com/hMatoba/piexifjs/blob/master/samples/RotateByExif.html ctxFixOrientation() { let isExif = this.readExif(); if (!isExif) return false; else { this._createCanvas(); let x = 0; let y = 0; let orientation = this.getExifOrientation(); if (orientation == 2) { x = -this.canvas.width; this.ctx.scale(-1, 1); } else if (orientation == 3) { x = -this.canvas.width; y = -this.canvas.height; this.ctx.scale(-1, -1); } else if (orientation == 4) { y = -this.canvas.height; this.ctx.scale(1, -1); } else if (orientation == 5) { this.canvas.width = this.imageObj.height; this.canvas.height = this.imageObj.width; this.ctx.translate(this.canvas.width, this.canvas.height / this.canvas.width); this.ctx.rotate(Math.PI / 2); y = -this.canvas.width; this.ctx.scale(1, -1); } else if (orientation == 6) { this.canvas.width = this.imageObj.height; this.canvas.height = this.imageObj.width; this.ctx.translate(this.canvas.width, this.canvas.height / this.canvas.width); this.ctx.rotate(Math.PI / 2); } else if (orientation == 7) { this.canvas.width = this.imageObj.height; this.canvas.height = this.imageObj.width; this.ctx.translate(this.canvas.width, this.canvas.height / this.canvas.width); this.ctx.rotate(Math.PI / 2); x = -this.canvas.height; this.ctx.scale(-1, 1); } else if (orientation == 8) { this.canvas.width = this.imageObj.height; this.canvas.height = this.imageObj.width; this.ctx.translate(this.canvas.width, this.canvas.height / this.canvas.width); this.ctx.rotate(Math.PI / 2); x = -this.canvas.height; y = -this.canvas.width; this.ctx.scale(-1, -1); } this.ctx.drawImage(this.imageObj, x, y); this.ctx.restore(); return true; } } getBase64() { if (this.canvas) return this.canvas.toDataURL("image/jpeg", 1.0); } } |
After we’ve integrated this provider, we just have to call its methods and update our “file” object
I use FileReader() in order to show the preview, so I modify the image as soon as I load it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
//this method is called after the user selects a single image (I did not need a multi) async filesAdded(event: Event) { let files: FileList = this.nativeInputBtn.nativeElement.files; let reader = new FileReader(); //I don't use the onprogress but I left the method in case you need it /*reader.onprogress = function(data) { if (data.lengthComputable) { let progress = ((data.loaded / data.total) * 100); //console.log(progress); } }*/ //this is called from reader.readAsDataURL(file) reader.onload = (async (e) => { let dataImage = _.get(e,'target.result'); //here I've the base64 of the image await this.imageman.readImg(dataImage); //I need await since it is async (I can even use then since it's a promise) if (this.imageman.ctxFixOrientation()) { //this actually rotates the image and returns true if succeeds dataImage = this.imageman.getBase64(); //to get the new base64 of the rotated image } this.imageman.ctxFixOrientation(); dataImage = this.imageman.getBase64(); this.read_preview='url(' + dataImage + ')'; //this is for the preview this.read.data=dataImage; //this finally is the step3, here I convert the base64 in a blob and then I use it to build a new "file" object let blobBin = atob(dataImage.split(',')[1]); let array = []; for(let i = 0; i < blobBin.length; i++) { array.push(blobBin.charCodeAt(i)); } let blob = new Blob([new Uint8Array(array)], {type: this.read.file.type}); //I substitute the original file object, on iphone new files() creates empty files. //I pass directly the blob to fd.append() this.read.file = blob;//new File([blob], this.read.file.name); //end of step 3 this.propagateChange(this.read); //this for the integration with angular form if (typeof this.btnCallback == 'function') this.btnCallback(this.read.file); //the callback to get the file out of a form this.readingImgFromDisk=false; //I don't need the spinner anymore }); let file = _.get(files,'[0]'); if (file) { this.readingImgFromDisk=true; //shows a loader while the pic is loading //this little delay is useful to give the time to the view to render a spinner setTimeout(() => { reader.readAsDataURL(file); //this triggers "reader.onload" },100); this.read = { file:file //I am gonna overwrite it }; } } |