Image upload from the clipboard
A client recently asked me whether it was possible to modify a webapp that we've been building for them to allow for somehow uploading graphics from the clipboard. I hesitated and said that it wasn't possible with HTML / Javascript but then did a bit of research. It quickly became clear that one would need to use some Java applet / ActiveX control (do these still exist?) or Flash app to achieve the effect. So I set out to create something.
I am a decent backend Java programmer but I've never much liked the GUI side of Java and applets are GUI to some extent, so I searched the web and nicked ideas all over the place and this is what I came up with:
package de.woerd.applet;
import de.woerd.io.MultiPartFormOutputStream;
import java.awt.Color;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.image.BufferedImage;
import java.awt.image.ImageObserver;
import java.awt.image.RenderedImage;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JApplet;
import javax.swing.JLabel;
/**
*
* @author joerg
*/
public class ImagePaster extends JApplet {
Clipboard clipboard;
Toolkit toolkit;
JLabel status;
@Override
public void init() {
super.init();
toolkit = Toolkit.getDefaultToolkit();
clipboard = toolkit.getSystemClipboard();
}
/**
*
* @param targetUri - relative to location of page this applet is embedded in
* @param format format of resulting image bytes, currently only support 'jpeg' or 'png'
* @return
*/
public AppletResult pasteImage(String targetUrl, String format) {
//get image data from clipboard
Image image = getImageFromClipboard();
if(image == null)
return new AppletResult(false, "Kein Bild!", null);
if(!("jpeg".equals(format) || "png".equals(format)))
return new AppletResult(false, "Format nicht unterstützt. Bitte entweder 'jpeg' oder 'png' wählen", null);
// create a byte stream of that image, encoded in the correct image format
ByteArrayOutputStream imageBytes = null;
try{
imageBytes = getImageBytes(image, format);
}
catch(IOException e)
{
return new AppletResult(false, "Bild konnte nicht gelesen werden", null);
}
// upload bytes to targetUrl as
String result = null;
try{
result = uploadImage(targetUrl, imageBytes, format);
}
catch(Exception e)
{
return new AppletResult(false, "Bildaten konnten nicht heraufgeladen werden", null);
}
return new AppletResult(true, null, result);
}
private Image getImageFromClipboard() {
Transferable transferable = clipboard.getContents(null);
if(!transferable.isDataFlavorSupported(DataFlavor.imageFlavor))
return null;
try {
Image img = (Image) clipboard.getContents(null).getTransferData(DataFlavor.imageFlavor);
BufferedImage newImg = null;
int w = img.getWidth(null);
int h = img.getHeight(null);
newImg = new BufferedImage(w,h,BufferedImage.TYPE_INT_RGB);
ImageIcon ii = new ImageIcon(img);
ImageObserver is = ii.getImageObserver();
newImg.getGraphics().setColor(new Color(255, 255, 255));
newImg.getGraphics().fillRect(0, 0, w, h);
newImg.getGraphics().drawImage(ii.getImage(), 0, 0, is);
return newImg;
} catch (Exception e) {
return null;
}
}
private ByteArrayOutputStream getImageBytes(Image image, String format) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
if(image instanceof RenderedImage)
{
ImageIO.write((RenderedImage)image, format, baos);
}
if(baos.size() == 0)
throw new IOException("No image data found");
return baos;
}
private String uploadImage(String targetUrl, ByteArrayOutputStream imageBytes, String format) throws Exception {
URL url = new URL(getDocumentBase(), targetUrl);
// create a boundary string
String boundary = MultiPartFormOutputStream.createBoundary();
URLConnection urlConn = MultiPartFormOutputStream.createConnection(url);
urlConn.setRequestProperty("Accept", "*/*");
urlConn.setRequestProperty("Content-Type", MultiPartFormOutputStream.getContentType(boundary));
// set some other request headers...
urlConn.setRequestProperty("Connection", "Keep-Alive");
urlConn.setRequestProperty("Cache-Control", "no-cache");
// no need to connect because getOutputStream() does it
MultiPartFormOutputStream out = new MultiPartFormOutputStream(urlConn.getOutputStream(), boundary);
// write bytes
out.writeFile("cbUpload", "image/" + format, "clipboardImageUpload." + format, imageBytes.toByteArray());
out.close();
// read response from server
StringBuffer buf = new StringBuffer();
BufferedReader in = new BufferedReader(new InputStreamReader(urlConn.getInputStream()));
String line = "";
while ((line = in.readLine()) != null) {
buf.append(line);
}
in.close();
return buf.toString();
}
}
package de.woerd.applet;
public class AppletResult {
private boolean success;
private String message;
private String result;
public AppletResult(boolean success, String message, String result) {
this.success = success;
this.message = message;
this.result = result;
}
public boolean isSuccess() {
return this.success;
}
public void setSuccess(boolean success) {
this.success = success;
}
public String getMessage() {
return this.message;
}
public void setMessage(String message) {
this.message = message;
}
public String getResult() {
return this.result;
}
public void setResult(String result) {
this.result = message;
}
}
Now for this to work as an applet you also need the accompanying class MultiPartFormOutputStream to do the actual multipart posting which I nicked as well and which you can download here and the applet needs to be signed.
I used Netbeans 6.7 following these instructions.
You can then embed the applet into an html page like this:
<body>
<script type="text/javascript">
function upload() {
obj = document.getElementById('paste-image');
result = obj.pasteImage('/uploadscript', "jpeg");
if(result.isSuccess()) {
// do something with result.getResult()
// result.getResult() will contain whatever the uploadscript returns. So it'd make some sense to make it return the URI of the new upload ;-)
}
else { // error
// display result.getMessage() in some way
}
}
</script>
<object id="paste-image" classid="java:de/woerd/applet/ImagePaster.class" type="application/x-java-applet" archive="/path/to/signed/jarfile.jar" />" width="1" height="1"></object>
<input type="button" value="Paste it!" onclick="upload();">
</body>
When you then copy an image to the clipboard and press the 'Paste it!' button, the image content is obtained from the clipboard, transformed into a jpeg or png and uploaded to your server script.
Notes
-
What I don't really understand is what happens in the 'getImageFromClipboard()' method, which - you guessed it - is also nicked. But if I take the image that I obtain with clipboard.getContents(null).getTransferData(DataFlavor.imageFlavor) directly and pass it to ImageIO I get a black empty graphic.
-
What I haven't fully grasped yet is whether in certain circumstances I need to to a Base64 transfer encoding and how I would do that without additional libraries (I have implemented the upload with nicked code rather than with e.g. Commons HttpClient because in a signed applet context all libs need to be signed as well and I couldn't face the extra depoyment steps for that)
-
This is code that I'm currently integrating into a client project. It is only being tested for the (controlled) environment at this client and might now work in all OS/Browser combinations
-
My next step will be doing a drag'n drop file upload which is easy now that I got the basic understanding of how applets work
References
- http://tinymce.moxiecode.com/punbb/viewtopic.php?pid=18088#p18088
- http://lassebunk.dk/2009/08/04/clipboard-java-applet/
- http://forums.sun.com/thread.jspa?forumID=31&threadID=451245
- http://java.sun.com/javase/6/webnotes/6u10/plugin2/liveconnect/
My thanks once more goes to all the people on the net sharing their findings.
Posted at 11:20AM Sep 16, 2009 by joerg in Allgemein | Kommentare[0]