Detecting New Images in the Clipboard

I have 2 applications that should work together.
These applications are end-user applications. The link between them is in fact the system’s clipboard (this is a Windows 7’s use case, but it might be encountered on other platforms). Basically, the idea is that the user selects an image in the first application and copies it in the clipboard. The second application should detect this new image and process it.

By browsing the web, I found out that there is no perfect solution. Sun did not provide a complete solution for this in Java because there were portability issues. One alternative was to use the ava.awt.datatransfer.ClipboardOwner class. A subclass instance acquires the ownership of the clipboard by putting something into it. When another application copies something into the clipboard, the previous owner is notified that it has lost the ownership. The implementation would be something like:

public class ClipboardWatcher implements ClipboardOwner {

	/**
	 * @see java.awt.datatransfer.ClipboardOwner
	 * #lostOwnership(java.awt.datatransfer.Clipboard, java.awt.datatransfer.Transferable)
	 */
	@Override
	public void lostOwnership( Clipboard clipboard, Transferable contents ) {
		// Get the clipboard's content and process it if required
		// ...

		// Acquire the ownership again by putting the same content
		// (Same content because otherwise, copy/paste does not work anymore for other applications)
		// ...
	}
}

This is not very convenient.
I also discovered it was not working very well when there are several running applications.
So, I implemented a solution with a thread which polls the clipboard regularly. I put here a complete sample with an example of image processing.

import java.awt.Image;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.ClipboardOwner;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.imageio.ImageIO;

/**
 * @author Vincent Zurczak
 */
public class ClipboardWatcher implements ClipboardOwner {
	
	static final File PREVIEW_DIRECTORY = new File( System.getProperty( "java.io.tmpdir" ), "Image-Store" );
	private final List<PreviewFileListener> listeners;
	private final SimpleDateFormat sdf;
	private final AtomicBoolean run;
	
	
	/**
	 * Constructor.
	 */
	public ClipboardWatcher() {
		 this.listeners = new ArrayList<PreviewFileListener> ();
		 this.sdf = new SimpleDateFormat( "yy-MM-dd---HH-mm-ss" );
		 this.run = new AtomicBoolean( true );
		 
		 if( ! PREVIEW_DIRECTORY.exists()
				 && ! PREVIEW_DIRECTORY.mkdir()) {
			 // TODO: process this case in a better way
			 System.out.println( "The preview directory could not be created." );
		 }
	}
	
	
	/**
	 * @param e
	 * @return
	 * @see java.util.List#add(java.lang.Object)
	 */
	public boolean addPreviewFileListener( PreviewFileListener e ) {
		synchronized( this.listeners ) {
			return this.listeners.add( e );
		}
	}


	/**
	 * @param o
	 * @return
	 * @see java.util.List#remove(java.lang.Object)
	 */
	public boolean removePreviewFileListener( PreviewFileListener o ) {
		synchronized( this.listeners ) {
			return this.listeners.remove( o );
		}
	}
	
	
	/**
	 * @see java.awt.datatransfer.ClipboardOwner
	 * #lostOwnership(java.awt.datatransfer.Clipboard, java.awt.datatransfer.Transferable)
	 */
	@Override
	public void lostOwnership( Clipboard clipboard, Transferable contents ) {
		// Acquire the ownership again is not working very well for a periodic check
	}
	
	
	/**
	 * Starts polling the clipboard.
	 */
	public void start() {
		
		// Define the polling delay
		final int pollDelay = 5000;
		
		// Create the runnable
		Runnable runnable = new Runnable() {
			@Override
			public void run() {
				
				Transferable contents = null;
				while( ClipboardWatcher.this.run.get()) {
					
					// Get the clipboard's content
					Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
					try {
						contents = clipboard.getContents( null );
						if ( contents == null 
								|| ! contents.isDataFlavorSupported( DataFlavor.imageFlavor ))
							continue;
						
						Image img = (Image) clipboard.getData( DataFlavor.imageFlavor );
						saveClipboardImage( img );
						clipboard.setContents( new StringSelection( "The clipboard watcher was here!" ), ClipboardWatcher.this );
						
					} catch( UnsupportedFlavorException ex ) {
						ex.printStackTrace();

					} catch( IOException ex ) {
						ex.printStackTrace();
						
					} catch( Exception e1 ) {
						// We get here if we could not get the clipboard's content
						continue;
					
					} finally {						
					
						try {
							Thread.sleep( pollDelay );
							
						} catch( InterruptedException e ) {
							e.printStackTrace();
						}
					}
				}
			}
		};
		
		// Run it in a thread
		Thread thread = new Thread( runnable );
		thread.start();
	}
	
	
	/**
	 * Stops listening.
	 */
	public void stop() {
		this.run.set( false );
	}
	

	/**
	 * Saves the image residing on the clip board.
	 */
	public void saveClipboardImage( Image img ) {

		if( img != null ) {	    	
			if( img instanceof BufferedImage ) {
				try {
					File file = new File( PREVIEW_DIRECTORY, this.sdf.format( new Date()) + ".jpg" );
					ImageIO.write((BufferedImage) img, "jpg", file );
					for( PreviewFileListener listener : this.listeners )
						listener.newCreatedFile( file );

				} catch ( IOException e ) {
					e.printStackTrace();
				}

			} else {
				System.out.println( "Unsupported image: " + img.getClass());
			}	
		}
	}
	
	
	/**
	 * A interface to listen to image detection.
	 */
	public interface PreviewFileListener {

		/**
		 * Notifies this instance that an image file was created from the clip board.
		 * @param imageFile the image file that was saved on the disk
		 */
		void newCreatedFile( File imageFile );
	}
}

There is another alternative for Windows platforms.
However, it was only implemented for an OSGi context. Notice that it should not be too difficult to adapt it for standalone Java applications. I read and understood most of the code, but I was a little bit lazy on this one. Check this blog entry for more details. The code is hosted on GitHub.