+ /**
+ * SixelPalette is used to manage the conversion of images between 24-bit
+ * RGB color and a palette of sixelPaletteSize colors.
+ */
+ private class SixelPalette {
+
+ /**
+ * Color palette for sixel output, sorted low to high.
+ */
+ private List<Integer> rgbColors = new ArrayList<Integer>();
+
+ /**
+ * Map of color palette index for sixel output, from the order it was
+ * generated by makePalette() to rgbColors.
+ */
+ private int [] rgbSortedIndex = new int[sixelPaletteSize];
+
+ /**
+ * The color palette, organized by hue, saturation, and luminance.
+ * This is used for a fast color match.
+ */
+ private ArrayList<ArrayList<ArrayList<ColorIdx>>> hslColors;
+
+ /**
+ * Number of bits for hue.
+ */
+ private int hueBits = -1;
+
+ /**
+ * Number of bits for saturation.
+ */
+ private int satBits = -1;
+
+ /**
+ * Number of bits for luminance.
+ */
+ private int lumBits = -1;
+
+ /**
+ * Step size for hue bins.
+ */
+ private int hueStep = -1;
+
+ /**
+ * Step size for saturation bins.
+ */
+ private int satStep = -1;
+
+ /**
+ * Cached RGB to HSL result.
+ */
+ private int hsl[] = new int[3];
+
+ /**
+ * ColorIdx records a RGB color and its palette index.
+ */
+ private class ColorIdx {
+ /**
+ * The 24-bit RGB color.
+ */
+ public int color;
+
+ /**
+ * The palette index for this color.
+ */
+ public int index;
+
+ /**
+ * Public constructor.
+ *
+ * @param color the 24-bit RGB color
+ * @param index the palette index for this color
+ */
+ public ColorIdx(final int color, final int index) {
+ this.color = color;
+ this.index = index;
+ }
+ }
+
+ /**
+ * Public constructor.
+ */
+ public SixelPalette() {
+ makePalette();
+ }
+
+ /**
+ * Find the nearest match for a color in the palette.
+ *
+ * @param color the RGB color
+ * @return the index in rgbColors that is closest to color
+ */
+ public int matchColor(final int color) {
+
+ assert (color >= 0);
+
+ /*
+ * matchColor() is a critical performance bottleneck. To make it
+ * decent, we do the following:
+ *
+ * 1. Find the nearest two hues that bracket this color.
+ *
+ * 2. Find the nearest two saturations that bracket this color.
+ *
+ * 3. Iterate within these four bands of luminance values,
+ * returning the closest color by Euclidean distance.
+ *
+ * This strategy reduces the search space by about 97%.
+ */
+ int red = (color >>> 16) & 0xFF;
+ int green = (color >>> 8) & 0xFF;
+ int blue = color & 0xFF;
+
+ if (sixelPaletteSize == 2) {
+ if (((red * red) + (green * green) + (blue * blue)) < 35568) {
+ // Black
+ return 0;
+ }
+ // White
+ return 1;
+ }
+
+
+ rgbToHsl(red, green, blue, hsl);
+ int hue = hsl[0];
+ int sat = hsl[1];
+ int lum = hsl[2];
+ // System.err.printf("%d %d %d\n", hue, sat, lum);
+
+ double diff = Double.MAX_VALUE;
+ int idx = -1;
+
+ int hue1 = hue / (360/hueStep);
+ int hue2 = hue1 + 1;
+ if (hue1 >= hslColors.size() - 1) {
+ // Bracket pure red from above.
+ hue1 = hslColors.size() - 1;
+ hue2 = 0;
+ } else if (hue1 == 0) {
+ // Bracket pure red from below.
+ hue2 = hslColors.size() - 1;
+ }
+
+ for (int hI = hue1; hI != -1;) {
+ ArrayList<ArrayList<ColorIdx>> sats = hslColors.get(hI);
+ if (hI == hue1) {
+ hI = hue2;
+ } else if (hI == hue2) {
+ hI = -1;
+ }
+
+ int sMin = (sat / satStep) - 1;
+ int sMax = sMin + 1;
+ if (sMin < 0) {
+ sMin = 0;
+ sMax = 1;
+ } else if (sMin == sats.size() - 1) {
+ sMax = sMin;
+ sMin--;
+ }
+ assert (sMin >= 0);
+ assert (sMax - sMin == 1);
+
+ // int sMin = 0;
+ // int sMax = sats.size() - 1;
+
+ for (int sI = sMin; sI <= sMax; sI++) {
+ ArrayList<ColorIdx> lums = sats.get(sI);
+
+ // True 3D colorspace match for the remaining values
+ for (ColorIdx c: lums) {
+ int rgbColor = c.color;
+ double newDiff = 0;
+ int red2 = (rgbColor >>> 16) & 0xFF;
+ int green2 = (rgbColor >>> 8) & 0xFF;
+ int blue2 = rgbColor & 0xFF;
+ newDiff += Math.pow(red2 - red, 2);
+ newDiff += Math.pow(green2 - green, 2);
+ newDiff += Math.pow(blue2 - blue, 2);
+ if (newDiff < diff) {
+ idx = rgbSortedIndex[c.index];
+ diff = newDiff;
+ }
+ }
+ }
+ }
+
+ if (((red * red) + (green * green) + (blue * blue)) < diff) {
+ // Black is a closer match.
+ idx = 0;
+ } else if ((((255 - red) * (255 - red)) +
+ ((255 - green) * (255 - green)) +
+ ((255 - blue) * (255 - blue))) < diff) {
+
+ // White is a closer match.
+ idx = sixelPaletteSize - 1;
+ }
+ assert (idx != -1);
+ return idx;
+ }
+
+ /**
+ * Clamp an int value to [0, 255].
+ *
+ * @param x the int value
+ * @return an int between 0 and 255.
+ */
+ private int clamp(final int x) {
+ if (x < 0) {
+ return 0;
+ }
+ if (x > 255) {
+ return 255;
+ }
+ return x;
+ }
+
+ /**
+ * Dither an image to a sixelPaletteSize palette. The dithered
+ * image cells will contain indexes into the palette.
+ *
+ * @param image the image to dither
+ * @return the dithered image. Every pixel is an index into the
+ * palette.
+ */
+ public BufferedImage ditherImage(final BufferedImage image) {
+
+ BufferedImage ditheredImage = new BufferedImage(image.getWidth(),
+ image.getHeight(), BufferedImage.TYPE_INT_ARGB);
+
+ int [] rgbArray = image.getRGB(0, 0, image.getWidth(),
+ image.getHeight(), null, 0, image.getWidth());
+ ditheredImage.setRGB(0, 0, image.getWidth(), image.getHeight(),
+ rgbArray, 0, image.getWidth());
+
+ for (int imageY = 0; imageY < image.getHeight(); imageY++) {
+ for (int imageX = 0; imageX < image.getWidth(); imageX++) {
+ int oldPixel = ditheredImage.getRGB(imageX,
+ imageY) & 0xFFFFFF;
+ int colorIdx = matchColor(oldPixel);
+ assert (colorIdx >= 0);
+ assert (colorIdx < sixelPaletteSize);
+ int newPixel = rgbColors.get(colorIdx);
+ ditheredImage.setRGB(imageX, imageY, colorIdx);
+
+ int oldRed = (oldPixel >>> 16) & 0xFF;
+ int oldGreen = (oldPixel >>> 8) & 0xFF;
+ int oldBlue = oldPixel & 0xFF;
+
+ int newRed = (newPixel >>> 16) & 0xFF;
+ int newGreen = (newPixel >>> 8) & 0xFF;
+ int newBlue = newPixel & 0xFF;
+
+ int redError = (oldRed - newRed) / 16;
+ int greenError = (oldGreen - newGreen) / 16;
+ int blueError = (oldBlue - newBlue) / 16;
+
+ int red, green, blue;
+ if (imageX < image.getWidth() - 1) {
+ int pXpY = ditheredImage.getRGB(imageX + 1, imageY);
+ red = ((pXpY >>> 16) & 0xFF) + (7 * redError);
+ green = ((pXpY >>> 8) & 0xFF) + (7 * greenError);
+ blue = ( pXpY & 0xFF) + (7 * blueError);
+ red = clamp(red);
+ green = clamp(green);
+ blue = clamp(blue);
+ pXpY = ((red & 0xFF) << 16);
+ pXpY |= ((green & 0xFF) << 8) | (blue & 0xFF);
+ ditheredImage.setRGB(imageX + 1, imageY, pXpY);
+
+ if (imageY < image.getHeight() - 1) {
+ int pXpYp = ditheredImage.getRGB(imageX + 1,
+ imageY + 1);
+ red = ((pXpYp >>> 16) & 0xFF) + redError;
+ green = ((pXpYp >>> 8) & 0xFF) + greenError;
+ blue = ( pXpYp & 0xFF) + blueError;
+ red = clamp(red);
+ green = clamp(green);
+ blue = clamp(blue);
+ pXpYp = ((red & 0xFF) << 16);
+ pXpYp |= ((green & 0xFF) << 8) | (blue & 0xFF);
+ ditheredImage.setRGB(imageX + 1, imageY + 1, pXpYp);
+ }
+ } else if (imageY < image.getHeight() - 1) {
+ int pXmYp = ditheredImage.getRGB(imageX - 1,
+ imageY + 1);
+ int pXYp = ditheredImage.getRGB(imageX,
+ imageY + 1);
+
+ red = ((pXmYp >>> 16) & 0xFF) + (3 * redError);
+ green = ((pXmYp >>> 8) & 0xFF) + (3 * greenError);
+ blue = ( pXmYp & 0xFF) + (3 * blueError);
+ red = clamp(red);
+ green = clamp(green);
+ blue = clamp(blue);
+ pXmYp = ((red & 0xFF) << 16);
+ pXmYp |= ((green & 0xFF) << 8) | (blue & 0xFF);
+ ditheredImage.setRGB(imageX - 1, imageY + 1, pXmYp);
+
+ red = ((pXYp >>> 16) & 0xFF) + (5 * redError);
+ green = ((pXYp >>> 8) & 0xFF) + (5 * greenError);
+ blue = ( pXYp & 0xFF) + (5 * blueError);
+ red = clamp(red);
+ green = clamp(green);
+ blue = clamp(blue);
+ pXYp = ((red & 0xFF) << 16);
+ pXYp |= ((green & 0xFF) << 8) | (blue & 0xFF);
+ ditheredImage.setRGB(imageX, imageY + 1, pXYp);
+ }
+ } // for (int imageY = 0; imageY < image.getHeight(); imageY++)
+ } // for (int imageX = 0; imageX < image.getWidth(); imageX++)
+
+ return ditheredImage;
+ }
+
+ /**
+ * Convert an RGB color to HSL.
+ *
+ * @param red red color, between 0 and 255
+ * @param green green color, between 0 and 255
+ * @param blue blue color, between 0 and 255
+ * @param hsl the hsl color as [hue, saturation, luminance]
+ */
+ private void rgbToHsl(final int red, final int green,
+ final int blue, final int [] hsl) {
+
+ assert ((red >= 0) && (red <= 255));
+ assert ((green >= 0) && (green <= 255));
+ assert ((blue >= 0) && (blue <= 255));
+
+ double R = red / 255.0;
+ double G = green / 255.0;
+ double B = blue / 255.0;
+ boolean Rmax = false;
+ boolean Gmax = false;
+ boolean Bmax = false;
+ double min = (R < G ? R : G);
+ min = (min < B ? min : B);
+ double max = 0;
+ if ((R >= G) && (R >= B)) {
+ max = R;
+ Rmax = true;
+ } else if ((G >= R) && (G >= B)) {
+ max = G;
+ Gmax = true;
+ } else if ((B >= G) && (B >= R)) {
+ max = B;
+ Bmax = true;
+ }
+
+ double L = (min + max) / 2.0;
+ double H = 0.0;
+ double S = 0.0;
+ if (min != max) {
+ if (L < 0.5) {
+ S = (max - min) / (max + min);
+ } else {
+ S = (max - min) / (2.0 - max - min);
+ }
+ }
+ if (Rmax) {
+ assert (Gmax == false);
+ assert (Bmax == false);
+ H = (G - B) / (max - min);
+ } else if (Gmax) {
+ assert (Rmax == false);
+ assert (Bmax == false);
+ H = 2.0 + (B - R) / (max - min);
+ } else if (Bmax) {
+ assert (Rmax == false);
+ assert (Gmax == false);
+ H = 4.0 + (R - G) / (max - min);
+ }
+ if (H < 0.0) {
+ H += 6.0;
+ }
+ hsl[0] = (int) (H * 60.0);
+ hsl[1] = (int) (S * 100.0);
+ hsl[2] = (int) (L * 100.0);
+
+ assert ((hsl[0] >= 0) && (hsl[0] <= 360));
+ assert ((hsl[1] >= 0) && (hsl[1] <= 100));
+ assert ((hsl[2] >= 0) && (hsl[2] <= 100));
+ }
+
+ /**
+ * Convert a HSL color to RGB.
+ *
+ * @param hue hue, between 0 and 359
+ * @param sat saturation, between 0 and 100
+ * @param lum luminance, between 0 and 100
+ * @return the rgb color as 0x00RRGGBB
+ */
+ private int hslToRgb(final int hue, final int sat, final int lum) {
+ assert ((hue >= 0) && (hue <= 360));
+ assert ((sat >= 0) && (sat <= 100));
+ assert ((lum >= 0) && (lum <= 100));
+
+ double S = sat / 100.0;
+ double L = lum / 100.0;
+ double C = (1.0 - Math.abs((2.0 * L) - 1.0)) * S;
+ double Hp = hue / 60.0;
+ double X = C * (1.0 - Math.abs((Hp % 2) - 1.0));
+ double Rp = 0.0;
+ double Gp = 0.0;
+ double Bp = 0.0;
+ if (Hp <= 1.0) {
+ Rp = C;
+ Gp = X;
+ } else if (Hp <= 2.0) {
+ Rp = X;
+ Gp = C;
+ } else if (Hp <= 3.0) {
+ Gp = C;
+ Bp = X;
+ } else if (Hp <= 4.0) {
+ Gp = X;
+ Bp = C;
+ } else if (Hp <= 5.0) {
+ Rp = X;
+ Bp = C;
+ } else if (Hp <= 6.0) {
+ Rp = C;
+ Bp = X;
+ }
+ double m = L - (C / 2.0);
+ int red = ((int) ((Rp + m) * 255.0)) << 16;
+ int green = ((int) ((Gp + m) * 255.0)) << 8;
+ int blue = (int) ((Bp + m) * 255.0);
+
+ return (red | green | blue);
+ }
+
+ /**
+ * Create the sixel palette.
+ */
+ private void makePalette() {
+ // Generate the sixel palette. Because we have no idea at this
+ // layer which image(s) will be shown, we have to use a common
+ // palette with sixelPaletteSize colors for everything, and
+ // map the BufferedImage colors to their nearest neighbor in RGB
+ // space.
+
+ if (sixelPaletteSize == 2) {
+ rgbColors.add(0);
+ rgbColors.add(0xFFFFFF);
+ rgbSortedIndex[0] = 0;
+ rgbSortedIndex[1] = 1;
+ return;
+ }
+
+ // We build a palette using the Hue-Saturation-Luminence model,
+ // with 5+ bits for Hue, 2+ bits for Saturation, and 1+ bit for
+ // Luminance. We convert these colors to 24-bit RGB, sort them
+ // ascending, and steal the first index for pure black and the
+ // last for pure white. The 8-bit final palette favors bright
+ // colors, somewhere between pastel and classic television
+ // technicolor. 9- and 10-bit palettes are more uniform.
+
+ // Default at 256 colors.
+ hueBits = 5;
+ satBits = 2;
+ lumBits = 1;
+
+ assert (sixelPaletteSize >= 256);
+ assert ((sixelPaletteSize == 256)
+ || (sixelPaletteSize == 512)
+ || (sixelPaletteSize == 1024)
+ || (sixelPaletteSize == 2048));
+
+ switch (sixelPaletteSize) {
+ case 512:
+ hueBits = 5;
+ satBits = 2;
+ lumBits = 2;
+ break;
+ case 1024:
+ hueBits = 5;
+ satBits = 2;
+ lumBits = 3;
+ break;
+ case 2048:
+ hueBits = 5;
+ satBits = 3;
+ lumBits = 3;
+ break;
+ }
+ hueStep = (int) (Math.pow(2, hueBits));
+ satStep = (int) (100 / Math.pow(2, satBits));
+ // 1 bit for luminance: 40 and 70.
+ int lumBegin = 40;
+ int lumStep = 30;
+ switch (lumBits) {
+ case 2:
+ // 2 bits: 20, 40, 60, 80
+ lumBegin = 20;
+ lumStep = 20;
+ break;
+ case 3:
+ // 3 bits: 8, 20, 32, 44, 56, 68, 80, 92
+ lumBegin = 8;
+ lumStep = 12;
+ break;
+ }
+
+ // System.err.printf("<html><body>\n");
+ // Hue is evenly spaced around the wheel.
+ hslColors = new ArrayList<ArrayList<ArrayList<ColorIdx>>>();
+
+ final boolean DEBUG = false;
+ ArrayList<Integer> rawRgbList = new ArrayList<Integer>();
+
+ for (int hue = 0; hue < (360 - (360 % hueStep));
+ hue += (360/hueStep)) {
+
+ ArrayList<ArrayList<ColorIdx>> satList = null;
+ satList = new ArrayList<ArrayList<ColorIdx>>();
+ hslColors.add(satList);
+
+ // Saturation is linearly spaced between pastel and pure.
+ for (int sat = satStep; sat <= 100; sat += satStep) {
+
+ ArrayList<ColorIdx> lumList = new ArrayList<ColorIdx>();
+ satList.add(lumList);
+
+ // Luminance brackets the pure color, but leaning toward
+ // lighter.
+ for (int lum = lumBegin; lum < 100; lum += lumStep) {
+ /*
+ System.err.printf("<font style = \"color:");
+ System.err.printf("hsl(%d, %d%%, %d%%)",
+ hue, sat, lum);
+ System.err.printf(";\">=</font>\n");
+ */
+ int rgbColor = hslToRgb(hue, sat, lum);
+ rgbColors.add(rgbColor);
+ ColorIdx colorIdx = new ColorIdx(rgbColor,
+ rgbColors.size() - 1);
+ lumList.add(colorIdx);
+
+ rawRgbList.add(rgbColor);
+ if (DEBUG) {
+ int red = (rgbColor >>> 16) & 0xFF;
+ int green = (rgbColor >>> 8) & 0xFF;
+ int blue = rgbColor & 0xFF;
+ int [] backToHsl = new int[3];
+ rgbToHsl(red, green, blue, backToHsl);
+ System.err.printf("%d [%d] %d [%d] %d [%d]\n",
+ hue, backToHsl[0], sat, backToHsl[1],
+ lum, backToHsl[2]);
+ }
+ }
+ }
+ }
+ // System.err.printf("\n</body></html>\n");
+
+ assert (rgbColors.size() == sixelPaletteSize);
+
+ /*
+ * We need to sort rgbColors, so that toSixel() can know where
+ * BLACK and WHITE are in it. But we also need to be able to
+ * find the sorted values using the old unsorted indexes. So we
+ * will sort it, put all the indexes into a HashMap, and then
+ * build rgbSortedIndex[].
+ */
+ Collections.sort(rgbColors);
+ HashMap<Integer, Integer> rgbColorIndices = null;
+ rgbColorIndices = new HashMap<Integer, Integer>();
+ for (int i = 0; i < sixelPaletteSize; i++) {
+ rgbColorIndices.put(rgbColors.get(i), i);
+ }
+ for (int i = 0; i < sixelPaletteSize; i++) {
+ int rawColor = rawRgbList.get(i);
+ rgbSortedIndex[i] = rgbColorIndices.get(rawColor);
+ }
+ if (DEBUG) {
+ for (int i = 0; i < sixelPaletteSize; i++) {
+ assert (rawRgbList != null);
+ int idx = rgbSortedIndex[i];
+ int rgbColor = rgbColors.get(idx);
+ if ((idx != 0) && (idx != sixelPaletteSize - 1)) {
+ /*
+ System.err.printf("%d %06x --> %d %06x\n",
+ i, rawRgbList.get(i), idx, rgbColors.get(idx));
+ */
+ assert (rgbColor == rawRgbList.get(i));
+ }
+ }
+ }
+
+ // Set the dimmest color as true black, and the brightest as true
+ // white.
+ rgbColors.set(0, 0);
+ rgbColors.set(sixelPaletteSize - 1, 0xFFFFFF);
+
+ /*
+ System.err.printf("<html><body>\n");
+ for (Integer rgb: rgbColors) {
+ System.err.printf("<font style = \"color:");
+ System.err.printf("#%06x", rgb);
+ System.err.printf(";\">=</font>\n");
+ }
+ System.err.printf("\n</body></html>\n");
+ */
+
+ }
+
+ /**
+ * Emit the sixel palette.
+ *
+ * @param sb the StringBuilder to append to
+ * @param used array of booleans set to true for each color actually
+ * used in this cell, or null to emit the entire palette
+ * @return the string to emit to an ANSI / ECMA-style terminal
+ */
+ public String emitPalette(final StringBuilder sb,
+ final boolean [] used) {
+
+ for (int i = 0; i < sixelPaletteSize; i++) {
+ if (((used != null) && (used[i] == true)) || (used == null)) {
+ int rgbColor = rgbColors.get(i);
+ sb.append(String.format("#%d;2;%d;%d;%d", i,
+ ((rgbColor >>> 16) & 0xFF) * 100 / 255,
+ ((rgbColor >>> 8) & 0xFF) * 100 / 255,
+ ( rgbColor & 0xFF) * 100 / 255));
+ }
+ }
+ return sb.toString();
+ }
+ }
+
+ /**
+ * SixelCache is a least-recently-used cache that hangs on to the
+ * post-rendered sixel string for a particular set of cells.
+ */
+ private class SixelCache {
+
+ /**
+ * Maximum size of the cache.
+ */
+ private int maxSize = 100;
+
+ /**
+ * The entries stored in the cache.
+ */
+ private HashMap<String, CacheEntry> cache = null;
+
+ /**
+ * CacheEntry is one entry in the cache.
+ */
+ private class CacheEntry {
+ /**
+ * The cache key.
+ */
+ public String key;
+
+ /**
+ * The cache data.
+ */
+ public String data;
+
+ /**
+ * The last time this entry was used.
+ */
+ public long millis = 0;
+
+ /**
+ * Public constructor.
+ *
+ * @param key the cache entry key
+ * @param data the cache entry data
+ */
+ public CacheEntry(final String key, final String data) {
+ this.key = key;
+ this.data = data;
+ this.millis = System.currentTimeMillis();
+ }
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param maxSize the maximum size of the cache
+ */
+ public SixelCache(final int maxSize) {
+ this.maxSize = maxSize;
+ cache = new HashMap<String, CacheEntry>();
+ }
+
+ /**
+ * Make a unique key for a list of cells.
+ *
+ * @param cells the cells
+ * @return the key
+ */
+ private String makeKey(final ArrayList<Cell> cells) {
+ StringBuilder sb = new StringBuilder();
+ for (Cell cell: cells) {
+ sb.append(cell.hashCode());
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Get an entry from the cache.
+ *
+ * @param cells the list of cells that are the cache key
+ * @return the sixel string representing these cells, or null if this
+ * list of cells is not in the cache
+ */
+ public String get(final ArrayList<Cell> cells) {
+ CacheEntry entry = cache.get(makeKey(cells));
+ if (entry == null) {
+ return null;
+ }
+ entry.millis = System.currentTimeMillis();
+ return entry.data;
+ }
+
+ /**
+ * Put an entry into the cache.
+ *
+ * @param cells the list of cells that are the cache key
+ * @param data the sixel string representing these cells
+ */
+ public void put(final ArrayList<Cell> cells, final String data) {
+ String key = makeKey(cells);
+
+ // System.err.println("put() " + key + " size " + cache.size());
+
+ assert (!cache.containsKey(key));
+
+ assert (cache.size() <= maxSize);
+ if (cache.size() == maxSize) {
+ // Cache is at limit, evict oldest entry.
+ long oldestTime = Long.MAX_VALUE;
+ String keyToRemove = null;
+ for (CacheEntry entry: cache.values()) {
+ if ((entry.millis < oldestTime) || (keyToRemove == null)) {
+ keyToRemove = entry.key;
+ oldestTime = entry.millis;
+ }
+ }
+ /*
+ System.err.println("put() remove key = " + keyToRemove +
+ " size " + cache.size());
+ */
+ assert (keyToRemove != null);
+ cache.remove(keyToRemove);
+ /*
+ System.err.println("put() removed, size " + cache.size());
+ */
+ }
+ assert (cache.size() <= maxSize);
+ CacheEntry entry = new CacheEntry(key, data);
+ assert (key.equals(entry.key));
+ cache.put(key, entry);
+ /*
+ System.err.println("put() added key " + key + " " +
+ " size " + cache.size());
+ */
+ }
+
+ }
+