Mittwoch Sep 16, 2009

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

My thanks once more goes to all the people on the net sharing their findings.

Kommentare:

Senden Sie einen Kommentar:
  • HTML Syntax: Eingeschaltet