diff src/myVncProxy/ProxyVncCanvas.java @ 24:87b29d6039a6

add package myVncProxy
author e085711
date Sun, 24 Apr 2011 23:03:00 +0900
parents src/ProxyVncCanvas.java@b51bb7bc0766
children cded9fd297ab
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/myVncProxy/ProxyVncCanvas.java	Sun Apr 24 23:03:00 2011 +0900
@@ -0,0 +1,1878 @@
+package myVncProxy;
+import java.awt.*;
+import java.awt.event.*;
+import java.awt.image.*;
+import java.io.*;
+import java.lang.*;
+import java.nio.ByteBuffer;
+import java.util.zip.*;
+
+import java.net.Socket;
+
+//
+//VncCanvas is a subclass of Canvas which draws a VNC desktop on it.
+//
+
+class ProxyVncCanvas extends Canvas implements KeyListener, MouseListener,
+		MouseMotionListener {
+
+	VncProxyService viewer;
+	MyRfbProto rfb;
+	ColorModel cm8, cm24;
+	Color[] colors;
+	int bytesPixel;
+
+	int maxWidth = 0, maxHeight = 0;
+	int scalingFactor;
+	int scaledWidth, scaledHeight;
+
+	Image memImage;
+	Graphics memGraphics;
+
+	Image rawPixelsImage;
+	MemoryImageSource pixelsSource;
+	byte[] pixels8;
+	int[] pixels24;
+
+	// Update statistics.
+	long statStartTime; // time on first framebufferUpdateRequest
+	int statNumUpdates; // counter for FramebufferUpdate messages
+	int statNumTotalRects; // rectangles in FramebufferUpdate messages
+	int statNumPixelRects; // the same, but excluding pseudo-rectangles
+	int statNumRectsTight; // Tight-encoded rectangles (including JPEG)
+	int statNumRectsTightJPEG; // JPEG-compressed Tight-encoded rectangles
+	int statNumRectsZRLE; // ZRLE-encoded rectangles
+	int statNumRectsHextile; // Hextile-encoded rectangles
+	int statNumRectsRaw; // Raw-encoded rectangles
+	int statNumRectsCopy; // CopyRect rectangles
+	int statNumBytesEncoded; // number of bytes in updates, as received
+	int statNumBytesDecoded; // number of bytes, as if Raw encoding was used
+
+	// ZRLE encoder's data.
+	byte[] zrleBuf;
+	int zrleBufLen = 0;
+	byte[] zrleTilePixels8;
+	int[] zrleTilePixels24;
+	ZlibInStream zrleInStream;
+	boolean zrleRecWarningShown = false;
+
+	// Zlib encoder's data.
+	byte[] zlibBuf;
+	int zlibBufLen = 0;
+	Inflater zlibInflater;
+
+	// Tight encoder's data.
+	final static int tightZlibBufferSize = 512;
+	Inflater[] tightInflaters;
+
+	// Since JPEG images are loaded asynchronously, we have to remember
+	// their position in the framebuffer. Also, this jpegRect object is
+	// used for synchronization between the rfbThread and a JVM's thread
+	// which decodes and loads JPEG images.
+	Rectangle jpegRect;
+
+	// True if we process keyboard and mouse events.
+	boolean inputEnabled;
+
+	
+	//
+	// The constructors.
+	//
+
+	public ProxyVncCanvas(VncProxyService v, int maxWidth_, int maxHeight_)
+			throws IOException {
+
+		viewer = v;
+		maxWidth = maxWidth_;
+		maxHeight = maxHeight_;
+
+		rfb = viewer.rfb;
+
+		tightInflaters = new Inflater[4];
+
+		cm8 = new DirectColorModel(8, 7, (7 << 3), (3 << 6));
+		cm24 = new DirectColorModel(24, 0xFF0000, 0x00FF00, 0x0000FF);
+
+		colors = new Color[256];
+		for (int i = 0; i < 256; i++)
+			colors[i] = new Color(cm8.getRGB(i));
+
+		// setPixelFormat();
+
+		inputEnabled = false;
+		// Keyboard listener is enabled even in view-only mode, to catch
+		// 'r' or 'R' key presses used to request screen update.
+		addKeyListener(this);
+	}
+
+	public ProxyVncCanvas(VncProxyService v) throws IOException {
+		this(v, 0, 0);
+	}
+
+	//
+	// Callback methods to determine geometry of our Component.
+	//
+
+	public Dimension getPreferredSize() {
+		return new Dimension(scaledWidth, scaledHeight);
+	}
+
+	public Dimension getMinimumSize() {
+		return new Dimension(scaledWidth, scaledHeight);
+	}
+
+	public Dimension getMaximumSize() {
+		return new Dimension(scaledWidth, scaledHeight);
+	}
+
+	//
+	// All painting is performed here.
+	//
+
+	public void update(Graphics g) {
+		paint(g);
+	}
+
+	public void paint(Graphics g) {
+		synchronized (memImage) {
+			if (rfb.framebufferWidth == scaledWidth) {
+				g.drawImage(memImage, 0, 0, null);
+			} else {
+				paintScaledFrameBuffer(g);
+			}
+		}
+		if (showSoftCursor) {
+			int x0 = cursorX - hotX, y0 = cursorY - hotY;
+			Rectangle r = new Rectangle(x0, y0, cursorWidth, cursorHeight);
+			if (r.intersects(g.getClipBounds())) {
+				g.drawImage(softCursor, x0, y0, null);
+			}
+		}
+	}
+
+	public void paintScaledFrameBuffer(Graphics g) {
+		g.drawImage(memImage, 0, 0, scaledWidth, scaledHeight, null);
+	}
+
+	//
+	// Override the ImageObserver interface method to handle drawing of
+	// JPEG-encoded data.
+	//
+
+	public boolean imageUpdate(Image img, int infoflags, int x, int y,
+			int width, int height) {
+		if ((infoflags & (ALLBITS | ABORT)) == 0) {
+			return true; // We need more image data.
+		} else {
+			// If the whole image is available, draw it now.
+			if ((infoflags & ALLBITS) != 0) {
+				if (jpegRect != null) {
+					synchronized (jpegRect) {
+						memGraphics
+								.drawImage(img, jpegRect.x, jpegRect.y, null);
+						scheduleRepaint(jpegRect.x, jpegRect.y, jpegRect.width,
+								jpegRect.height);
+						jpegRect.notify();
+					}
+				}
+			}
+			return false; // All image data was processed.
+		}
+	}
+
+	//
+	// Start/stop receiving mouse events. Keyboard events are received
+	// even in view-only mode, because we want to map the 'r' key to the
+	// screen refreshing function.
+	//
+
+	public synchronized void enableInput(boolean enable) {
+		if (enable && !inputEnabled) {
+			inputEnabled = true;
+			addMouseListener(this);
+			addMouseMotionListener(this);
+			if (viewer.showControls) {
+				viewer.buttonPanel.enableRemoteAccessControls(true);
+			}
+			createSoftCursor(); // scaled cursor
+		} else if (!enable && inputEnabled) {
+			inputEnabled = false;
+			removeMouseListener(this);
+			removeMouseMotionListener(this);
+			if (viewer.showControls) {
+				viewer.buttonPanel.enableRemoteAccessControls(false);
+			}
+			createSoftCursor(); // non-scaled cursor
+		}
+	}
+
+	public void setPixelFormat() throws IOException {
+		if (viewer.options.eightBitColors) {
+			rfb.writeSetPixelFormat(8, 8, false, true, 7, 7, 3, 0, 3, 6);
+			bytesPixel = 1;
+		} else {
+			rfb.writeSetPixelFormat(32, 24, false, true, 255, 255, 255, 16, 8,
+					0);
+			bytesPixel = 4;
+		}
+		updateFramebufferSize();
+	}
+
+	void updateFramebufferSize() {
+
+		// Useful shortcuts.
+		int fbWidth = rfb.framebufferWidth;
+		int fbHeight = rfb.framebufferHeight;
+
+		// Calculate scaling factor for auto scaling.
+		if (maxWidth > 0 && maxHeight > 0) {
+			int f1 = maxWidth * 100 / fbWidth;
+			int f2 = maxHeight * 100 / fbHeight;
+			scalingFactor = Math.min(f1, f2);
+			if (scalingFactor > 100)
+				scalingFactor = 100;
+			System.out.println("Scaling desktop at " + scalingFactor + "%");
+		}
+
+		// Update scaled framebuffer geometry.
+		scaledWidth = (fbWidth * scalingFactor + 50) / 100;
+		scaledHeight = (fbHeight * scalingFactor + 50) / 100;
+
+		// Create new off-screen image either if it does not exist, or if
+		// its geometry should be changed. It's not necessary to replace
+		// existing image if only pixel format should be changed.
+		if (memImage == null) {
+			memImage = viewer.vncContainer.createImage(fbWidth, fbHeight);
+			memGraphics = memImage.getGraphics();
+		} else if (memImage.getWidth(null) != fbWidth
+				|| memImage.getHeight(null) != fbHeight) {
+			synchronized (memImage) {
+				memImage = viewer.vncContainer.createImage(fbWidth, fbHeight);
+				memGraphics = memImage.getGraphics();
+			}
+		}
+
+		// Images with raw pixels should be re-allocated on every change
+		// of geometry or pixel format.
+		if (bytesPixel == 1) {
+
+			pixels24 = null;
+			pixels8 = new byte[fbWidth * fbHeight];
+
+			pixelsSource = new MemoryImageSource(fbWidth, fbHeight, cm8,
+					pixels8, 0, fbWidth);
+
+			zrleTilePixels24 = null;
+			zrleTilePixels8 = new byte[64 * 64];
+
+		} else {
+
+			pixels8 = null;
+			pixels24 = new int[fbWidth * fbHeight];
+
+			pixelsSource = new MemoryImageSource(fbWidth, fbHeight, cm24,
+					pixels24, 0, fbWidth);
+
+			zrleTilePixels8 = null;
+			zrleTilePixels24 = new int[64 * 64];
+
+		}
+		pixelsSource.setAnimated(true);
+		rawPixelsImage = Toolkit.getDefaultToolkit().createImage(pixelsSource);
+
+	}
+
+	void resizeDesktopFrame() {
+		setSize(scaledWidth, scaledHeight);
+
+		// FIXME: Find a better way to determine correct size of a
+		// ScrollPane. -- const
+		Insets insets = viewer.desktopScrollPane.getInsets();
+		viewer.desktopScrollPane.setSize(
+				scaledWidth + 2 * Math.min(insets.left, insets.right),
+				scaledHeight + 2 * Math.min(insets.top, insets.bottom));
+
+		viewer.vncFrame.pack();
+
+		// Try to limit the frame size to the screen size.
+
+		Dimension screenSize = viewer.vncFrame.getToolkit().getScreenSize();
+		Dimension frameSize = viewer.vncFrame.getSize();
+		Dimension newSize = frameSize;
+
+		// Reduce Screen Size by 30 pixels in each direction;
+		// This is a (poor) attempt to account for
+		// 1) Menu bar on Macintosh (should really also account for
+		// Dock on OSX). Usually 22px on top of screen.
+		// 2) Taxkbar on Windows (usually about 28 px on bottom)
+		// 3) Other obstructions.
+
+		screenSize.height -= 30;
+		screenSize.width -= 30;
+
+		boolean needToResizeFrame = false;
+		if (frameSize.height > screenSize.height) {
+			newSize.height = screenSize.height;
+			needToResizeFrame = true;
+		}
+		if (frameSize.width > screenSize.width) {
+			newSize.width = screenSize.width;
+			needToResizeFrame = true;
+		}
+		if (needToResizeFrame) {
+			viewer.vncFrame.setSize(newSize);
+		}
+
+		viewer.desktopScrollPane.doLayout();
+	}
+
+	//
+	// processNormalProtocol() - executed by the rfbThread to deal with the
+	// RFB socket.
+	//
+
+	public void processNormalProtocol() throws Exception {
+
+		// Start/stop session recording if necessary.
+		viewer.checkRecordingStatus();
+
+		rfb.writeFramebufferUpdateRequest(0, 0, rfb.framebufferWidth,
+				rfb.framebufferHeight, false);
+
+		resetStats();
+		boolean statsRestarted = false;
+
+		//
+		// main dispatch loop
+		//
+
+		long count = 0;
+
+		// single thread
+/*
+		try {
+			// rfb.setSoTimeout(1000);
+			Socket newCli = rfb.accept();
+			rfb.sendInitData(newCli);
+			rfb.addSock(newCli);
+		} catch (IOException e) {
+		}
+*/
+
+		
+		 Thread accept = new Thread(new acceptThread(rfb)); 
+		 accept.start();
+		
+
+		while (true) {
+
+//			System.out.println("\ncount=" + count);
+			count++;
+			
+			rfb.regiFramebufferUpdate();
+			rfb.checkAndMark();
+//			rfb.printFramebufferUpdate();
+
+			int bufSize = (int)rfb.getNumBytesRead();
+			
+			// Read message type from the server. 
+			int msgType = rfb.readServerMessageType();
+
+			// Process the message depending on its type.
+			switch (msgType) {
+			case RfbProto.FramebufferUpdate:
+
+				if (statNumUpdates == viewer.debugStatsExcludeUpdates
+						&& !statsRestarted) {
+					resetStats();
+					statsRestarted = true;
+				} else if (statNumUpdates == viewer.debugStatsMeasureUpdates
+						&& statsRestarted) {
+					viewer.disconnect();
+				}
+
+				rfb.readFramebufferUpdate();
+				statNumUpdates++;
+
+				boolean cursorPosReceived = false;
+
+				for (int i = 0; i < rfb.updateNRects; i++) {
+
+					rfb.readFramebufferUpdateRectHdr();
+					statNumTotalRects++;
+					int rx = rfb.updateRectX, ry = rfb.updateRectY;
+					int rw = rfb.updateRectW, rh = rfb.updateRectH;
+
+					if (rfb.updateRectEncoding == rfb.EncodingLastRect)
+						break;
+
+					if (rfb.updateRectEncoding == rfb.EncodingNewFBSize) {
+						rfb.setFramebufferSize(rw, rh);
+						updateFramebufferSize();
+						break;
+					}
+
+					if (rfb.updateRectEncoding == rfb.EncodingXCursor
+							|| rfb.updateRectEncoding == rfb.EncodingRichCursor) {
+						handleCursorShapeUpdate(rfb.updateRectEncoding, rx, ry,
+								rw, rh);
+						continue;
+					}
+
+					if (rfb.updateRectEncoding == rfb.EncodingPointerPos) {
+						softCursorMove(rx, ry);
+						cursorPosReceived = true;
+						continue;
+					}
+
+					long numBytesReadBefore = rfb.getNumBytesRead();
+
+					rfb.startTiming();
+
+					switch (rfb.updateRectEncoding) {
+					case RfbProto.EncodingRaw:
+						statNumRectsRaw++;
+						handleRawRect(rx, ry, rw, rh);
+						break;
+					case RfbProto.EncodingCopyRect:
+						statNumRectsCopy++;
+						handleCopyRect(rx, ry, rw, rh);
+						break;
+					case RfbProto.EncodingRRE:
+						handleRRERect(rx, ry, rw, rh);
+						break;
+					case RfbProto.EncodingCoRRE:
+						handleCoRRERect(rx, ry, rw, rh);
+						break;
+					case RfbProto.EncodingHextile:
+						statNumRectsHextile++;
+						handleHextileRect(rx, ry, rw, rh);
+						break;
+					case RfbProto.EncodingZRLE:
+						statNumRectsZRLE++;
+						handleZRLERect(rx, ry, rw, rh);
+						break;
+					case RfbProto.EncodingZlib:
+						handleZlibRect(rx, ry, rw, rh);
+						break;
+					case RfbProto.EncodingTight:
+						statNumRectsTight++;
+						handleTightRect(rx, ry, rw, rh);
+						break;
+					default:
+						throw new Exception("Unknown RFB rectangle encoding "
+								+ rfb.updateRectEncoding);
+					}
+
+					rfb.stopTiming();
+
+					statNumPixelRects++;
+					statNumBytesDecoded += rw * rh * bytesPixel;
+					statNumBytesEncoded += (int) (rfb.getNumBytesRead() - numBytesReadBefore);
+				}
+
+				boolean fullUpdateNeeded = false;
+
+				// Start/stop session recording if necessary. Request full
+				// update if a new session file was opened.
+				if (viewer.checkRecordingStatus())
+					fullUpdateNeeded = true;
+
+				// Defer framebuffer update request if necessary. But wake up
+				// immediately on keyboard or mouse event. Also, don't sleep
+				// if there is some data to receive, or if the last update
+				// included a PointerPos message.
+				if (viewer.deferUpdateRequests > 0 && rfb.available() == 0
+						&& !cursorPosReceived) {
+					synchronized (rfb) {
+						try {
+							rfb.wait(viewer.deferUpdateRequests);
+						} catch (InterruptedException e) {
+						}
+					}
+				}
+
+				viewer.autoSelectEncodings();
+
+				// Before requesting framebuffer update, check if the pixel
+				// format should be changed.
+/*
+				if (viewer.options.eightBitColors != (bytesPixel == 1)) {
+					// Pixel format should be changed.
+					setPixelFormat();
+					fullUpdateNeeded = true;
+				}
+*/
+				// Request framebuffer update if needed.
+				int w = rfb.framebufferWidth;
+				int h = rfb.framebufferHeight;
+				rfb.writeFramebufferUpdateRequest(0, 0, w, h, !fullUpdateNeeded);
+
+				break;
+
+			case RfbProto.SetColourMapEntries:
+				throw new Exception("Can't handle SetColourMapEntries message");
+
+			case RfbProto.Bell:
+				Toolkit.getDefaultToolkit().beep();
+				break;
+
+			case RfbProto.ServerCutText:
+				String s = rfb.readServerCutText();
+				viewer.clipboard.setCutText(s);
+				break;
+
+			default:
+				throw new Exception("Unknown RFB message type " + msgType);
+			}
+			bufSize = (int)rfb.getNumBytesRead() - bufSize;
+//			System.out.println("bufSize="+bufSize);
+			rfb.bufResetSend(bufSize);
+		}
+	}
+
+	//
+	// Handle a raw rectangle. The second form with paint==false is used
+	// by the Hextile decoder for raw-encoded tiles.
+	//
+
+	void handleRawRect(int x, int y, int w, int h) throws IOException {
+		handleRawRect(x, y, w, h, true);
+	}
+
+	void handleRawRect(int x, int y, int w, int h, boolean paint)
+			throws IOException {
+
+		if (bytesPixel == 1) {
+			for (int dy = y; dy < y + h; dy++) {
+				rfb.readFully(pixels8, dy * rfb.framebufferWidth + x, w);
+				if (rfb.rec != null) {
+					rfb.rec.write(pixels8, dy * rfb.framebufferWidth + x, w);
+				}
+			}
+		} else {
+			byte[] buf = new byte[w * 4];
+			int i, offset;
+			for (int dy = y; dy < y + h; dy++) {
+				rfb.readFully(buf);
+				if (rfb.rec != null) {
+					rfb.rec.write(buf);
+				}
+				/*
+				 * offset = dy * rfb.framebufferWidth + x; for (i = 0; i < w;
+				 * i++) { pixels24[offset + i] = (buf[i * 4 + 2] & 0xFF) << 16 |
+				 * (buf[i * 4 + 1] & 0xFF) << 8 | (buf[i * 4] & 0xFF); }
+				 */
+			}
+		}
+		/*
+		 * handleUpdatedPixels(x, y, w, h); if (paint) scheduleRepaint(x, y, w,
+		 * h);
+		 */
+	}
+
+	//
+	// Handle a CopyRect rectangle.
+	//
+
+	void handleCopyRect(int x, int y, int w, int h) throws IOException {
+
+		rfb.readCopyRect();
+		memGraphics.copyArea(rfb.copyRectSrcX, rfb.copyRectSrcY, w, h, x
+				- rfb.copyRectSrcX, y - rfb.copyRectSrcY);
+
+		scheduleRepaint(x, y, w, h);
+	}
+
+	//
+	// Handle an RRE-encoded rectangle.
+	//
+
+	void handleRRERect(int x, int y, int w, int h) throws IOException {
+
+		int nSubrects = rfb.readU32();
+
+		byte[] bg_buf = new byte[bytesPixel];
+		rfb.readFully(bg_buf);
+		Color pixel;
+		if (bytesPixel == 1) {
+			pixel = colors[bg_buf[0] & 0xFF];
+		} else {
+			pixel = new Color(bg_buf[2] & 0xFF, bg_buf[1] & 0xFF,
+					bg_buf[0] & 0xFF);
+		}
+		memGraphics.setColor(pixel);
+		memGraphics.fillRect(x, y, w, h);
+
+		byte[] buf = new byte[nSubrects * (bytesPixel + 8)];
+		rfb.readFully(buf);
+		DataInputStream ds = new DataInputStream(new ByteArrayInputStream(buf));
+
+		if (rfb.rec != null) {
+			rfb.rec.writeIntBE(nSubrects);
+			rfb.rec.write(bg_buf);
+			rfb.rec.write(buf);
+		}
+
+		int sx, sy, sw, sh;
+
+		for (int j = 0; j < nSubrects; j++) {
+			if (bytesPixel == 1) {
+				pixel = colors[ds.readUnsignedByte()];
+			} else {
+				ds.skip(4);
+				pixel = new Color(buf[j * 12 + 2] & 0xFF,
+						buf[j * 12 + 1] & 0xFF, buf[j * 12] & 0xFF);
+			}
+			sx = x + ds.readUnsignedShort();
+			sy = y + ds.readUnsignedShort();
+			sw = ds.readUnsignedShort();
+			sh = ds.readUnsignedShort();
+
+			memGraphics.setColor(pixel);
+			memGraphics.fillRect(sx, sy, sw, sh);
+		}
+
+		scheduleRepaint(x, y, w, h);
+	}
+
+	//
+	// Handle a CoRRE-encoded rectangle.
+	//
+
+	void handleCoRRERect(int x, int y, int w, int h) throws IOException {
+		int nSubrects = rfb.readU32();
+
+		byte[] bg_buf = new byte[bytesPixel];
+		rfb.readFully(bg_buf);
+		Color pixel;
+		if (bytesPixel == 1) {
+			pixel = colors[bg_buf[0] & 0xFF];
+		} else {
+			pixel = new Color(bg_buf[2] & 0xFF, bg_buf[1] & 0xFF,
+					bg_buf[0] & 0xFF);
+		}
+		memGraphics.setColor(pixel);
+		memGraphics.fillRect(x, y, w, h);
+
+		byte[] buf = new byte[nSubrects * (bytesPixel + 4)];
+		rfb.readFully(buf);
+
+		if (rfb.rec != null) {
+			rfb.rec.writeIntBE(nSubrects);
+			rfb.rec.write(bg_buf);
+			rfb.rec.write(buf);
+		}
+
+		int sx, sy, sw, sh;
+		int i = 0;
+
+		for (int j = 0; j < nSubrects; j++) {
+			if (bytesPixel == 1) {
+				pixel = colors[buf[i++] & 0xFF];
+			} else {
+				pixel = new Color(buf[i + 2] & 0xFF, buf[i + 1] & 0xFF,
+						buf[i] & 0xFF);
+				i += 4;
+			}
+			sx = x + (buf[i++] & 0xFF);
+			sy = y + (buf[i++] & 0xFF);
+			sw = buf[i++] & 0xFF;
+			sh = buf[i++] & 0xFF;
+
+			memGraphics.setColor(pixel);
+			memGraphics.fillRect(sx, sy, sw, sh);
+		}
+
+		scheduleRepaint(x, y, w, h);
+	}
+
+	//
+	// Handle a Hextile-encoded rectangle.
+	//
+
+	// These colors should be kept between handleHextileSubrect() calls.
+	private Color hextile_bg, hextile_fg;
+
+	void handleHextileRect(int x, int y, int w, int h) throws IOException {
+
+		hextile_bg = new Color(0);
+		hextile_fg = new Color(0);
+
+		for (int ty = y; ty < y + h; ty += 16) {
+			int th = 16;
+			if (y + h - ty < 16)
+				th = y + h - ty;
+
+			for (int tx = x; tx < x + w; tx += 16) {
+				int tw = 16;
+				if (x + w - tx < 16)
+					tw = x + w - tx;
+
+				handleHextileSubrect(tx, ty, tw, th);
+			}
+
+			// Finished with a row of tiles, now let's show it.
+			scheduleRepaint(x, y, w, h);
+		}
+	}
+
+	//
+	// Handle one tile in the Hextile-encoded data.
+	//
+
+	void handleHextileSubrect(int tx, int ty, int tw, int th)
+			throws IOException {
+
+		int subencoding = rfb.readU8();
+		if (rfb.rec != null) {
+			rfb.rec.writeByte(subencoding);
+		}
+
+		// Is it a raw-encoded sub-rectangle?
+		if ((subencoding & rfb.HextileRaw) != 0) {
+			handleRawRect(tx, ty, tw, th, false);
+			return;
+		}
+
+		// Read and draw the background if specified.
+		byte[] cbuf = new byte[bytesPixel];
+		if ((subencoding & rfb.HextileBackgroundSpecified) != 0) {
+			rfb.readFully(cbuf);
+			if (bytesPixel == 1) {
+				hextile_bg = colors[cbuf[0] & 0xFF];
+			} else {
+				hextile_bg = new Color(cbuf[2] & 0xFF, cbuf[1] & 0xFF,
+						cbuf[0] & 0xFF);
+			}
+			if (rfb.rec != null) {
+				rfb.rec.write(cbuf);
+			}
+		}
+		memGraphics.setColor(hextile_bg);
+		memGraphics.fillRect(tx, ty, tw, th);
+
+		// Read the foreground color if specified.
+		if ((subencoding & rfb.HextileForegroundSpecified) != 0) {
+			rfb.readFully(cbuf);
+			if (bytesPixel == 1) {
+				hextile_fg = colors[cbuf[0] & 0xFF];
+			} else {
+				hextile_fg = new Color(cbuf[2] & 0xFF, cbuf[1] & 0xFF,
+						cbuf[0] & 0xFF);
+			}
+			if (rfb.rec != null) {
+				rfb.rec.write(cbuf);
+			}
+		}
+
+		// Done with this tile if there is no sub-rectangles.
+		if ((subencoding & rfb.HextileAnySubrects) == 0)
+			return;
+
+		int nSubrects = rfb.readU8();
+		int bufsize = nSubrects * 2;
+		if ((subencoding & rfb.HextileSubrectsColoured) != 0) {
+			bufsize += nSubrects * bytesPixel;
+		}
+		byte[] buf = new byte[bufsize];
+		rfb.readFully(buf);
+		if (rfb.rec != null) {
+			rfb.rec.writeByte(nSubrects);
+			rfb.rec.write(buf);
+		}
+
+		int b1, b2, sx, sy, sw, sh;
+		int i = 0;
+
+		if ((subencoding & rfb.HextileSubrectsColoured) == 0) {
+
+			// Sub-rectangles are all of the same color.
+			memGraphics.setColor(hextile_fg);
+			for (int j = 0; j < nSubrects; j++) {
+				b1 = buf[i++] & 0xFF;
+				b2 = buf[i++] & 0xFF;
+				sx = tx + (b1 >> 4);
+				sy = ty + (b1 & 0xf);
+				sw = (b2 >> 4) + 1;
+				sh = (b2 & 0xf) + 1;
+				memGraphics.fillRect(sx, sy, sw, sh);
+			}
+		} else if (bytesPixel == 1) {
+
+			// BGR233 (8-bit color) version for colored sub-rectangles.
+			for (int j = 0; j < nSubrects; j++) {
+				hextile_fg = colors[buf[i++] & 0xFF];
+				b1 = buf[i++] & 0xFF;
+				b2 = buf[i++] & 0xFF;
+				sx = tx + (b1 >> 4);
+				sy = ty + (b1 & 0xf);
+				sw = (b2 >> 4) + 1;
+				sh = (b2 & 0xf) + 1;
+				memGraphics.setColor(hextile_fg);
+				memGraphics.fillRect(sx, sy, sw, sh);
+			}
+
+		} else {
+
+			// Full-color (24-bit) version for colored sub-rectangles.
+			for (int j = 0; j < nSubrects; j++) {
+				hextile_fg = new Color(buf[i + 2] & 0xFF, buf[i + 1] & 0xFF,
+						buf[i] & 0xFF);
+				i += 4;
+				b1 = buf[i++] & 0xFF;
+				b2 = buf[i++] & 0xFF;
+				sx = tx + (b1 >> 4);
+				sy = ty + (b1 & 0xf);
+				sw = (b2 >> 4) + 1;
+				sh = (b2 & 0xf) + 1;
+				memGraphics.setColor(hextile_fg);
+				memGraphics.fillRect(sx, sy, sw, sh);
+			}
+
+		}
+	}
+
+	//
+	// Handle a ZRLE-encoded rectangle.
+	//
+	// FIXME: Currently, session recording is not fully supported for ZRLE.
+	//
+
+	void handleZRLERect(int x, int y, int w, int h) throws Exception {
+
+		if (zrleInStream == null)
+			zrleInStream = new ZlibInStream();
+
+		int nBytes = rfb.readU32();
+		if (nBytes > 64 * 1024 * 1024)
+			throw new Exception("ZRLE decoder: illegal compressed data size");
+
+		if (zrleBuf == null || zrleBufLen < nBytes) {
+			zrleBufLen = nBytes + 4096;
+			zrleBuf = new byte[zrleBufLen];
+		}
+
+		// FIXME: Do not wait for all the data before decompression.
+		rfb.readFully(zrleBuf, 0, nBytes);
+
+		if (rfb.rec != null) {
+			if (rfb.recordFromBeginning) {
+				rfb.rec.writeIntBE(nBytes);
+				rfb.rec.write(zrleBuf, 0, nBytes);
+			} else if (!zrleRecWarningShown) {
+				System.out.println("Warning: ZRLE session can be recorded"
+						+ " only from the beginning");
+				System.out.println("Warning: Recorded file may be corrupted");
+				zrleRecWarningShown = true;
+			}
+
+		}
+
+		zrleInStream.setUnderlying(new MemInStream(zrleBuf, 0, nBytes), nBytes);
+
+		for (int ty = y; ty < y + h; ty += 64) {
+
+			int th = Math.min(y + h - ty, 64);
+
+			for (int tx = x; tx < x + w; tx += 64) {
+
+				int tw = Math.min(x + w - tx, 64);
+
+				int mode = zrleInStream.readU8();
+				boolean rle = (mode & 128) != 0;
+				int palSize = mode & 127;
+				int[] palette = new int[128];
+
+				readZrlePalette(palette, palSize);
+
+				if (palSize == 1) {
+					int pix = palette[0];
+					Color c = (bytesPixel == 1) ? colors[pix] : new Color(
+							0xFF000000 | pix);
+					memGraphics.setColor(c);
+					memGraphics.fillRect(tx, ty, tw, th);
+					continue;
+				}
+
+				if (!rle) {
+					if (palSize == 0) {
+						readZrleRawPixels(tw, th);
+					} else {
+						readZrlePackedPixels(tw, th, palette, palSize);
+					}
+				} else {
+					if (palSize == 0) {
+						readZrlePlainRLEPixels(tw, th);
+					} else {
+						readZrlePackedRLEPixels(tw, th, palette);
+					}
+				}
+				handleUpdatedZrleTile(tx, ty, tw, th);
+			}
+		}
+
+		zrleInStream.reset();
+
+		scheduleRepaint(x, y, w, h);
+	}
+
+	int readPixel(InStream is) throws Exception {
+		int pix;
+
+		if (bytesPixel == 1) {
+
+			pix = is.readU8();
+		} else {
+			int p1 = is.readU8();
+			int p2 = is.readU8();
+			int p3 = is.readU8();
+			pix = (p3 & 0xFF) << 16 | (p2 & 0xFF) << 8 | (p1 & 0xFF);
+		}
+		return pix;
+	}
+
+	void readPixels(InStream is, int[] dst, int count) throws Exception {
+		int pix;
+		if (bytesPixel == 1) {
+			byte[] buf = new byte[count];
+			is.readBytes(buf, 0, count);
+			for (int i = 0; i < count; i++) {
+				dst[i] = (int) buf[i] & 0xFF;
+			}
+		} else {
+			byte[] buf = new byte[count * 3];
+			is.readBytes(buf, 0, count * 3);
+			for (int i = 0; i < count; i++) {
+				dst[i] = ((buf[i * 3 + 2] & 0xFF) << 16
+						| (buf[i * 3 + 1] & 0xFF) << 8 | (buf[i * 3] & 0xFF));
+			}
+		}
+	}
+
+	void readZrlePalette(int[] palette, int palSize) throws Exception {
+		readPixels(zrleInStream, palette, palSize);
+	}
+
+	void readZrleRawPixels(int tw, int th) throws Exception {
+		if (bytesPixel == 1) {
+			zrleInStream.readBytes(zrleTilePixels8, 0, tw * th);
+		} else {
+			readPixels(zrleInStream, zrleTilePixels24, tw * th); // /
+		}
+	}
+
+	void readZrlePackedPixels(int tw, int th, int[] palette, int palSize)
+			throws Exception {
+
+		int bppp = ((palSize > 16) ? 8 : ((palSize > 4) ? 4
+				: ((palSize > 2) ? 2 : 1)));
+		int ptr = 0;
+
+		for (int i = 0; i < th; i++) {
+			int eol = ptr + tw;
+			int b = 0;
+			int nbits = 0;
+
+			while (ptr < eol) {
+				if (nbits == 0) {
+					b = zrleInStream.readU8();
+					nbits = 8;
+				}
+				nbits -= bppp;
+				int index = (b >> nbits) & ((1 << bppp) - 1) & 127;
+				if (bytesPixel == 1) {
+					zrleTilePixels8[ptr++] = (byte) palette[index];
+				} else {
+					zrleTilePixels24[ptr++] = palette[index];
+				}
+			}
+		}
+	}
+
+	void readZrlePlainRLEPixels(int tw, int th) throws Exception {
+		int ptr = 0;
+		int end = ptr + tw * th;
+		while (ptr < end) {
+			int pix = readPixel(zrleInStream);
+			int len = 1;
+			int b;
+			do {
+				b = zrleInStream.readU8();
+				len += b;
+			} while (b == 255);
+
+			if (!(len <= end - ptr))
+				throw new Exception("ZRLE decoder: assertion failed"
+						+ " (len <= end-ptr)");
+
+			if (bytesPixel == 1) {
+				while (len-- > 0)
+					zrleTilePixels8[ptr++] = (byte) pix;
+			} else {
+				while (len-- > 0)
+					zrleTilePixels24[ptr++] = pix;
+			}
+		}
+	}
+
+	void readZrlePackedRLEPixels(int tw, int th, int[] palette)
+			throws Exception {
+
+		int ptr = 0;
+		int end = ptr + tw * th;
+		while (ptr < end) {
+			int index = zrleInStream.readU8();
+			int len = 1;
+			if ((index & 128) != 0) {
+				int b;
+				do {
+					b = zrleInStream.readU8();
+					len += b;
+				} while (b == 255);
+
+				if (!(len <= end - ptr))
+					throw new Exception("ZRLE decoder: assertion failed"
+							+ " (len <= end - ptr)");
+			}
+
+			index &= 127;
+			int pix = palette[index];
+
+			if (bytesPixel == 1) {
+				while (len-- > 0)
+					zrleTilePixels8[ptr++] = (byte) pix;
+			} else {
+				while (len-- > 0)
+					zrleTilePixels24[ptr++] = pix;
+			}
+		}
+	}
+
+	//
+	// Copy pixels from zrleTilePixels8 or zrleTilePixels24, then update.
+	//
+
+	void handleUpdatedZrleTile(int x, int y, int w, int h) {
+		Object src, dst;
+		if (bytesPixel == 1) {
+			src = zrleTilePixels8;
+			dst = pixels8;
+		} else {
+			src = zrleTilePixels24;
+			dst = pixels24;
+		}
+		int offsetSrc = 0;
+		int offsetDst = (y * rfb.framebufferWidth + x);
+		for (int j = 0; j < h; j++) {
+			System.arraycopy(src, offsetSrc, dst, offsetDst, w);
+			offsetSrc += w;
+			offsetDst += rfb.framebufferWidth;
+		}
+		handleUpdatedPixels(x, y, w, h);
+	}
+
+	//
+	// Handle a Zlib-encoded rectangle.
+	//
+
+	void handleZlibRect(int x, int y, int w, int h) throws Exception {
+
+		int nBytes = rfb.readU32();
+
+		if (zlibBuf == null || zlibBufLen < nBytes) {
+			zlibBufLen = nBytes * 2;
+			zlibBuf = new byte[zlibBufLen];
+		}
+
+		rfb.readFully(zlibBuf, 0, nBytes);
+
+		if (rfb.rec != null && rfb.recordFromBeginning) {
+			rfb.rec.writeIntBE(nBytes);
+			rfb.rec.write(zlibBuf, 0, nBytes);
+		}
+
+		if (zlibInflater == null) {
+			zlibInflater = new Inflater();
+		}
+		zlibInflater.setInput(zlibBuf, 0, nBytes);
+
+		if (bytesPixel == 1) {
+			for (int dy = y; dy < y + h; dy++) {
+				zlibInflater.inflate(pixels8, dy * rfb.framebufferWidth + x, w);
+				if (rfb.rec != null && !rfb.recordFromBeginning)
+					rfb.rec.write(pixels8, dy * rfb.framebufferWidth + x, w);
+			}
+		} else {
+			byte[] buf = new byte[w * 4];
+			int i, offset;
+			for (int dy = y; dy < y + h; dy++) {
+				zlibInflater.inflate(buf);
+				offset = dy * rfb.framebufferWidth + x;
+				for (i = 0; i < w; i++) {
+					pixels24[offset + i] = (buf[i * 4 + 2] & 0xFF) << 16
+							| (buf[i * 4 + 1] & 0xFF) << 8
+							| (buf[i * 4] & 0xFF);
+				}
+				if (rfb.rec != null && !rfb.recordFromBeginning)
+					rfb.rec.write(buf);
+			}
+		}
+
+		handleUpdatedPixels(x, y, w, h);
+		scheduleRepaint(x, y, w, h);
+	}
+
+	//
+	// Handle a Tight-encoded rectangle.
+	//
+
+	void handleTightRect(int x, int y, int w, int h) throws Exception {
+
+		int comp_ctl = rfb.readU8();
+		if (rfb.rec != null) {
+			if (rfb.recordFromBeginning || comp_ctl == (rfb.TightFill << 4)
+					|| comp_ctl == (rfb.TightJpeg << 4)) {
+				// Send data exactly as received.
+				rfb.rec.writeByte(comp_ctl);
+			} else {
+				// Tell the decoder to flush each of the four zlib streams.
+				rfb.rec.writeByte(comp_ctl | 0x0F);
+			}
+		}
+
+		// Flush zlib streams if we are told by the server to do so.
+		for (int stream_id = 0; stream_id < 4; stream_id++) {
+			if ((comp_ctl & 1) != 0 && tightInflaters[stream_id] != null) {
+				tightInflaters[stream_id] = null;
+			}
+			comp_ctl >>= 1;
+		}
+
+		// Check correctness of subencoding value.
+		if (comp_ctl > rfb.TightMaxSubencoding) {
+			throw new Exception("Incorrect tight subencoding: " + comp_ctl);
+		}
+
+		// Handle solid-color rectangles.
+		if (comp_ctl == rfb.TightFill) {
+
+			if (bytesPixel == 1) {
+				int idx = rfb.readU8();
+				memGraphics.setColor(colors[idx]);
+				if (rfb.rec != null) {
+					rfb.rec.writeByte(idx);
+				}
+			} else {
+				byte[] buf = new byte[3];
+				rfb.readFully(buf);
+				if (rfb.rec != null) {
+					rfb.rec.write(buf);
+				}
+				Color bg = new Color(0xFF000000 | (buf[0] & 0xFF) << 16
+						| (buf[1] & 0xFF) << 8 | (buf[2] & 0xFF));
+				memGraphics.setColor(bg);
+			}
+			memGraphics.fillRect(x, y, w, h);
+			scheduleRepaint(x, y, w, h);
+			return;
+
+		}
+
+		if (comp_ctl == rfb.TightJpeg) {
+
+			statNumRectsTightJPEG++;
+
+			// Read JPEG data.
+			byte[] jpegData = new byte[rfb.readCompactLen()];
+			rfb.readFully(jpegData);
+			if (rfb.rec != null) {
+				if (!rfb.recordFromBeginning) {
+					rfb.recordCompactLen(jpegData.length);
+				}
+				rfb.rec.write(jpegData);
+			}
+
+			// Create an Image object from the JPEG data.
+			Image jpegImage = Toolkit.getDefaultToolkit().createImage(jpegData);
+
+			// Remember the rectangle where the image should be drawn.
+			jpegRect = new Rectangle(x, y, w, h);
+
+			// Let the imageUpdate() method do the actual drawing, here just
+			// wait until the image is fully loaded and drawn.
+			synchronized (jpegRect) {
+				Toolkit.getDefaultToolkit().prepareImage(jpegImage, -1, -1,
+						this);
+				try {
+					// Wait no longer than three seconds.
+					jpegRect.wait(3000);
+				} catch (InterruptedException e) {
+					throw new Exception("Interrupted while decoding JPEG image");
+				}
+			}
+
+			// Done, jpegRect is not needed any more.
+			jpegRect = null;
+			return;
+
+		}
+
+		// Read filter id and parameters.
+		int numColors = 0, rowSize = w;
+		byte[] palette8 = new byte[2];
+		int[] palette24 = new int[256];
+		boolean useGradient = false;
+		if ((comp_ctl & rfb.TightExplicitFilter) != 0) {
+			int filter_id = rfb.readU8();
+			if (rfb.rec != null) {
+				rfb.rec.writeByte(filter_id);
+			}
+			if (filter_id == rfb.TightFilterPalette) {
+				numColors = rfb.readU8() + 1;
+				if (rfb.rec != null) {
+					rfb.rec.writeByte(numColors - 1);
+				}
+				if (bytesPixel == 1) {
+					if (numColors != 2) {
+						throw new Exception("Incorrect tight palette size: "
+								+ numColors);
+					}
+					rfb.readFully(palette8);
+					if (rfb.rec != null) {
+						rfb.rec.write(palette8);
+					}
+				} else {
+					byte[] buf = new byte[numColors * 3];
+					rfb.readFully(buf);
+					if (rfb.rec != null) {
+						rfb.rec.write(buf);
+					}
+					for (int i = 0; i < numColors; i++) {
+						palette24[i] = ((buf[i * 3] & 0xFF) << 16
+								| (buf[i * 3 + 1] & 0xFF) << 8 | (buf[i * 3 + 2] & 0xFF));
+					}
+				}
+				if (numColors == 2)
+					rowSize = (w + 7) / 8;
+			} else if (filter_id == rfb.TightFilterGradient) {
+				useGradient = true;
+			} else if (filter_id != rfb.TightFilterCopy) {
+				throw new Exception("Incorrect tight filter id: " + filter_id);
+			}
+		}
+		if (numColors == 0 && bytesPixel == 4)
+			rowSize *= 3;
+
+		// Read, optionally uncompress and decode data.
+		int dataSize = h * rowSize;
+		if (dataSize < rfb.TightMinToCompress) {
+			// Data size is small - not compressed with zlib.
+			if (numColors != 0) {
+				// Indexed colors.
+				byte[] indexedData = new byte[dataSize];
+				rfb.readFully(indexedData);
+				if (rfb.rec != null) {
+					rfb.rec.write(indexedData);
+				}
+				if (numColors == 2) {
+					// Two colors.
+					if (bytesPixel == 1) {
+						decodeMonoData(x, y, w, h, indexedData, palette8);
+					} else {
+						decodeMonoData(x, y, w, h, indexedData, palette24);
+					}
+				} else {
+					// 3..255 colors (assuming bytesPixel == 4).
+					int i = 0;
+					for (int dy = y; dy < y + h; dy++) {
+						for (int dx = x; dx < x + w; dx++) {
+							pixels24[dy * rfb.framebufferWidth + dx] = palette24[indexedData[i++] & 0xFF];
+						}
+					}
+				}
+			} else if (useGradient) {
+				// "Gradient"-processed data
+				byte[] buf = new byte[w * h * 3];
+				rfb.readFully(buf);
+				if (rfb.rec != null) {
+					rfb.rec.write(buf);
+				}
+				decodeGradientData(x, y, w, h, buf);
+			} else {
+				// Raw truecolor data.
+				if (bytesPixel == 1) {
+					for (int dy = y; dy < y + h; dy++) {
+						rfb.readFully(pixels8, dy * rfb.framebufferWidth + x, w);
+						if (rfb.rec != null) {
+							rfb.rec.write(pixels8, dy * rfb.framebufferWidth
+									+ x, w);
+						}
+					}
+				} else {
+					byte[] buf = new byte[w * 3];
+					int i, offset;
+					for (int dy = y; dy < y + h; dy++) {
+						rfb.readFully(buf);
+						if (rfb.rec != null) {
+							rfb.rec.write(buf);
+						}
+						offset = dy * rfb.framebufferWidth + x;
+						for (i = 0; i < w; i++) {
+							pixels24[offset + i] = (buf[i * 3] & 0xFF) << 16
+									| (buf[i * 3 + 1] & 0xFF) << 8
+									| (buf[i * 3 + 2] & 0xFF);
+						}
+					}
+				}
+			}
+		} else {
+			// Data was compressed with zlib.
+			int zlibDataLen = rfb.readCompactLen();
+			byte[] zlibData = new byte[zlibDataLen];
+			rfb.readFully(zlibData);
+			if (rfb.rec != null && rfb.recordFromBeginning) {
+				rfb.rec.write(zlibData);
+			}
+			int stream_id = comp_ctl & 0x03;
+			if (tightInflaters[stream_id] == null) {
+				tightInflaters[stream_id] = new Inflater();
+			}
+			Inflater myInflater = tightInflaters[stream_id];
+			myInflater.setInput(zlibData);
+			byte[] buf = new byte[dataSize];
+			myInflater.inflate(buf);
+			if (rfb.rec != null && !rfb.recordFromBeginning) {
+				rfb.recordCompressedData(buf);
+			}
+
+			if (numColors != 0) {
+				// Indexed colors.
+				if (numColors == 2) {
+					// Two colors.
+					if (bytesPixel == 1) {
+						decodeMonoData(x, y, w, h, buf, palette8);
+					} else {
+						decodeMonoData(x, y, w, h, buf, palette24);
+					}
+				} else {
+					// More than two colors (assuming bytesPixel == 4).
+					int i = 0;
+					for (int dy = y; dy < y + h; dy++) {
+						for (int dx = x; dx < x + w; dx++) {
+							pixels24[dy * rfb.framebufferWidth + dx] = palette24[buf[i++] & 0xFF];
+						}
+					}
+				}
+			} else if (useGradient) {
+				// Compressed "Gradient"-filtered data (assuming bytesPixel ==
+				// 4).
+				decodeGradientData(x, y, w, h, buf);
+			} else {
+				// Compressed truecolor data.
+				if (bytesPixel == 1) {
+					int destOffset = y * rfb.framebufferWidth + x;
+					for (int dy = 0; dy < h; dy++) {
+						System.arraycopy(buf, dy * w, pixels8, destOffset, w);
+						destOffset += rfb.framebufferWidth;
+					}
+				} else {
+					int srcOffset = 0;
+					int destOffset, i;
+					for (int dy = 0; dy < h; dy++) {
+						myInflater.inflate(buf);
+						destOffset = (y + dy) * rfb.framebufferWidth + x;
+						for (i = 0; i < w; i++) {
+							pixels24[destOffset + i] = (buf[srcOffset] & 0xFF) << 16
+									| (buf[srcOffset + 1] & 0xFF) << 8
+									| (buf[srcOffset + 2] & 0xFF);
+							srcOffset += 3;
+						}
+					}
+				}
+			}
+		}
+
+		handleUpdatedPixels(x, y, w, h);
+		scheduleRepaint(x, y, w, h);
+	}
+
+	//
+	// Decode 1bpp-encoded bi-color rectangle (8-bit and 24-bit versions).
+	//
+
+	void decodeMonoData(int x, int y, int w, int h, byte[] src, byte[] palette) {
+
+		int dx, dy, n;
+		int i = y * rfb.framebufferWidth + x;
+		int rowBytes = (w + 7) / 8;
+		byte b;
+
+		for (dy = 0; dy < h; dy++) {
+			for (dx = 0; dx < w / 8; dx++) {
+				b = src[dy * rowBytes + dx];
+				for (n = 7; n >= 0; n--)
+					pixels8[i++] = palette[b >> n & 1];
+			}
+			for (n = 7; n >= 8 - w % 8; n--) {
+				pixels8[i++] = palette[src[dy * rowBytes + dx] >> n & 1];
+			}
+			i += (rfb.framebufferWidth - w);
+		}
+	}
+
+	void decodeMonoData(int x, int y, int w, int h, byte[] src, int[] palette) {
+
+		int dx, dy, n;
+		int i = y * rfb.framebufferWidth + x;
+		int rowBytes = (w + 7) / 8;
+		byte b;
+
+		for (dy = 0; dy < h; dy++) {
+			for (dx = 0; dx < w / 8; dx++) {
+				b = src[dy * rowBytes + dx];
+				for (n = 7; n >= 0; n--)
+					pixels24[i++] = palette[b >> n & 1];
+			}
+			for (n = 7; n >= 8 - w % 8; n--) {
+				pixels24[i++] = palette[src[dy * rowBytes + dx] >> n & 1];
+			}
+			i += (rfb.framebufferWidth - w);
+		}
+	}
+
+	//
+	// Decode data processed with the "Gradient" filter.
+	//
+
+	void decodeGradientData(int x, int y, int w, int h, byte[] buf) {
+
+		int dx, dy, c;
+		byte[] prevRow = new byte[w * 3];
+		byte[] thisRow = new byte[w * 3];
+		byte[] pix = new byte[3];
+		int[] est = new int[3];
+
+		int offset = y * rfb.framebufferWidth + x;
+
+		for (dy = 0; dy < h; dy++) {
+
+			/* First pixel in a row */
+			for (c = 0; c < 3; c++) {
+				pix[c] = (byte) (prevRow[c] + buf[dy * w * 3 + c]);
+				thisRow[c] = pix[c];
+			}
+			pixels24[offset++] = (pix[0] & 0xFF) << 16 | (pix[1] & 0xFF) << 8
+					| (pix[2] & 0xFF);
+
+			/* Remaining pixels of a row */
+			for (dx = 1; dx < w; dx++) {
+				for (c = 0; c < 3; c++) {
+					est[c] = ((prevRow[dx * 3 + c] & 0xFF) + (pix[c] & 0xFF) - (prevRow[(dx - 1)
+							* 3 + c] & 0xFF));
+					if (est[c] > 0xFF) {
+						est[c] = 0xFF;
+					} else if (est[c] < 0x00) {
+						est[c] = 0x00;
+					}
+					pix[c] = (byte) (est[c] + buf[(dy * w + dx) * 3 + c]);
+					thisRow[dx * 3 + c] = pix[c];
+				}
+				pixels24[offset++] = (pix[0] & 0xFF) << 16
+						| (pix[1] & 0xFF) << 8 | (pix[2] & 0xFF);
+			}
+
+			System.arraycopy(thisRow, 0, prevRow, 0, w * 3);
+			offset += (rfb.framebufferWidth - w);
+		}
+	}
+
+	//
+	// Display newly updated area of pixels.
+	//
+
+	void handleUpdatedPixels(int x, int y, int w, int h) {
+
+		// Draw updated pixels of the off-screen image.
+		pixelsSource.newPixels(x, y, w, h);
+		memGraphics.setClip(x, y, w, h);
+		memGraphics.drawImage(rawPixelsImage, 0, 0, null);
+		memGraphics.setClip(0, 0, rfb.framebufferWidth, rfb.framebufferHeight);
+	}
+
+	//
+	// Tell JVM to repaint specified desktop area.
+	//
+
+	void scheduleRepaint(int x, int y, int w, int h) {
+		// Request repaint, deferred if necessary.
+		if (rfb.framebufferWidth == scaledWidth) {
+			repaint(viewer.deferScreenUpdates, x, y, w, h);
+		} else {
+			int sx = x * scalingFactor / 100;
+			int sy = y * scalingFactor / 100;
+			int sw = ((x + w) * scalingFactor + 49) / 100 - sx + 1;
+			int sh = ((y + h) * scalingFactor + 49) / 100 - sy + 1;
+			repaint(viewer.deferScreenUpdates, sx, sy, sw, sh);
+		}
+	}
+
+	//
+	// Handle events.
+	//
+
+	public void keyPressed(KeyEvent evt) {
+		processLocalKeyEvent(evt);
+	}
+
+	public void keyReleased(KeyEvent evt) {
+		processLocalKeyEvent(evt);
+	}
+
+	public void keyTyped(KeyEvent evt) {
+		evt.consume();
+	}
+
+	public void mousePressed(MouseEvent evt) {
+		processLocalMouseEvent(evt, false);
+	}
+
+	public void mouseReleased(MouseEvent evt) {
+		processLocalMouseEvent(evt, false);
+	}
+
+	public void mouseMoved(MouseEvent evt) {
+		processLocalMouseEvent(evt, true);
+	}
+
+	public void mouseDragged(MouseEvent evt) {
+		processLocalMouseEvent(evt, true);
+	}
+
+	public void processLocalKeyEvent(KeyEvent evt) {
+		if (viewer.rfb != null && rfb.inNormalProtocol) {
+			if (!inputEnabled) {
+				if ((evt.getKeyChar() == 'r' || evt.getKeyChar() == 'R')
+						&& evt.getID() == KeyEvent.KEY_PRESSED) {
+					// Request screen update.
+					try {
+						rfb.writeFramebufferUpdateRequest(0, 0,
+								rfb.framebufferWidth, rfb.framebufferHeight,
+								false);
+					} catch (IOException e) {
+						e.printStackTrace();
+					}
+				}
+			} else {
+				// Input enabled.
+				synchronized (rfb) {
+					try {
+						rfb.writeKeyEvent(evt);
+					} catch (Exception e) {
+						e.printStackTrace();
+					}
+					rfb.notify();
+				}
+			}
+		}
+		// Don't ever pass keyboard events to AWT for default processing.
+		// Otherwise, pressing Tab would switch focus to ButtonPanel etc.
+		evt.consume();
+	}
+
+	public void processLocalMouseEvent(MouseEvent evt, boolean moved) {
+		if (viewer.rfb != null && rfb.inNormalProtocol) {
+			if (moved) {
+				softCursorMove(evt.getX(), evt.getY());
+			}
+			if (rfb.framebufferWidth != scaledWidth) {
+				int sx = (evt.getX() * 100 + scalingFactor / 2) / scalingFactor;
+				int sy = (evt.getY() * 100 + scalingFactor / 2) / scalingFactor;
+				evt.translatePoint(sx - evt.getX(), sy - evt.getY());
+			}
+			synchronized (rfb) {
+				try {
+					rfb.writePointerEvent(evt);
+				} catch (Exception e) {
+					e.printStackTrace();
+				}
+				rfb.notify();
+			}
+		}
+	}
+
+	//
+	// Ignored events.
+	//
+
+	public void mouseClicked(MouseEvent evt) {
+	}
+
+	public void mouseEntered(MouseEvent evt) {
+	}
+
+	public void mouseExited(MouseEvent evt) {
+	}
+
+	//
+	// Reset update statistics.
+	//
+
+	void resetStats() {
+		statStartTime = System.currentTimeMillis();
+		statNumUpdates = 0;
+		statNumTotalRects = 0;
+		statNumPixelRects = 0;
+		statNumRectsTight = 0;
+		statNumRectsTightJPEG = 0;
+		statNumRectsZRLE = 0;
+		statNumRectsHextile = 0;
+		statNumRectsRaw = 0;
+		statNumRectsCopy = 0;
+		statNumBytesEncoded = 0;
+		statNumBytesDecoded = 0;
+	}
+
+	// ////////////////////////////////////////////////////////////////
+	//
+	// Handle cursor shape updates (XCursor and RichCursor encodings).
+	//
+
+	boolean showSoftCursor = false;
+
+	MemoryImageSource softCursorSource;
+	Image softCursor;
+
+	int cursorX = 0, cursorY = 0;
+	int cursorWidth, cursorHeight;
+	int origCursorWidth, origCursorHeight;
+	int hotX, hotY;
+	int origHotX, origHotY;
+
+	//
+	// Handle cursor shape update (XCursor and RichCursor encodings).
+	//
+
+	synchronized void handleCursorShapeUpdate(int encodingType, int xhot,
+			int yhot, int width, int height) throws IOException {
+
+		softCursorFree();
+
+		if (width * height == 0)
+			return;
+
+		// Ignore cursor shape data if requested by user.
+		if (viewer.options.ignoreCursorUpdates) {
+			int bytesPerRow = (width + 7) / 8;
+			int bytesMaskData = bytesPerRow * height;
+
+			if (encodingType == rfb.EncodingXCursor) {
+				rfb.skipBytes(6 + bytesMaskData * 2);
+			} else {
+				// rfb.EncodingRichCursor
+				rfb.skipBytes(width * height * bytesPixel + bytesMaskData);
+			}
+			return;
+		}
+
+		// Decode cursor pixel data.
+		softCursorSource = decodeCursorShape(encodingType, width, height);
+
+		// Set original (non-scaled) cursor dimensions.
+		origCursorWidth = width;
+		origCursorHeight = height;
+		origHotX = xhot;
+		origHotY = yhot;
+
+		// Create off-screen cursor image.
+		createSoftCursor();
+
+		// Show the cursor.
+		showSoftCursor = true;
+		repaint(viewer.deferCursorUpdates, cursorX - hotX, cursorY - hotY,
+				cursorWidth, cursorHeight);
+	}
+
+	//
+	// decodeCursorShape(). Decode cursor pixel data and return
+	// corresponding MemoryImageSource instance.
+	//
+
+	synchronized MemoryImageSource decodeCursorShape(int encodingType,
+			int width, int height) throws IOException {
+
+		int bytesPerRow = (width + 7) / 8;
+		int bytesMaskData = bytesPerRow * height;
+
+		int[] softCursorPixels = new int[width * height];
+
+		if (encodingType == rfb.EncodingXCursor) {
+
+			// Read foreground and background colors of the cursor.
+			byte[] rgb = new byte[6];
+			rfb.readFully(rgb);
+			int[] colors = {
+					(0xFF000000 | (rgb[3] & 0xFF) << 16 | (rgb[4] & 0xFF) << 8 | (rgb[5] & 0xFF)),
+					(0xFF000000 | (rgb[0] & 0xFF) << 16 | (rgb[1] & 0xFF) << 8 | (rgb[2] & 0xFF)) };
+
+			// Read pixel and mask data.
+			byte[] pixBuf = new byte[bytesMaskData];
+			rfb.readFully(pixBuf);
+			byte[] maskBuf = new byte[bytesMaskData];
+			rfb.readFully(maskBuf);
+
+			// Decode pixel data into softCursorPixels[].
+			byte pixByte, maskByte;
+			int x, y, n, result;
+			int i = 0;
+			for (y = 0; y < height; y++) {
+				for (x = 0; x < width / 8; x++) {
+					pixByte = pixBuf[y * bytesPerRow + x];
+					maskByte = maskBuf[y * bytesPerRow + x];
+					for (n = 7; n >= 0; n--) {
+						if ((maskByte >> n & 1) != 0) {
+							result = colors[pixByte >> n & 1];
+						} else {
+							result = 0; // Transparent pixel
+						}
+						softCursorPixels[i++] = result;
+					}
+				}
+				for (n = 7; n >= 8 - width % 8; n--) {
+					if ((maskBuf[y * bytesPerRow + x] >> n & 1) != 0) {
+						result = colors[pixBuf[y * bytesPerRow + x] >> n & 1];
+					} else {
+						result = 0; // Transparent pixel
+					}
+					softCursorPixels[i++] = result;
+				}
+			}
+
+		} else {
+			// encodingType == rfb.EncodingRichCursor
+
+			// Read pixel and mask data.
+			byte[] pixBuf = new byte[width * height * bytesPixel];
+			rfb.readFully(pixBuf);
+			byte[] maskBuf = new byte[bytesMaskData];
+			rfb.readFully(maskBuf);
+
+			// Decode pixel data into softCursorPixels[].
+			byte pixByte, maskByte;
+			int x, y, n, result;
+			int i = 0;
+			for (y = 0; y < height; y++) {
+				for (x = 0; x < width / 8; x++) {
+					maskByte = maskBuf[y * bytesPerRow + x];
+					for (n = 7; n >= 0; n--) {
+						if ((maskByte >> n & 1) != 0) {
+							if (bytesPixel == 1) {
+								result = cm8.getRGB(pixBuf[i]);
+							} else {
+								result = 0xFF000000
+										| (pixBuf[i * 4 + 2] & 0xFF) << 16
+										| (pixBuf[i * 4 + 1] & 0xFF) << 8
+										| (pixBuf[i * 4] & 0xFF);
+							}
+						} else {
+							result = 0; // Transparent pixel
+						}
+						softCursorPixels[i++] = result;
+					}
+				}
+				for (n = 7; n >= 8 - width % 8; n--) {
+					if ((maskBuf[y * bytesPerRow + x] >> n & 1) != 0) {
+						if (bytesPixel == 1) {
+							result = cm8.getRGB(pixBuf[i]);
+						} else {
+							result = 0xFF000000
+									| (pixBuf[i * 4 + 2] & 0xFF) << 16
+									| (pixBuf[i * 4 + 1] & 0xFF) << 8
+									| (pixBuf[i * 4] & 0xFF);
+						}
+					} else {
+						result = 0; // Transparent pixel
+					}
+					softCursorPixels[i++] = result;
+				}
+			}
+
+		}
+
+		return new MemoryImageSource(width, height, softCursorPixels, 0, width);
+	}
+
+	//
+	// createSoftCursor(). Assign softCursor new Image (scaled if necessary).
+	// Uses softCursorSource as a source for new cursor image.
+	//
+
+	synchronized void createSoftCursor() {
+
+		if (softCursorSource == null)
+			return;
+
+		int scaleCursor = viewer.options.scaleCursor;
+		if (scaleCursor == 0 || !inputEnabled)
+			scaleCursor = 100;
+
+		// Save original cursor coordinates.
+		int x = cursorX - hotX;
+		int y = cursorY - hotY;
+		int w = cursorWidth;
+		int h = cursorHeight;
+
+		cursorWidth = (origCursorWidth * scaleCursor + 50) / 100;
+		cursorHeight = (origCursorHeight * scaleCursor + 50) / 100;
+		hotX = (origHotX * scaleCursor + 50) / 100;
+		hotY = (origHotY * scaleCursor + 50) / 100;
+		softCursor = Toolkit.getDefaultToolkit().createImage(softCursorSource);
+
+		if (scaleCursor != 100) {
+			softCursor = softCursor.getScaledInstance(cursorWidth,
+					cursorHeight, Image.SCALE_SMOOTH);
+		}
+
+		if (showSoftCursor) {
+			// Compute screen area to update.
+			x = Math.min(x, cursorX - hotX);
+			y = Math.min(y, cursorY - hotY);
+			w = Math.max(w, cursorWidth);
+			h = Math.max(h, cursorHeight);
+
+			repaint(viewer.deferCursorUpdates, x, y, w, h);
+		}
+	}
+
+	//
+	// softCursorMove(). Moves soft cursor into a particular location.
+	//
+
+	synchronized void softCursorMove(int x, int y) {
+		int oldX = cursorX;
+		int oldY = cursorY;
+		cursorX = x;
+		cursorY = y;
+		if (showSoftCursor) {
+			repaint(viewer.deferCursorUpdates, oldX - hotX, oldY - hotY,
+					cursorWidth, cursorHeight);
+			repaint(viewer.deferCursorUpdates, cursorX - hotX, cursorY - hotY,
+					cursorWidth, cursorHeight);
+		}
+	}
+
+	//
+	// softCursorFree(). Remove soft cursor, dispose resources.
+	//
+
+	synchronized void softCursorFree() {
+		if (showSoftCursor) {
+			showSoftCursor = false;
+			softCursor = null;
+			softCursorSource = null;
+
+			repaint(viewer.deferCursorUpdates, cursorX - hotX, cursorY - hotY,
+					cursorWidth, cursorHeight);
+		}
+	}
+}