Pasting Images Into Your App Using File Blobs And URL.createObjectURL() In Angular 7.2.15
In the past couple of months, I've been playing around a lot more with File handling in Angular. Things like reading a drag-and-drop text File, uploading a single File with HttpClient
and, uploading multiple File objects as a single Form Post all turn out to be somewhat simple in Angular. As another fun experiment in file handling, I waned to see if I could allow the user to Paste a copied Image File
from their computer's clipboard right into my Angular 7.2.15 app.
Run this demo in my JavaScript Demos project on GitHub.
View this code in my JavaScript Demos project on GitHub.
A few years ago, I learned about the ability to render image previews using "Object URLs". This approach uses the URL.createObjectURL()
method to convert a Blob (binary object) into an addressable URL like:
blob:http://127.0.0.1:56809/92ece87b-9242-4cf5-b027-90bdb7939dbb
This URL can then be treated just like any other URL; and, in particular, can be used as the src
attribute for an image (or the href
attribute of an anchor tag). This allows us to navigate to dynamic data without having to worry about Base64-encoding complexities and URL length limitations.
Given this functionality - which has been supported since Internet Explorer 10 (IE10) - I wanted to see if I could capture the File
object attached to a Window paste
event, turn that File
object into an "Object URL", and then render it in my Angular application.
To explore this idea, I created an App component that provides a few demo images (of Wildlings from Game of Thrones). These images can be right-clicked for "Copy Image" functionality. Then, if the user pastes (Cmd+V
) anywhere in the Browser window, I'm going to intercept the event
, extract the File
, and then render it in a list of img
elements:
// Import the core angular services.
import { Component } from "@angular/core";
import { DomSanitizer } from "@angular/platform-browser";
import { SafeUrl } from "@angular/platform-browser";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
@Component({
selector: "my-app",
host: {
"(window:paste)": "handlePaste( $event )"
},
styleUrls: [ "./app.component.less" ],
template:
`
<h2>
Sample Images (That You Can Right-Click And Copy)
</h2>
<p class="sample-images">
<img src="./img/image-1.jpg" class="sample-image" />
<img src="./img/image-2.jpg" class="sample-image" />
<img src="./img/image-3.png" class="sample-image" />
<img src="./img/image-4.jpg" class="sample-image" />
<img src="./img/image-5.jpg" class="sample-image" />
</p>
<h2>
Pasted Images
</h2>
<p class="images">
<ng-template ngFor let-imageUrl [ngForOf]="imageUrls">
<img [src]="imageUrl" class="image" />
</ng-template>
</p>
`
})
export class AppComponent {
public imageUrls: SafeUrl[];
private lastObjectUrl: string;
private sanitizer: DomSanitizer;
// I initialize the app component.
constructor( sanitizer: DomSanitizer ) {
this.sanitizer = sanitizer;
this.imageUrls = [];
this.lastObjectUrl = "";
}
// ---
// PUBLIC METHODS.
// ---
// I handle the paste event on the Window (see host bindings).
public handlePaste( event: ClipboardEvent ) : void {
var pastedImage = this.getPastedImage( event );
if ( ! pastedImage ) {
return;
}
// When we create Object URLs, the browser will keep them in memory until the
// document is unloaded or until the URL is explicitly released. Since we are
// going to create a new URL every time the user pastes an image into the app (in
// this particular demo), we need to be sure to release the previous Object URL
// before we create the new one.
// --
// NOTE: One the Image is rendered in the DOM, releasing the Object URL will not
// affect the rendering.
if ( this.lastObjectUrl ) {
URL.revokeObjectURL( this.lastObjectUrl );
}
// At this point, the "pastedImage" is a File object, which is a specialized type
// of "Blob". We can now generate a "blob:" URL using the given File.
this.lastObjectUrl = URL.createObjectURL( pastedImage );
// By default, Angular WILL NOT TRUST this "blob:" style URLs. However, since we
// know these are going to be expected, we can use the DOM Sanitizer to bypass
// the security checks on these images.
// --
// NOTE: The sanitizer doesn't return Strings - it returns SafeUrls.
this.imageUrls.unshift(
this.sanitizer.bypassSecurityTrustUrl( this.lastObjectUrl )
);
}
// ---
// PRIVATE METHODS.
// ---
// I return the first Image File from the given paste event (or null).
private getPastedImage( event: ClipboardEvent ) : File | null {
// NOTE: I am not very familiar with the Paste Event. As such, I am probably
// being more cautious here than I need to be. However, in an abundance of
// caution, I am checking each part of the targeted object path.
if (
event.clipboardData &&
event.clipboardData.files &&
event.clipboardData.files.length &&
this.isImageFile( event.clipboardData.files[ 0 ] )
) {
return( event.clipboardData.files[ 0 ] );
}
return( null );
}
// I determine if the given File is an Image (according do its Mime-Type).
private isImageFile( file: File ) : boolean {
return( file.type.search( /^image\//i ) === 0 );
}
}
As you can see, this turns out to be a fairly simple workflow. The ClipboardEvent
contains a list of File
objects. I grab the first File
in the list, check to make sure its mime-type
matches image/*
, and then I generate an Object URL which I store in my view-model.
If I run this Angular app in the browser and copy-paste a few of the images, we get the following output:
The one hoop that we have to jump through is Security. By default, Angular tries to protect us from potentially malicious behavior. And, it categorizes blob:
URLs as potentially malicious. So, in order for us to be able to consume the generated Object URL in our Image src
attribute, we have to pass it through the DOMSanitizer
, which will return an instance of SafeUrl
(this is not a String).
The more I play with File
management in my Angular applications, the more I am delighted to see that modern Browser APIs make things rather simple. I think this opens up a whole world of interesting interaction workflows, like being able to paste Images directly into an application. This is definitely something I'm going to start thinking about at InVision.
Want to use code from this post? Check out the license.
Reader Comments
Hi this is great idea i liked to and i have doubt here how to upload pasted (attached) images in to the serve.Can i upload blob URL Directly ?
Could you help me please.
Thank you
@Thejesh,
I'm glad you find this stuff interesting. Yes, you can definitely upload a pasted-image. The
paste
event actually contains aFile
object. And, you should be able to take thatFile
object and just upload it using an HTTP client. For example, in this post:www.bennadel.com/blog/3593-uploading-files-with-httpclient-in-angular-7-2-11.htm
.... I am uploading a
File
object via theHttpClient
in Angular. In that post, I was selecting the File using the File Input element; but, you could just as easily -- I am pretty sure -- use the paste event to do the same thing.Let me know if that helps. Otherwise, I can try to put together a demo.
@Ben,
Thank you for your great reply get back to you soon for the same.
And I need one more help related to rich text editor for angular 7( Eample link : https://ckeditor.com/docs/ckeditor5/latest/examples/builds/inline-editor.html).Is there any open source/free library are available, could you suggest me?
@Thejesh,
I am not sure. I've been using Markdown as my editing approach (which is nice because it doesn't require much client-side functionality). But, I see there is a Ticket here about adding a rich text editor to Angular Material:
https://github.com/angular/material/issues/8487
... while there isn't one currently, it looks like there are several mentioned in the ticket comments.
Hi,
I love this approach. Anyhow, when I tried copying an image with transparent background from windows application such as powerpoint, and pasting it in web app, the transparency is not preserved. Image is being displayed with white background.
Could you provide some idea on how to maintain the transparency?
Thanks in advance
@Ganesh,
I am not sure what in this process would be responsible for stripping the transparency from the image. After all, we're not modifying the image - we're just pasting the binary data into the Angular app and then uploading it via AJAX.
As a sanity check, I would try right-clicking the image in the source page and do "Save As" to your local computer. And then, confirm that the saved image has transparency. It could be that there's something confusing about the image?? I'm not really sure.
URL.createObjectURL has been depreceated in chrom recently. Do you know any other way to fix this?
Thanks for the article.
It really helped me!
@Akhil,
Hmmm, very interesting. I just did some Googling and it looks like the new approach is to use
HTMLMediaElement
andsrcObject
. But, according to MDN, the support for that is not great:https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/srcObject
MDN suggests falling back to the old
URL.createObjectURL
for most browsers. Since I've only just learned about this (from you), I don't have any good insights. I'll have to dig a little deeper.@Werner,
Awesome! I'm glad you found this helpful :D
@Akhil,
Actually, looking closer at the deprecation notes, I do not think that it is fully deprecating all uses of
URL.createObjectURL()
. It seems to only say that it is being deprecated specifically when the argument is of typeMediaStream
. In my demos, the arguments are generally of typeFile
orBlob
. As such, I am not sure this deprecation applies to us.In fact, if you run this Angular demo in either Firefox or Chrome, you will see that no deprecation warning shows up in the logs. So, I think we may be safe!
Great stuff !
I was looking around for something like this, I guess I just found a gem ! Thank you
@Saad,
Awesome! I'm happy to hear this helped you out. I love the interaction we get with File objects and Blob data.
Great article!
How can trigger paster event from a button?