/*
* Copyright 2010 jOpenRay, ILM Informatique
* Copyright 2005 Propero Limited
*
* This program 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 program 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 program. If not, see <http://www.gnu.org/licenses/>
*/
package org.jopenray.server.thinclient;
import java.awt.Color;
import java.awt.image.BufferedImage;
import java.awt.image.PixelGrabber;
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;
import org.jopenray.operation.BitmapOperation;
import org.jopenray.operation.BitmapRGBOperation;
import org.jopenray.operation.FillOperation;
import org.jopenray.util.BitArray;
import org.jopenray.util.TIntArrayList;
import org.jopenray.util.Util;
public class BitmapEncoder {
private static final int NO_COLOR = 888;
private DisplayWriterThread writer;
private int toX, toY;
private List<BitmapLine> l = new ArrayList<BitmapLine>(1200);
private int[] pixels;
private int bitmapWidth;
private int bitmapHeight;
public BitmapEncoder(DisplayWriterThread writer) {
this.writer = writer;
}
public void setDestination(int toX, int toY) {
this.toX = toX;
this.toY = toY;
}
long encodedBitmap;
long smalBitmapEncodedBitmap;
private long smalBitmapEncodedBitmapByFill;
private long smalBitmapEncodedBitmapByBiColo;
private long encodedBitmapByBiColor;
private long encodedBitmapByRGBColor;
private long encodedBitmapByFill;
public String getStats() {
return "Encoded bitmap:" + encodedBitmap + " [Small: "
+ smalBitmapEncodedBitmapByFill + ":"
+ smalBitmapEncodedBitmapByBiColo + "]" + encodedBitmapByFill
+ ":" + encodedBitmapByBiColor + ":" + encodedBitmapByRGBColor;
}
public void encode(int[] pixels, int bitmapWidth, int bitmapHeight) {
encodedBitmap++;
this.pixels = pixels;
this.bitmapWidth = bitmapWidth;
this.bitmapHeight = bitmapHeight;
// Test small bitmap
if (bitmapWidth * bitmapHeight <= 2048) {
if (encodeSmall()) {
return;
}
}
if (bitmapWidth < 5 && bitmapHeight > 64) {
DisplayMessage message = new DisplayMessage(writer);
sendFullColorRaw(0, message, bitmapHeight, bitmapWidth);
writer.addMessage(message);
return;
}
analyseLines();
DisplayMessage message = new DisplayMessage(writer);
BitmapLine currentLine = l.get(0);
int first = 0;
// System.out.println(currentLine);
long t1 = System.nanoTime();
for (int i = 1; i < bitmapHeight; i++) {
BitmapLine line = l.get(i);
if (!currentLine.canBeMergedWith(line)) {
// Encode the merged
encodeLines(first, i - first, message);
//
// System.out.println("Cannot be merged");
currentLine = line;
first = i;
}
// System.out.println(line);
}
encodeLines(first, bitmapHeight - first, message);
long t2 = System.nanoTime();
// System.out.println("BitmapEncoder.encode(): in " + (t2 - t1) /
// 1000000
// + " ms");
writer.addMessage(message);
}
private boolean encodeSmall() {
smalBitmapEncodedBitmap++;
int stop = bitmapHeight * bitmapWidth;
int col0 = pixels[0];
int col1 = NO_COLOR;
boolean fill = true;
boolean biColor = true;
for (int i = 0; i < stop; i++) {
if (pixels[i] != col0) {
if (col1 == NO_COLOR) {
col1 = pixels[i];
fill = false;
}
if (pixels[i] != col1) {
biColor = false;
break;
}
}
}
if (fill) {
DisplayMessage m = new DisplayMessage(writer);
m.addOperation(new FillOperation(toX, toY, bitmapWidth,
bitmapHeight, new Color(col0)));
writer.addMessage(m);
smalBitmapEncodedBitmapByFill++;
// System.out.println("Bitmap replaced by fill");
return true;
} else if (biColor) {
DisplayMessage m = new DisplayMessage(writer);
BitArray b = BitmapOperation.getBytes(pixels, bitmapWidth, 0, 0,
bitmapWidth, bitmapHeight, col1);
m.addOperation(new BitmapOperation(toX, toY, bitmapWidth,
bitmapHeight, new Color(col0), new Color(col1), b));
writer.addMessage(m);
smalBitmapEncodedBitmapByBiColo++;
// System.out.println("Bitmap replaced by bicolor");
return true;
}
return false;
}
private final void analyseLines() {
l.clear();
for (int i = 0; i < bitmapHeight; i++) {
l.add(new BitmapLine(pixels, bitmapWidth, i));
}
}
private void encodeLines(int first, int height, DisplayMessage m) {
BitmapLine firstLine = l.get(first);
// System.out.println("Encode from the line " + first
// + " height: " + height + " [first line:" + firstLine
// + "]");
int width = firstLine.getWidth();
if (firstLine.getType() == BitmapLine.TYPE_MONOCOLOR) {
m.addOperation(new FillOperation(toX, toY + first, width, height,
new Color(firstLine.getColor0())));
encodedBitmapByFill++;
return;
}
if (firstLine.getType() == BitmapLine.TYPE_BICOLOR) {
encodedBitmapByBiColor++;
sendBiColor(first, m, height, width, firstLine);
return;
}
encodedBitmapByRGBColor++;
sendFullColorOptimized(first, m, height, width);
}
private void sendFullColorRaw(int first, DisplayMessage m, int height,
int width) {
// Compute Widths
int nbColor = width;
int MAX = (1448 - 128) / 4;
int nbSegmentW = (int) Math.ceil((double) nbColor / MAX);
int limitedWidth = (int) Math.ceil((double) width / nbSegmentW);
if (limitedWidth > width) {
limitedWidth = width;
} else if (limitedWidth < 1) {
limitedWidth = 1;
}
// System.out.println("width:" + width + " nbSegm:" + nbSegmentW
// + " limit width to:" + limitedWidth + " MAX:" + MAX);
int[] widths = Util.split(width, limitedWidth);
// Compute Heights
nbColor = limitedWidth * height;
int nbSegmentH = (int) Math.ceil((double) nbColor / MAX);
int limitedHeight = (int) Math.floor((double) height / nbSegmentH);
if (limitedHeight < 1) {
limitedHeight = 1;
} else if (limitedHeight > height) {
limitedHeight = height;
}
int[] heights = Util.split(height, limitedHeight);
// System.out.println("height:" + height + " nbSegm:" + nbSegmentH
// + " limit height to:" + limitedHeight + " MAX:" + MAX);
for (int j = 0; j < heights.length; j++) {
int newHeight = heights[j];
for (int i = 0; i < widths.length; i++) {
int newWidth = widths[i];
byte[] bytes = BitmapRGBOperation.getBytes(pixels, width,
limitedWidth * i, first + limitedHeight * j, newWidth,
newHeight);
m.addOperation(new BitmapRGBOperation(toX + limitedWidth * i,
toY + first + limitedHeight * j, newWidth, newHeight,
bytes));
}
}
}
private void sendFullColorOptimized(int first, DisplayMessage m,
int height, int width) {
// Is sending full
// System.out.println("RGB: " + toX + " , " + (toY + first) + " "
// + width + " x " + height);
for (int i = first; i < first + height; i++) {
sendFullColorLine(m, i);
}
}
private void sendFullColorLineAsRGB(int first, DisplayMessage m,
int height, int width) {
// Is sending full
// System.out.println("RGB: " + toX + " , " + (toY + first) + " "
// + width + " x " + height);
for (int i = first; i < first + height; i++) {
sendFullColorLineAsRGB(m, i);
}
}
private void sendBiColor(int first, DisplayMessage m, int height,
int width, BitmapLine firstLine) {
// On envoi en bi
// System.out.println("BI: " + toX + " , " + (toY + first) + " " + width
// + " x " + height);
if (width > 32) {
for (int i = 0; i < height; i++) {
sendBiColorLine(pixels, width, i + first, toX, toY, firstLine
.getColor0(), firstLine.getColor1(), m);
}
return;
}
// Compute Widths
int nbColor = width;
int MAX = (1448 - 64);
int nbSegmentW = (int) Math.ceil((double) nbColor / MAX);
int limitedWidth = (int) Math.ceil((double) width / nbSegmentW);
if (limitedWidth > width) {
limitedWidth = width;
} else if (limitedWidth < 1) {
limitedWidth = 1;
}
// System.out.println("width:" + width + " nbSegm:" + nbSegmentW
// + " limit width to:" + limitedWidth + " MAX:" + MAX);
int[] widths = Util.split(width, limitedWidth);
// Compute Heights
nbColor = limitedWidth * height;
int nbSegmentH = (int) Math.ceil((double) nbColor / MAX);
int limitedHeight = (int) Math.floor((double) height / nbSegmentH);
if (limitedHeight < 1) {
limitedHeight = 1;
} else if (limitedHeight > height) {
limitedHeight = height;
}
int[] heights = Util.split(height, limitedHeight);
// System.out.println("height:" + height + " nbSegm:" + nbSegmentH
// + " limit height to:" + limitedHeight + " MAX:" + MAX);
for (int j = 0; j < heights.length; j++) {
int newHeight = heights[j];
for (int i = 0; i < widths.length; i++) {
int newWidth = widths[i];
BitArray bytes = BitmapOperation.getBytes(pixels, width,
limitedWidth * i, first + limitedHeight * j, newWidth,
newHeight, firstLine.getColor1());
m.addOperation(new BitmapOperation(toX + limitedWidth * i, toY
+ first + limitedHeight * j, newWidth, newHeight,
new Color(firstLine.getColor0()), new Color(firstLine
.getColor1()), bytes));
}
}
}
private void sendBiColorLine(int[] pixels, int width, int y, int toX,
int toY, int c0, int c1, DisplayMessage m) {
int offset = y * width;
int count = 0;// number of contiguous pixels
int currentColor = pixels[offset];
Vector<Integer> counts = new Vector<Integer>();
// System.out.println("Analysing line");
for (int i = 0; i < width; i++) {
int p1 = pixels[offset];
offset++;
// System.out.print(p1 + " ,");
if (p1 == currentColor) {
count++;
} else {
counts.addElement(count);
count = 1;
currentColor = p1;
}
}
if (count > 0) {
counts.addElement(count);
}
// System.out.println();
// System.out.println("Counts:");
int offsetX = 0;
int lastSentX = 0;
// for (int i = 0; i < counts.size(); i++) {
// int nb = counts.elementAt(i);
// System.out.print(nb + " ,");
// }
// System.out.println();
int size = counts.size();
for (int i = 0; i < size; i++) {
int nb = counts.elementAt(i);
if (nb > 16) {
// Sending mono
int nbMono = offsetX - lastSentX;
if (nbMono > 0) {
// System.out.println("Bi "+(toX + lastSentX)+","+(toY +
// y)+" "+nbMono+"x1");
BitArray b = BitmapOperation.getBytes(pixels, width,
lastSentX, y, nbMono, 1, c1);
BitmapOperation bOp = new BitmapOperation(
(toX + lastSentX), (toY + y), nbMono, 1, new Color(
c0), new Color(c1), b);
m.addOperation(bOp);
}
// Sending fill
// System.out.println("Fill rect:"+(toX + offsetX)+","+(toY +
// y)+" "+nb+"x1");
FillOperation fOp = new FillOperation(toX + offsetX, toY + y,
nb, 1, new Color(pixels[y * width + offsetX]));
m.addOperation(fOp);
lastSentX = offsetX + nb;
}
offsetX += nb;
}
int nbMono = offsetX - lastSentX;
if (nbMono > 0) {
// System.out.println("Bi "+(toX + lastSentX)+","+(toY +
// y)+" "+nbMono+"x1");
BitArray b = BitmapOperation.getBytes(pixels, width, lastSentX, y,
nbMono, 1, c1);
BitmapOperation bOp = new BitmapOperation((toX + lastSentX),
(toY + y), nbMono, 1, new Color(c0), new Color(c1), b);
m.addOperation(bOp);
}
// System.out.println();
}
public void encode(BufferedImage image, int toX, int toY) {
int width = image.getWidth();
int height = image.getHeight();
int[] pixels = new int[width * height];
PixelGrabber pg = new PixelGrabber(image, 0, 0, width, height, pixels,
0, width);
try {
pg.grabPixels();
} catch (InterruptedException e) {
}
encode(toX, toY, width, height, pixels);
// int r = sendBitmap_new(pixels, width, height, toX, toY);
}
public void encode(int toX, int toY, int width, int height, int[] pixels) {
//long t1 = System.nanoTime();
setDestination(toX, toY);
encode(pixels, width, height);
//long t2 = System.nanoTime();
//System.out.println("Bitmap "+width+"x"+height+" at "+toX+","+toY+" encoded in ms:" + (t2 - t1) / 1000000);
}
private static final boolean DEBUG = false;
private static final int MAX_SIZE = 128;
private static final int HASH_SIZE = 1283; // Prime,optimus :)
private final int getColorCountForLine(int y, int max) {
// TIntHashSet colors = new TIntHashSet();
TIntArrayList colors = new TIntArrayList(64);
int ref[] = new int[HASH_SIZE * 2];
int nbColor = 0;
for (int x = y * bitmapWidth; x < y * bitmapWidth + bitmapWidth; x++) {
int c = pixels[x];
int hash = HASH_SIZE + c % HASH_SIZE;
boolean isNewColor = (ref[hash] == 0);
if (!isNewColor) {
isNewColor = !colors.contains(c);
}
if (!isNewColor) {
// never
nbColor++;
ref[hash] = 1;
colors.add(c);
if (nbColor > max) {
break;
}
}
}
return nbColor;
}
private void sendFullColorLine(DisplayMessage m, int y) {
if (bitmapWidth < 3) {
Thread.dumpStack();
System.exit(0);
}
// Nb color analysis
if (bitmapWidth > 0 /* 32 */) {
int maxColor = Math.min(2 + bitmapWidth / 10, 32);
int nbColor = getColorCountForLine(y, maxColor);
if (nbColor > maxColor) {
sendFullColorLineAsRGB(m, y);
return;
}
}
Vector<Integer> counts = new Vector<Integer>();
int currentCount = 0;
int offset = y * bitmapWidth;
int c1 = NO_COLOR;
int c2 = NO_COLOR;
int startX = 0;
if (DEBUG) {
System.err.println("\nLine:" + y);
for (int x = 0; x < bitmapWidth; x++) {
int c = pixels[offset];// image.getRGB(x, y);
offset++;
System.err.print(c + ",");
if (offset % 10 == 0) {
System.err.println();
}
}
System.err.println();
}
boolean isC1 = true;
currentCount = 0;
counts.removeAllElements();
int encodedPixel = 0;
for (int x = 0; x < bitmapWidth; x++) {
if (DEBUG) {
System.err.println("Reading pixel index:" + x + " (encoded:"
+ encodedPixel + ")");
}
int c = pixels[offset];// image.getRGB(x, y);
offset++;
if (c1 == NO_COLOR) {
c1 = c;
} else if (c != c1 && c2 == NO_COLOR) {
// 2nd color we discover
c2 = c;
}
if (c == c1) {
if (isC1) {
currentCount++;
} else {
counts.addElement(currentCount);
currentCount = 1;
isC1 = true;
}
} else if (c == c2) {
if (!isC1) {
currentCount++;
} else {
counts.addElement(currentCount);
currentCount = 1;
isC1 = false;
}
}
if (c != c1 && c != c2 || x == bitmapWidth - 1) {
// We discover a 3rd color or are in end of line
counts.addElement(currentCount);
int size = counts.size();
if (DEBUG) {
System.err.println("============== Color change");
dumpCounts(y, counts, c1, c2, startX, x, size);
}
// Creation of operations
int cumulatedCount = 0; // Cumulative length of the segment
int s = 0; // Cumulative beginning of the current segment
int nbCumulated = 0;// Cumulative number of segment
int lastSentSegment = -2;
for (int i = 0; i < size; i++) {
int v = counts.elementAt(i);
if (cumulatedCount + v > MAX_SIZE) {
// sending up to i-1
if (cumulatedCount > 0) {
//System.err.println("cumulated:" + nbCumulated);
if (nbCumulated == 1) {
/*System.err.println("mono de " + s + " l:"
+ (cumulatedCount) + " color: isC1:"
+ (i % 2 == 1));*/
if (i % 2 == 1) {
m.addOperation(new FillOperation(startX + s
+ toX, y + toY, cumulatedCount, 1,
getColorFrom(c1)));
encodedPixel += cumulatedCount;
} else {
m.addOperation(new FillOperation(startX + s
+ toX, y + toY, cumulatedCount, 1,
getColorFrom(c2)));
encodedPixel += cumulatedCount;
}
} else {
//System.err.println("bi de " + s + " l:"
// + (cumulatedCount));
m.addOperation(getBiColorBitmapOperation(
pixels, startX + s, cumulatedCount,
bitmapWidth, y, c1, c2, toX, toY));
encodedPixel += cumulatedCount;
}
}
nbCumulated = 0;
s += cumulatedCount;
cumulatedCount = 0;
lastSentSegment = i;
}
cumulatedCount += v;
nbCumulated++;
}
if (lastSentSegment <= size) {
// the end
if (nbCumulated == 1) {
// System.err.println("end mono " + s + " l:"
// + (cumulatedCount) + " color: isC1:"
// + (lastSentSegment % 2 == 0));
if (lastSentSegment % 2 == 0) {
m.addOperation(new FillOperation(startX + s + toX,
y + toY, cumulatedCount, 1,
getColorFrom(c1)));
encodedPixel += cumulatedCount;
} else {
m.addOperation(new FillOperation(startX + s + toX,
y + toY, cumulatedCount, 1,
getColorFrom(c2)));
encodedPixel += cumulatedCount;
}
} else {
// System.err.println("end bi " + s + " l:"
// + (cumulatedCount));
m.addOperation(getBiColorBitmapOperation(pixels, startX
+ s, cumulatedCount, bitmapWidth, y, c1, c2,
toX, toY));
encodedPixel += cumulatedCount;
}
nbCumulated = 0;
s = cumulatedCount;
cumulatedCount = 0;
}
if (DEBUG) {
System.err.println("============== Clear count");
dumpCounts(y, counts, c1, c2, startX, x, size);
}
counts.removeAllElements();
currentCount = 1;
isC1 = true;
startX = x;
c1 = c;
c2 = NO_COLOR;
if (DEBUG) {
System.err.println("============== Encoded pixels:"
+ encodedPixel + " / " + bitmapWidth);
}
}
}
if (encodedPixel != bitmapWidth /* && lastPainted == bitmapWidth - 1 */) {
final int width = bitmapWidth - encodedPixel;
if (width != 1) {
throw new IllegalStateException("More than 1 pixel missing");
}
m.addOperation(new FillOperation(toX + encodedPixel, y + toY,
width, 1, getColorFrom(c1)));
encodedPixel += 1;
}
}
private void dumpCounts(int y, Vector<Integer> counts, int c1, int c2,
int startX, int x, int size) {
for (int i = 0; i < size; i++) {
System.err.print(counts.elementAt(i));
if (i < size - 1) {
System.err.print(", ");
}
}
System.err.println("--");
for (int i = bitmapWidth * y + startX; i < bitmapWidth * y + x; i++) {
if (pixels[i] == c1) {
System.err.print("0");
} else {
System.err.print("1");
}
}
System.err.println(":" + (x - startX) + " color:" + c1 + "/" + c2);
}
private void sendFullColorLineAsRGB(DisplayMessage m, int y) {
int MAX = 480;// (1448 - 64) / 4;
int nbSegmentW = (int) Math.ceil((double) bitmapWidth / MAX);
int limitedWidth = (int) Math.ceil((double) bitmapWidth / nbSegmentW);
if (limitedWidth > bitmapWidth) {
limitedWidth = bitmapWidth;
} else if (limitedWidth < 1) {
limitedWidth = 1;
}
// System.out.println("width:" + width + " nbSegm:" + nbSegmentW
// + " limit width to:" + limitedWidth + " MAX:" + MAX);
int[] widths = Util.split(bitmapWidth, limitedWidth);
for (int i = 0; i < widths.length; i++) {
int newWidth = widths[i];
byte[] bytes = BitmapRGBOperation.getBytes(pixels, bitmapWidth,
limitedWidth * i, y, newWidth, 1);
m.addOperation(new BitmapRGBOperation(toX + limitedWidth * i, toY
+ y, newWidth, 1, bytes));
}
}
private BitmapOperation getBiColorBitmapOperation(int[] pixels, int startX,
int pixelToSend, int bitmapWidth, int y, int c1, int c2, int toX,
int toY) {
// We sent a setRectBitmap
// System.out.println("c:" + c + " c1:" + c1 + "c2:" +
// c2);
// System.out.println("x:" + x + " start:" + startX);
// int LIMIT = 112;
int nbBitsToSend = pixelToSend;
if (nbBitsToSend % 32 > 0) {
nbBitsToSend = nbBitsToSend + 32 - nbBitsToSend % 32;
}
BitArray b = new BitArray(nbBitsToSend);
int index = 0;
int counter = startX + y * bitmapWidth;
for (int i = 0; i < pixelToSend; i++) {
int col = pixels[counter + i];// ! out of bound
if (col == c2) {
// System.out.print(1);
b.set(index);
}
index++;
}
return new BitmapOperation(startX + toX, y + toY, pixelToSend, 1,
getColorFrom(c1), getColorFrom(c2), b);
}
public static Color getColorFrom(int c1) {
if (c1 == NO_COLOR) {
throw new IllegalArgumentException("Bad color");
}
int red1 = (c1 & 0x00ff0000) >> 16;
int green1 = (c1 & 0x0000ff00) >> 8;
int blue1 = c1 & 0x000000ff;
return new Color(red1, green1, blue1);
}
}