| // |
| // Copyright (C) 2004 Horizon Wimba. All Rights Reserved. |
| // Copyright (C) 2001-2003 HorizonLive.com, Inc. All Rights Reserved. |
| // Copyright (C) 2001 Constantin Kaplinsky. All Rights Reserved. |
| // Copyright (C) 2000 Tridia Corporation. All Rights Reserved. |
| // Copyright (C) 1999 AT&T Laboratories Cambridge. All Rights Reserved. |
| // |
| // This is free software; you can redistribute it and/or modify |
| // it under the terms of the GNU General Public License as published by |
| // the Free Software Foundation; either version 2 of the License, or |
| // (at your option) any later version. |
| // |
| // This software is distributed in the hope that it will be useful, |
| // but WITHOUT ANY WARRANTY; without even the implied warranty of |
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| // GNU General Public License for more details. |
| // |
| // You should have received a copy of the GNU General Public License |
| // along with this software; if not, write to the Free Software |
| // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, |
| // USA. |
| // |
| |
| package com.tightvnc.rfbplayer; |
| |
| import java.awt.*; |
| import java.awt.image.*; |
| import java.io.*; |
| import java.util.*; |
| import java.util.zip.*; |
| |
| |
| // |
| // VncCanvas is a subclass of Canvas which draws a VNC desktop on it. |
| // |
| class VncCanvas extends Component |
| implements Observer { |
| |
| RfbPlayer appClass; |
| RfbProto rfb; |
| ColorModel cm8, cm24; |
| int bytesPixel; |
| |
| Image memImage; |
| Graphics memGraphics; |
| |
| Image offImage; |
| Graphics offGraphics; |
| |
| Image rawPixelsImage; |
| MemoryImageSource pixelsSource; |
| byte[] pixels8; |
| int[] pixels24; |
| |
| // 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; |
| |
| // When we're in the seeking mode, we should not update the desktop. |
| // This variable helps us to remember that repainting the desktop at |
| // once is necessary when the seek operation is finished. |
| boolean seekMode; |
| |
| // Distance of mouse from viewer border to trigger automatic scrolling |
| final static int SCROLL_MARGIN = 50; |
| |
| // Color for border around small shared area |
| static final Color DARK_GRAY = new Color(132, 138, 156); |
| |
| // |
| // The constructor. |
| // |
| VncCanvas(RfbPlayer player) throws IOException { |
| this.appClass = player; |
| rfb = appClass.rfb; |
| seekMode = false; |
| |
| cm24 = new DirectColorModel(24, 0xFF0000, 0x00FF00, 0x0000FF); |
| |
| setPixelFormat(); |
| |
| // Style canvas. |
| setBackground(Color.white); |
| |
| } |
| |
| // |
| // Override the 1.4 focus traversal keys accesor to allow |
| // our keyListener to receive tab presses. |
| // |
| public boolean getFocusTraversalKeysEnabled() { |
| return false; |
| } |
| |
| // |
| // Callback methods to determine geometry of our Component. |
| // |
| public Dimension getPreferredSize() { |
| Dimension d = appClass.desktopScrollPane.getViewportSize(); |
| Dimension sz = new Dimension(rfb.framebufferWidth, rfb.framebufferHeight); |
| if (d.width > sz.width) |
| sz.width = d.width; |
| if (d.height > sz.height) |
| sz.height = d.height; |
| return sz; |
| } |
| |
| public Dimension getMinimumSize() { |
| return getPreferredSize(); |
| } |
| |
| public Dimension getMaximumSize() { |
| return getPreferredSize(); |
| } |
| |
| Point getImageOrigin() { |
| int x = 0, y = 0; |
| Dimension d = appClass.desktopScrollPane.getViewportSize(); |
| if (rfb.framebufferWidth < d.width) |
| x = d.width / 2 - rfb.framebufferWidth / 2; |
| if (rfb.framebufferHeight < d.height) |
| y = d.height / 2 - rfb.framebufferHeight / 2; |
| return new Point(x, y); |
| } |
| |
| // |
| // All painting is performed here. |
| // |
| public void update(Graphics g) { |
| paint(g); |
| } |
| |
| public void paint(Graphics g) { |
| Point o = getImageOrigin(); |
| Dimension d = getPreferredSize(); |
| |
| // create new offscreen |
| if (offImage == null) { |
| offImage = createImage(d.width, d.height); |
| offGraphics = offImage.getGraphics(); |
| } else if (offImage.getWidth(null) != d.width || |
| offImage.getHeight(null) != d.height) { |
| offGraphics.dispose(); |
| offGraphics = null; |
| offImage.flush(); |
| offImage = null; |
| offImage = createImage(d.width, d.height); |
| offGraphics = offImage.getGraphics(); |
| } |
| |
| synchronized(memImage) { |
| offGraphics.drawImage(memImage, o.x, o.y, null); |
| } |
| |
| // fill in background |
| if (o.x > 0 || o.y > 0) { |
| // left, right, top, bottom |
| Color c = g.getColor(); |
| offGraphics.setColor(Color.white); |
| offGraphics.fillRect(0, 0, o.x, d.height); |
| offGraphics.fillRect(o.x + rfb.framebufferWidth, 0, d.width - (o.x + |
| rfb.framebufferWidth), d.height); |
| offGraphics.fillRect(o.x, 0, rfb.framebufferWidth, o.y); |
| offGraphics.fillRect(o.x, o.y + rfb.framebufferHeight, |
| rfb.framebufferWidth, d.height - (o.y + |
| rfb.framebufferHeight)); |
| offGraphics.setColor(c); |
| } |
| |
| // draw border |
| Color oc = g.getColor(); |
| offGraphics.setColor(DARK_GRAY); |
| offGraphics.drawRect(o.x - 1, o.y - 1, rfb.framebufferWidth + 1, |
| rfb.framebufferHeight + 1); |
| offGraphics.setColor(oc); |
| |
| // draw cursor |
| if (showSoftCursor) { |
| int x0 = cursorX - hotX, y0 = cursorY - hotY; |
| x0 += o.x; |
| y0 += o.y; |
| Rectangle r = new Rectangle(x0, y0, cursorWidth, cursorHeight); |
| if (r.intersects(g.getClipBounds())) { |
| offGraphics.setClip(o.x, o.y, rfb.framebufferWidth, |
| rfb.framebufferHeight); |
| offGraphics.drawImage(softCursor, x0, y0, null); |
| offGraphics.setClip(null); |
| } |
| } |
| |
| // draw offscreen |
| g.drawImage(offImage, 0, 0, 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. |
| } |
| } |
| |
| private void setPixelFormat() throws IOException { |
| bytesPixel = 4; |
| updateFramebufferSize(); |
| } |
| |
| void updateFramebufferSize() { |
| |
| // Useful shortcuts. |
| int fbWidth = rfb.framebufferWidth; |
| int fbHeight = rfb.framebufferHeight; |
| |
| // 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 = appClass.createImage(fbWidth, fbHeight); |
| memGraphics = memImage.getGraphics(); |
| } else if (memImage.getWidth(null) != fbWidth || |
| memImage.getHeight(null) != fbHeight) { |
| synchronized(memImage) { |
| memImage = appClass.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); |
| } else { |
| pixels8 = null; |
| pixels24 = new int[fbWidth * fbHeight]; |
| |
| pixelsSource = |
| new MemoryImageSource(fbWidth, fbHeight, cm24, pixels24, 0, fbWidth); |
| } |
| pixelsSource.setAnimated(true); |
| rawPixelsImage = createImage(pixelsSource); |
| |
| // Update the size of desktop containers unless seeking |
| if (!seekMode && appClass.desktopScrollPane != null) { |
| if (appClass.inSeparateFrame) { |
| resizeDesktopFrame(); |
| } else { |
| resizeEmbeddedApplet(); |
| // if the framebuffer shrunk, need to clear and repaint |
| appClass.desktopScrollPane.clearAndRepaint(); |
| } |
| } |
| } |
| |
| void resizeDesktopFrame() { |
| // determine screen size |
| Dimension screenSize = appClass.vncFrame.getToolkit().getScreenSize(); |
| Dimension scrollSize = appClass.desktopScrollPane.getSize(); |
| |
| // 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; |
| |
| // Further reduce the screen size to account for the |
| // scroll pane's insets. |
| Insets insets = appClass.desktopScrollPane.getInsets(); |
| screenSize.height -= insets.top + insets.bottom; |
| screenSize.width -= insets.left + insets.right; |
| |
| // Limit pane size to fit on screen. |
| boolean needResize = false; |
| if (scrollSize.width != rfb.framebufferWidth || |
| scrollSize.height != rfb.framebufferHeight) |
| needResize = true; |
| int w = rfb.framebufferWidth, h = rfb.framebufferHeight; |
| if (w > screenSize.width) { |
| w = screenSize.width; |
| needResize = true; |
| } |
| if (h > screenSize.height) { |
| h = screenSize.height; |
| needResize = true; |
| } |
| if (needResize) { |
| appClass.desktopScrollPane.setSize(w, h); |
| appClass.desktopScrollPane.validate(); |
| } |
| |
| // size the canvas |
| setSize(getPreferredSize()); |
| |
| appClass.vncFrame.pack(); |
| } |
| |
| void resizeEmbeddedApplet() { |
| // resize scroll pane if necessary |
| Dimension scrollSize = appClass.desktopScrollPane.getSize(); |
| if (scrollSize.width != appClass.dispW || |
| scrollSize.height != appClass.dispH) { |
| appClass.desktopScrollPane.setSize(appClass.dispW, appClass.dispH); |
| } |
| appClass.desktopScrollPane.validate(); |
| // size the canvas |
| setSize(getPreferredSize()); |
| } |
| |
| // |
| // processNormalProtocol() - executed by the rfbThread to deal with |
| // the RFB data. |
| // |
| public void processNormalProtocol() throws Exception { |
| |
| zlibInflater = new Inflater(); |
| tightInflaters = new Inflater[4]; |
| |
| // Show current time position in the control panel. |
| appClass.updatePos(); |
| |
| // Tell our FbsInputStream object to notify us when it goes to the |
| // `paused' mode. |
| appClass.fbs.addObserver(this); |
| |
| // |
| // main dispatch loop |
| // |
| |
| while (true) { |
| |
| int msgType = rfb.readServerMessageType(); |
| |
| // Process the message depending on its type. |
| switch (msgType) { |
| case RfbProto.FramebufferUpdate: |
| rfb.readFramebufferUpdate(); |
| |
| for (int i = 0; i < rfb.updateNRects; i++) { |
| rfb.readFramebufferUpdateRectHdr(); |
| 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) { |
| if (rw != 0 && rh != 0) { |
| 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); |
| continue; |
| } |
| |
| switch (rfb.updateRectEncoding) { |
| case RfbProto.EncodingRaw: |
| handleRawRect(rx, ry, rw, rh); |
| break; |
| case RfbProto.EncodingCopyRect: |
| 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: |
| handleHextileRect(rx, ry, rw, rh); |
| break; |
| case RfbProto.EncodingZlib: |
| handleZlibRect(rx, ry, rw, rh); |
| break; |
| case RfbProto.EncodingTight: |
| handleTightRect(rx, ry, rw, rh); |
| break; |
| default: |
| throw new Exception("Unknown RFB rectangle encoding " + |
| Integer.toString(rfb.updateRectEncoding, 16)); |
| } |
| } |
| break; |
| |
| case RfbProto.SetColourMapEntries: |
| throw new Exception("Can't handle SetColourMapEntries message"); |
| |
| case RfbProto.Bell: |
| Toolkit.getDefaultToolkit().beep(); |
| break; |
| |
| case RfbProto.ServerCutText: |
| rfb.readServerCutText(); |
| break; |
| |
| default: |
| throw new Exception("Unknown RFB message type " + msgType); |
| } |
| |
| appClass.updatePos(); |
| } |
| } |
| |
| |
| // |
| // 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.is.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.is.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.is.readInt(); |
| int sx, sy, sw, sh; |
| |
| byte[] buf = new byte[4]; |
| rfb.is.readFully(buf); |
| Color pixel = new Color(buf[2] & 0xFF, buf[1] & 0xFF, buf[0] & 0xFF); |
| memGraphics.setColor(pixel); |
| memGraphics.fillRect(x, y, w, h); |
| |
| for (int j = 0; j < nSubrects; j++) { |
| rfb.is.readFully(buf); |
| pixel = new Color(buf[2] & 0xFF, buf[1] & 0xFF, buf[0] & 0xFF); |
| sx = x + rfb.is.readUnsignedShort(); |
| sy = y + rfb.is.readUnsignedShort(); |
| sw = rfb.is.readUnsignedShort(); |
| sh = rfb.is.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.is.readInt(); |
| int sx, sy, sw, sh; |
| |
| byte[] buf = new byte[4]; |
| rfb.is.readFully(buf); |
| Color pixel = new Color(buf[2] & 0xFF, buf[1] & 0xFF, buf[0] & 0xFF); |
| memGraphics.setColor(pixel); |
| memGraphics.fillRect(x, y, w, h); |
| |
| for (int j = 0; j < nSubrects; j++) { |
| rfb.is.readFully(buf); |
| pixel = new Color(buf[2] & 0xFF, buf[1] & 0xFF, buf[0] & 0xFF); |
| sx = x + rfb.is.readUnsignedByte(); |
| sy = y + rfb.is.readUnsignedByte(); |
| sw = rfb.is.readUnsignedByte(); |
| sh = rfb.is.readUnsignedByte(); |
| |
| 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 { |
| |
| byte[] buf = new byte[256 * 4]; |
| |
| int subencoding = rfb.is.readUnsignedByte(); |
| |
| // Is it a raw-encoded sub-rectangle? |
| if ((subencoding & rfb.HextileRaw) != 0) { |
| int count, offset; |
| for (int j = ty; j < ty + th; j++) { |
| rfb.is.readFully(buf, 0, tw * 4); |
| offset = j * rfb.framebufferWidth + tx; |
| for (count = 0; count < tw; count++) { |
| pixels24[offset + count] = |
| (buf[count * 4 + 2] & 0xFF) << 16 | |
| (buf[count * 4 + 1] & 0xFF) << 8 | |
| (buf[count * 4] & 0xFF); |
| } |
| } |
| handleUpdatedPixels(tx, ty, tw, th); |
| return; |
| } |
| |
| // Read and draw the background if specified. |
| if ((subencoding & rfb.HextileBackgroundSpecified) != 0) { |
| rfb.is.readFully(buf, 0, 4); |
| hextile_bg = new Color(buf[2] & 0xFF, buf[1] & 0xFF, buf[0] & 0xFF); |
| } |
| memGraphics.setColor(hextile_bg); |
| memGraphics.fillRect(tx, ty, tw, th); |
| |
| // Read the foreground color if specified. |
| if ((subencoding & rfb.HextileForegroundSpecified) != 0) { |
| rfb.is.readFully(buf, 0, 4); |
| hextile_fg = new Color(buf[2] & 0xFF, buf[1] & 0xFF, buf[0] & 0xFF); |
| } |
| |
| // Done with this tile if there is no sub-rectangles. |
| if ((subencoding & rfb.HextileAnySubrects) == 0) |
| return; |
| |
| int nSubrects = rfb.is.readUnsignedByte(); |
| |
| int b1, b2, sx, sy, sw, sh; |
| if ((subencoding & rfb.HextileSubrectsColoured) != 0) { |
| for (int j = 0; j < nSubrects; j++) { |
| rfb.is.readFully(buf, 0, 4); |
| hextile_fg = new Color(buf[2] & 0xFF, buf[1] & 0xFF, buf[0] & 0xFF); |
| b1 = rfb.is.readUnsignedByte(); |
| b2 = rfb.is.readUnsignedByte(); |
| 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 { |
| memGraphics.setColor(hextile_fg); |
| for (int j = 0; j < nSubrects; j++) { |
| b1 = rfb.is.readUnsignedByte(); |
| b2 = rfb.is.readUnsignedByte(); |
| sx = tx + (b1 >> 4); |
| sy = ty + (b1 & 0xf); |
| sw = (b2 >> 4) + 1; |
| sh = (b2 & 0xf) + 1; |
| memGraphics.fillRect(sx, sy, sw, sh); |
| } |
| |
| } |
| } |
| |
| // |
| // Handle a Zlib-encoded rectangle. |
| // |
| void handleZlibRect(int x, int y, int w, int h) throws Exception { |
| |
| int nBytes = rfb.is.readInt(); |
| |
| if (zlibBuf == null || zlibBufLen < nBytes) { |
| zlibBufLen = nBytes * 2; |
| zlibBuf = new byte[zlibBufLen]; |
| } |
| |
| rfb.is.readFully(zlibBuf, 0, nBytes); |
| zlibInflater.setInput(zlibBuf, 0, nBytes); |
| |
| try { |
| 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); |
| } |
| } |
| } catch (DataFormatException dfe) { |
| throw new Exception(dfe.toString()); |
| } |
| |
| 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.is.readUnsignedByte(); |
| |
| // 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) { |
| byte[] buf = new byte[3]; |
| rfb.is.readFully(buf); |
| Color bg = new Color(buf[0] & 0xFF, buf[1] & 0xFF, buf[2] & 0xFF); |
| memGraphics.setColor(bg); |
| memGraphics.fillRect(x, y, w, h); |
| scheduleRepaint(x, y, w, h); |
| return; |
| |
| } |
| |
| if (comp_ctl == rfb.TightJpeg) { |
| |
| // Read JPEG data. |
| byte[] jpegData = new byte[rfb.readCompactLen()]; |
| rfb.is.readFully(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; |
| int[] palette24 = new int[256]; |
| boolean useGradient = false; |
| if ((comp_ctl & rfb.TightExplicitFilter) != 0) { |
| int filter_id = rfb.is.readUnsignedByte(); |
| if (filter_id == rfb.TightFilterPalette) { |
| numColors = rfb.is.readUnsignedByte() + 1; |
| byte[] buf = new byte[numColors * 3]; |
| rfb.is.readFully(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.is.readFully(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.is.readFully(buf); |
| decodeGradientData(x, y, w, h, buf); |
| } else { |
| // Raw truecolor data. |
| if (bytesPixel == 1) { |
| for (int dy = y; dy < y + h; dy++) { |
| rfb.is.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.is.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.is.readFully(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); |
| try { |
| if (numColors != 0) { |
| // Indexed colors. |
| byte[] indexedData = new byte[dataSize]; |
| myInflater.inflate(indexedData); |
| if (numColors == 2) { |
| // Two colors. |
| decodeMonoData(x, y, w, h, indexedData, palette24); |
| } else { |
| // More than two colors. |
| 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) { |
| // Compressed "Gradient"-filtered data. |
| byte[] buf = new byte[w * h * 3]; |
| myInflater.inflate(buf); |
| decodeGradientData(x, y, w, h, buf); |
| } else { |
| // Compressed truecolor data. |
| byte[] buf = new byte[w * 3]; |
| int i, offset; |
| for (int dy = y; dy < y + h; dy++) { |
| myInflater.inflate(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); |
| } |
| } |
| } |
| } catch (DataFormatException dfe) { |
| throw new Exception(dfe.toString()); |
| } |
| } |
| |
| 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) { |
| if (appClass.fbs.isSeeking()) { |
| // Do nothing, and remember we are seeking. |
| seekMode = true; |
| } else { |
| if (seekMode) { |
| // Immediate repaint of the whole desktop after seeking. |
| seekMode = false; |
| if (showSoftCursor) |
| scrollToPoint(cursorX, cursorY); |
| updateFramebufferSize(); |
| repaint(); |
| } else { |
| // Usual incremental repaint. |
| Point o = getImageOrigin(); |
| repaint(appClass.deferScreenUpdates, o.x + x, o.y + y, w, h); |
| } |
| } |
| } |
| |
| |
| ////////////////////////////////////////////////////////////////// |
| // |
| // 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 (appClass.options.ignoreCursorUpdates) { |
| if (false) { |
| int bytesPerRow = (width + 7) / 8; |
| int bytesMaskData = bytesPerRow * height; |
| |
| if (encodingType == rfb.EncodingXCursor) { |
| rfb.is.skipBytes(6 + bytesMaskData * 2); |
| } else { |
| // rfb.EncodingRichCursor |
| rfb.is.skipBytes(width * height + 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; |
| Point p = getImageOrigin(); |
| scheduleCursorRepaint(cursorX - hotX + p.x, cursorY - hotY + p.y, |
| cursorWidth, cursorHeight, 5); |
| } |
| |
| // |
| // 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.is.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.is.readFully(pixBuf); |
| byte[] maskBuf = new byte[bytesMaskData]; |
| rfb.is.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.is.readFully(pixBuf); |
| byte[] maskBuf = new byte[bytesMaskData]; |
| rfb.is.readFully(maskBuf); |
| |
| // Decode pixel data into softCursorPixels[]. |
| byte 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 = (pixBuf[i * 4] & 0xFF) << 24 | |
| (pixBuf[i * 4 + 1] & 0xFF) << 16 | |
| (pixBuf[i * 4 + 2] & 0xFF) << 8 | |
| (pixBuf[i * 4 + 3] & 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 + 1] & 0xFF) << 16 | |
| (pixBuf[i * 4 + 2] & 0xFF) << 8 | |
| (pixBuf[i * 4 + 3] & 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 = 100; |
| //int scaleCursor = appClass.options.scaleCursor; |
| //if (scaleCursor == 0 || !inputEnabled) |
| //scaleCursor = 100; |
| |
| 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); |
| } |
| } |
| |
| private void scrollToPoint(int x, int y) { |
| // Automatic viewport scrolling |
| if (appClass.desktopScrollPane != null) { |
| boolean needScroll = false; |
| Dimension d = appClass.desktopScrollPane.getViewportSize(); |
| Point topLeft = appClass.desktopScrollPane.getScrollPosition(); |
| Point botRight = new Point(topLeft.x + d.width, topLeft.y + d.height); |
| |
| if (x < topLeft.x + SCROLL_MARGIN) { |
| // shift left |
| topLeft.x = x - SCROLL_MARGIN; |
| needScroll = true; |
| } else if (x > botRight.x - SCROLL_MARGIN) { |
| // shift right |
| topLeft.x = x - d.width + SCROLL_MARGIN; |
| needScroll = true; |
| } |
| if (y < topLeft.y + SCROLL_MARGIN) { |
| // shift up |
| topLeft.y = y - SCROLL_MARGIN; |
| needScroll = true; |
| } else if (y > botRight.y - SCROLL_MARGIN) { |
| // shift down |
| topLeft.y = y - d.height + SCROLL_MARGIN; |
| needScroll = true; |
| } |
| if (needScroll) |
| appClass.desktopScrollPane.setScrollPosition(topLeft.x, topLeft.y); |
| } |
| } |
| |
| // |
| // softCursorMove(). Moves soft cursor into a particular location. |
| // |
| synchronized void softCursorMove(int x, int y) { |
| Point o = getImageOrigin(); |
| int oldX = cursorX + o.x; |
| int oldY = cursorY + o.y; |
| cursorX = x; |
| cursorY = y; |
| if (showSoftCursor) { |
| scheduleCursorRepaint(oldX - hotX, oldY - hotY, cursorWidth, cursorHeight, |
| 0); |
| scheduleCursorRepaint(cursorX - hotX + o.x, cursorY - hotY + o.y, |
| cursorWidth, cursorHeight, 1); |
| if (!seekMode) |
| scrollToPoint(x, y); |
| } |
| } |
| |
| // |
| // softCursorFree(). Remove soft cursor, dispose resources. |
| // |
| synchronized void softCursorFree() { |
| if (showSoftCursor) { |
| showSoftCursor = false; |
| softCursor = null; |
| softCursorSource = null; |
| |
| Point p = getImageOrigin(); |
| scheduleCursorRepaint(cursorX - hotX + p.x, cursorY - hotY + p.y, |
| cursorWidth, cursorHeight, 3); |
| } |
| } |
| |
| Point getFrameCenter() { |
| Dimension d = appClass.desktopScrollPane.getViewportSize(); |
| Point p = appClass.desktopScrollPane.getScrollPosition(); |
| if (d.width > rfb.framebufferWidth) |
| p.x = d.width / 2; |
| else |
| p.x += d.width / 2; |
| if (d.height > rfb.framebufferHeight) |
| p.y = d.height / 2; |
| else |
| p.y += d.height / 2; |
| return p; |
| } |
| |
| // |
| // We are observing our FbsInputStream object to get notified on |
| // switching to the `paused' mode. In such cases we want to repaint |
| // our desktop if we were seeking. |
| // |
| public void update(Observable o, Object arg) { |
| // Immediate repaint of the whole desktop after seeking. |
| repaint(); |
| // Let next scheduleRepaint() call invoke incremental drawing. |
| seekMode = false; |
| } |
| |
| void scheduleCursorRepaint(int x, int y, int w, int h, int n) { |
| if (appClass.fbs.isSeeking()) { |
| // Do nothing, and remember we are seeking. |
| seekMode = true; |
| } else { |
| if (seekMode) { |
| // Immediate repaint of the whole desktop after seeking. |
| seekMode = false; |
| if (showSoftCursor) |
| scrollToPoint(cursorX, cursorY); |
| updateFramebufferSize(); |
| repaint(); |
| } else { |
| // Usual incremental repaint. |
| repaint(appClass.deferScreenUpdates, x, y, w, h); |
| } |
| } |
| } |
| |
| } |