Revision: 20097 http://sourceforge.net/p/jmol/code/20097 Author: hansonr Date: 2014-11-10 02:35:52 +0000 (Mon, 10 Nov 2014) Log Message: ----------- Jmol.___JmolVersion="14.2.7_2014.11.09"
bug fix: GIF writer not properly handling large numbers of colors -- use of CIE L*a*b for color quantification -- nearly identical to GIMP -- uses MEAN_cut (not MEDIAN_cut) -- uses Floyd-Steinberg dithering -- will not discolor background (as GIMP will do) Modified Paths: -------------- branches/v14_2/Jmol/src/javajs/img/GifEncoder.java branches/v14_2/Jmol/src/javajs/img/ImageEncoder.java branches/v14_2/Jmol/src/javajs/util/CU.java branches/v14_2/Jmol/src/org/jmol/modelset/BondCollection.java branches/v14_2/Jmol/src/org/jmol/modelset/ModelSet.java branches/v14_2/Jmol/src/org/jmol/script/ScriptExpr.java branches/v14_2/Jmol/src/org/jmol/script/ScriptMathProcessor.java branches/v14_2/Jmol/src/org/jmol/scriptext/CmdExt.java branches/v14_2/Jmol/src/org/jmol/scriptext/MathExt.java branches/v14_2/Jmol/src/org/jmol/viewer/Jmol.properties branches/v14_2/Jmol/src/org/jmol/viewer/OutputManager.java Modified: branches/v14_2/Jmol/src/javajs/img/GifEncoder.java =================================================================== --- branches/v14_2/Jmol/src/javajs/img/GifEncoder.java 2014-11-10 02:28:42 UTC (rev 20096) +++ branches/v14_2/Jmol/src/javajs/img/GifEncoder.java 2014-11-10 02:35:52 UTC (rev 20097) @@ -22,8 +22,11 @@ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. */ +// Final encoding code from http://acme.com/resources/classes/Acme/JPM/Encoders/GifEncoder.java +// // GifEncoder - write out an image as a GIF // +// // Transparency handling and variable bit size courtesy of Jack Palevich. // // Copyright (C)1996,1998 by Jef Poskanzer <j...@mail.acme.com>. All rights reserved. @@ -59,135 +62,100 @@ // <P> // @see ToGif - package javajs.img; +import javajs.util.CU; import javajs.util.Lst; -import java.util.Collections; -import java.util.Comparator; +import javajs.util.M3; +import javajs.util.P3; + import java.util.Hashtable; import java.util.Map; import java.io.IOException; /** * - * GifEncoder extensively modified for Jmol by Bob Hanson + * GifEncoder extensively adapted for Jmol by Bob Hanson * + * Color quantization roughly follows the GIMP method + * "dither Floyd-Steinberg standard" but with some twists. + * (For example, we exclude the background color.) + * + * Note that although GIMP code annotation refers to "median-cut", + * it is really using MEAN-cut. That is what I use here as well. + * + * -- commented code allows visualization of the color space using Jmol. Very + * enlightening! + * * -- much simplified interface with ImageEncoder * - * -- uses simple Hashtable with Integer() + * -- uses simple Hashtable with Integer() to catalog colors * - * -- adds adaptive color reduction to generate 256 colors - * Reduction algorithm simply removes lower bits of red, green, and blue - * one at a time until the number of sets is <= 256. Then it creates a - * color for each set that is a weighted average of all the colors for that set. - * Seems to work reasonably well. Mapped isosurfaces look pretty crude. - * * -- allows progressive production of animated GIF via Jmol CAPTURE command * * -- uses general purpose javajs.util.OutputChannel for byte-handling options - * such as posting to a server, writing to disk, and retrieving bytes. - * + * such as posting to a server, writing to disk, and retrieving bytes. + * * -- allows JavaScript port - * - * -- Bob Hanson, 24 Sep 2013 - * + * + * -- Bob Hanson, first try: 24 Sep 2013; final coding: 9 Nov 2014 + * + * * @author Bob Hanson hans...@stolaf.edu */ public class GifEncoder extends ImageEncoder { - private Map<Integer, AdaptiveColorCollection> colorMap; - protected int[] red, green, blue; + private Map<String, Object> params; + private P3[] palette; + private int backgroundColor; - private class ColorItem { - - AdaptiveColorCollection acc; - int rgb; - int count; - - ColorItem(int rgb, int count) { - this.rgb = rgb; - this.count = count; - } - } - - protected class ColorVector extends Lst<ColorItem> { - - void sort() { - CountComparator comparator = new CountComparator(); - Collections.sort(this, comparator); - } - - protected class CountComparator implements Comparator<ColorItem> { - @Override - public int compare(ColorItem a, ColorItem b) { - return (a == null ? 1 : b == null ? -1 : a.count < b.count ? -1 - : a.count > b.count ? 1 : 0); - } - } - } - - private class AdaptiveColorCollection { - //int rgb; - protected int index; - // counts here are counts of color occurances for this grouped set. - // ints here allow for 2147483647/0x100 = count of 8388607 for THIS average color, which should be fine. - private int r; - private int g; - private int b; - private int count; - - AdaptiveColorCollection(int rgb, int index) { - //this.rgb = rgb; - this.index = index; - if (rgb >= 0) - transparentIndex = index; - } - void addRgb(int rgb, int count) { - this.count += count; - b += (rgb & 0xFF) * count; - g += ((rgb >> 8) & 0xFF) * count; - r += ((rgb >> 16) & 0xFF) * count; - } - - void setRgb() { - red[index] = (r / count) & 0xff; - green[index] =(g / count) & 0xff; - blue[index] = (b / count) & 0xff; - } - } - private boolean interlaced; private boolean addHeader = true; private boolean addImage = true; private boolean addTrailer = true; + private boolean isTransparent; + private boolean floydSteinberg = true; + private boolean capturing; + private boolean looping; + private int delayTime100ths = -1; - private boolean looping; - private Map<String, Object> params; + private int bitsPerPixel = 1; + private int byteCount; /** - * we allow for animated GIF by being able to re-enter - * the code with different parameters held in params + * we allow for animated GIF by being able to re-enter the code with different + * parameters held in params * * */ @Override protected void setParams(Map<String, Object> params) { this.params = params; + Integer ic = (Integer) params.get("transparentColor"); + if (ic == null) { + ic = (Integer) params.get("backgroundColor"); + if (ic != null) + backgroundColor = ic.intValue(); + } else { + backgroundColor = ic.intValue(); + isTransparent = true; + } + interlaced = (Boolean.TRUE == params.get("interlaced")); - if (interlaced || !params.containsKey("captureMode")) + if (params.containsKey("captureRootExt") // file0000.gif + || !params.containsKey("captureMode")) // animated gif return; + interlaced = false; + capturing = true; try { byteCount = ((Integer) params.get("captureByteCount")).intValue(); } catch (Exception e) { // ignore } - int imode = "maec".indexOf(((String) params.get("captureMode")).substring(0, 1)); - if (logging) - System.out.println("GIF capture mode " + imode); - switch (imode) { + switch ("maec" + .indexOf(((String) params.get("captureMode")).substring(0, 1))) { case 0: //"movie" params.put("captureMode", "add"); addImage = false; @@ -197,7 +165,7 @@ addHeader = false; addTrailer = false; int fps = Math.abs(((Integer) params.get("captureFps")).intValue()); - delayTime100ths = (fps == 0 ? 0 : 100 / fps); + delayTime100ths = (fps == 0 ? 0 : 100 / fps); looping = (Boolean.FALSE != params.get("captureLooping")); break; case 2: // end @@ -212,21 +180,13 @@ } } - - // Adapted from ppmtogif, which is based on GIFENCOD by David - // Rowley <mga...@watdscu.waterloo.edu>. Lempel-Zim compression - // based on "compress". - - private int bitsPerPixel = 1; - protected int transparentIndex = -1; - @Override protected void generate() throws IOException { if (addHeader) writeHeader(); addHeader = false; // only one header if (addImage) { - createColorTable(); + createPalette(); writeGraphicControlExtension(); if (delayTime100ths >= 0 && looping) writeNetscapeLoopExtension(); @@ -241,214 +201,593 @@ } else { doClose = false; } - params.put("captureByteCount", Integer.valueOf(byteCount)); + if (capturing) + params.put("captureByteCount", Integer.valueOf(byteCount)); } - /** - * includes logical screen descriptor - * @throws IOException - */ - private void writeHeader() throws IOException { - putString("GIF89a"); - putWord(width); - putWord(height); - putByte(0); // no global color table -- using local instead - putByte(0); // no background - putByte(0); // no pixel aspect ratio given + ////////////// 256-color quantization ////////////// + + private class ColorItem { + + int rgb; + P3 lab; + + ColorItem(int rgb) { + this.rgb = rgb; + lab = toLAB(rgb); + } + + @Override + public String toString() { + return Integer.toHexString(rgb) + " " + lab; + } } + private class ColorCell { + protected int index; + // counts here are counts of color occurances for this grouped set. + // ints here allow for 2147483647/0x100 = count of 8388607 for THIS average color, which should be fine. + protected P3 lab; + // min and max based on 0 0 0 for this rgb +// private float maxr = Integer.MAX_VALUE, minr = -Integer.MAX_VALUE, +// maxg = Integer.MAX_VALUE, ming = -Integer.MAX_VALUE, +// maxb = Integer.MAX_VALUE, minb = -Integer.MAX_VALUE; +// private float rmaxr = -Integer.MAX_VALUE, rminr = Integer.MAX_VALUE, +// rmaxg = -Integer.MAX_VALUE, rming = Integer.MAX_VALUE, +// rmaxb = -Integer.MAX_VALUE, rminb = Integer.MAX_VALUE; + int rgb; + Lst<ColorItem> lst; + private float volume; + + ColorCell(int index) { + this.index = index; + lst = new Lst<ColorItem>(); + } + + public float getVolume() { + if (volume != 0) + return volume; + if (lst.size() < 2) + return -1; + //if (true) + //return lst.size(); + //float d; + float maxx = -Integer.MAX_VALUE; + float minx = Integer.MAX_VALUE; + float maxy = -Integer.MAX_VALUE; + float miny = Integer.MAX_VALUE; + float maxz = -Integer.MAX_VALUE; + float minz = Integer.MAX_VALUE; + int n = lst.size(); + for (int i = n; --i >= 0;) { + P3 xyz = lst.get(i).lab; + if (xyz.x < minx) + minx = xyz.x; + if (xyz.y < miny) + miny = xyz.y; + if (xyz.z < minz) + minz = xyz.z; + if (xyz.x > maxx) + maxx = xyz.x; + if (xyz.y > maxy) + maxy = xyz.y; + if (xyz.z > maxz) + maxz = xyz.z; + } + float dx = (maxx - minx); + float dy = (maxy - miny); + float dz = (maxz - minz); + return volume = dx * dx + dy * dy + dz * dz; + } + + void addItem(ColorItem c) { + lst.addLast(c); + } + + /** + * Set the average L*a*b value for this box + * + * @return RGB point + * + */ + protected P3 setColor() { + int count = lst.size(); + lab = new P3(); + for (int i = count; --i >= 0;) { + lab.add(lst.get(i).lab); + } + lab.scale(1f / count); + P3 ptrgb = toRGB(lab); + rgb = CU.colorPtToFFRGB(ptrgb); + + //for (int i = 0; i < count; i++) { + // drawPt(index, i+1, lst.get(i).rgb, false); + // } + + // drawPt(index, 0, rgb, true); + // System.out.println("boundbox corners { " + Math.max(minr, 0) + " " + // + Math.max(ming, 0) + " " + Math.max(minb, 0) + "}{ " + // + Math.min(maxr, 100) + " " + Math.min(maxg, 100) + " " + // + Math.min(maxb, 100) + "}"); + // System.out.println("draw d" + index + " boundbox color " + // + CU.colorPtFromInt(rgb, null) + " mesh nofill"); + // System.out.println("//" + index + " " + volume); + + //System.out.println(index + " " + Integer.toHexString(rgb) + " " + ptrgb + " " + xyz + " " + (maxr - minr)+ " " + (maxg - ming) + " " + (maxb-minb)); + return ptrgb; + } + + /** + * use median_cut algorithm to split the box, creating a doubly linked list. + * + * Paul Heckbert, MIT thesis COLOR IMAGE QUANTIZATION FOR FRAME BUFFER + * DISPLAY https://www.cs.cmu.edu/~ph/ciq_thesis + * + * except, as in GIMP, we use mean, not median here. + * + * @param boxes + * @return true if split + */ + protected boolean splitBox(Lst<ColorCell> boxes) { + int n = lst.size(); + if (n < 2) + return false; + int newIndex = boxes.size(); + ColorCell newBox = new ColorCell(newIndex); + boxes.addLast(newBox); + float[][] ranges = new float[3][3]; + for (int ic = 0; ic < 3; ic++) { + float low = Float.MAX_VALUE; + float high = -Float.MAX_VALUE; + for (int i = lst.size(); --i >= 0;) { + P3 lab = lst.get(i).lab; + float v = (ic == 0 ? lab.x : ic == 1 ? lab.y : lab.z); + if (low > v) + low = v; + if (high < v) + high = v; + } + ranges[0][ic] = low; + ranges[1][ic] = high; + ranges[2][ic] = high - low; + } + float[] r = ranges[2]; + int mode = (r[0] >= r[1] ? (r[0] >= r[2] ? 0 : 2) + : r[1] >= r[2] ? 1 : 2); + // NOTE: GIMP does not use median! uses mean instead; + + // int median = n / 2; + // float val = a[median]; + // int dir = (val == a[0] ? 1 : -1); + // while (median >= 0 && median < n && a[median] == val) { + // median += dir; + // } + // if (dir == -1) + // median++; + // val = a[median]; + + float val = ranges[0][mode] + ranges[2][mode] / 2; + +// newBox.minr = minr; +// newBox.ming = ming; +// newBox.minb = minb; +// newBox.maxr = maxr; +// newBox.maxg = maxg; +// newBox.maxb = maxb; + volume = 0; + + switch (mode) { + case 0: + for (int i = lst.size(); --i >= 0;) + if (lst.get(i).lab.x >= val) + newBox.addItem(lst.remove(i)); +// maxr = val - 0.001f; +// newBox.minr = val; + break; + case 1: + for (int i = lst.size(); --i >= 0;) + if (lst.get(i).lab.y >= val) + newBox.addItem(lst.remove(i)); +// maxg = val - 0.001f; +// newBox.ming = val; + break; + case 2: + for (int i = lst.size(); --i >= 0;) + if (lst.get(i).lab.z >= val) + newBox.addItem(lst.remove(i)); +// maxb = val - 0.001f; +// newBox.minb = val; + break; + } + return true; + } + + @Override + public String toString() { + return index + " " + Integer.toHexString(rgb); + } + } + /** - * generates a 256-color or fewer color table consisting of a - * set of red, green, blue arrays and a hash table pointing to a color index; - * adapts to situations where more than 256 colors are present. + * Generate a palette and quantize all colors into it. * */ - private void createColorTable() { - ColorVector colors = getColors(); - Map<Integer, AdaptiveColorCollection> colors256 = getBest256(colors); - int nTotal = colors256.size(); + private void createPalette() { + Lst<ColorItem> colors = new Lst<ColorItem>(); + Map<Integer, ColorItem> ciHash = new Hashtable<Integer, ColorItem>(); + for (int i = 0, n = pixels.length; i < n; i++) { + int rgb = pixels[i]; + Integer key = Integer.valueOf(rgb); + ColorItem item = ciHash.get(key); + if (item == null) { + item = new ColorItem(rgb); + ciHash.put(key, item); + colors.addLast(item); + } + } + ciHash = null; + int nTotal = colors.size(); + System.out.println("GIF total image colors: " + nTotal); bitsPerPixel = (nTotal <= 2 ? 1 : nTotal <= 4 ? 2 : nTotal <= 16 ? 4 : 8); - colorMap = finalizeColorMap(colors, colors256); + palette = new P3[1 << bitsPerPixel]; + quantizeColors(colors); } /** - * Generate a list of all unique colors in the image. - * - * @return the vector + * Quantize colors by generating a set of boxes containing all colors. + * Start with just two boxes -- fixed background color and all others. + * Keep splitting boxes while there are fewer than 256 and some with + * multiple colors in them. + * + * It is possible that we will end up with fewer than 256 colors. + * + * @param colors */ - private ColorVector getColors() { - ColorVector colorVector = new ColorVector(); - Map<Integer, ColorItem> ciHash = new Hashtable<Integer, ColorItem>(); - int nColors = 0; - Integer key; - int ptTransparent = -1; - - for (int pt = 0, row = 0, transparentRgb = -1; row < height; ++row) { - for (int col = 0; col < width; ++col, pt++) { - int rgb = pixels[pt]; - boolean isTransparent = (rgb >= 0); - if (isTransparent) { - if (ptTransparent < 0) { - // First transparent color; remember it. - ptTransparent = nColors; - transparentRgb = rgb; - } else if (rgb != transparentRgb) { - // A second transparent color; replace it with - // the first one. - pixels[pt] = rgb = transparentRgb; - } + private void quantizeColors(Lst<ColorItem> colors) { + Map<Integer, ColorCell> colorMap = new Hashtable<Integer, ColorCell>(); + Lst<ColorCell> boxes = new Lst<ColorCell>(); + ColorCell cc = new ColorCell(0); + cc.addItem(new ColorItem(backgroundColor)); + boxes.addLast(cc); + boxes.addLast(cc = new ColorCell(1)); + for (int i = colors.size(); --i >= 0;) { + ColorItem c = colors.get(i); + if (c.rgb != backgroundColor) + cc.addItem(c); + } + colors.clear(); + int n; + while ((n = boxes.size()) < 256) { + float maxVol = 0; + ColorCell maxCell = null; + for (int i = n; --i >= 1;) { + ColorCell b = boxes.get(i); + float v = b.getVolume(); + if (v > maxVol) { + maxVol = v; + maxCell = b; } - ColorItem item = ciHash.get(key = Integer.valueOf(rgb)); - if (item == null) { - item = new ColorItem(rgb, 1); - ciHash.put(key, item); - colorVector.addLast(item); - nColors++; - } else { - item.count++; - } } + if (maxCell == null || !maxCell.splitBox(boxes)) + break; } - ciHash = null; - - if (logging) - System.out.println("# total image colors = " + nColors); - // sort by frequency - colorVector.sort(); - return colorVector; + for (int i = 0; i < n; i++) { + ColorCell b = boxes.get(i); + palette[i] = b.setColor(); + colorMap.put(Integer.valueOf(b.rgb), b); + } + System.out.println("GIF final color count: " + boxes.size()); + quantizePixels(colorMap, boxes); } /** - * reduce GIF color collection to 256 or fewer by grouping shadings; - * create an initial color hash that is only to the final colors. * - * @param colorVector - * @return nTotal; + * Assign all colors to their closest approximation and + * change pixels[] array to index values. + * + * Floyd-Steinberg dithering, with error limiting to 75%. Finds the closest + * known color and then spreads out the error over four leading pixels. + * + * @param colorMap + * @param boxes + * */ - private Map<Integer, AdaptiveColorCollection> getBest256(ColorVector colorVector) { - // mask allows reducing colors by shading changes - int mask = 0x010101; - int nColors = colorVector.size(); - int nMax = Math.max(nColors - 1, 0); // leave top 1 untouched - int nTotal = Integer.MAX_VALUE; - int index = 0; - Map<Integer, AdaptiveColorCollection> ht = null; - while (nTotal > 255) { - nTotal = nColors; - index = 0; - ht = new Hashtable<Integer, AdaptiveColorCollection>(); - for (int i = 0; i < nMax; i++) { - ColorItem item = colorVector.get(i); - int rgb = (nTotal < 256 ? item.rgb : item.rgb & ~mask); - Integer key = Integer.valueOf(rgb); - if ((item.acc = ht.get(key)) == null) - ht.put(key, item.acc = new AdaptiveColorCollection(rgb, index++)); - else - nTotal--; + private void quantizePixels(Map<Integer, ColorCell> colorMap, Lst<ColorCell> boxes) { + P3[] pixelErr = new P3[pixels.length]; + P3 err = new P3(); + P3 lab; + int rgb; + for (int i = 0, p = 0; i < height; ++i) { + boolean notLastRow = (i != height - 1); + for (int j = 0; j < width; ++j, p++) { + if (pixelErr[p] == null) { + lab = null; + rgb = pixels[p]; + } else { + lab = toLAB(pixels[p]); + err = pixelErr[p]; // it does not matter that we repurpose errors[p] here. + err.x = clamp(err.x, -75, 75); + err.y = clamp(err.y, -75, 75); + err.z = clamp(err.z, -75, 75); + lab.add(err); + rgb = CU.colorPtToFFRGB(toRGB(lab)); + } + ColorCell app = colorMap.get(Integer.valueOf(rgb)); + if (app == null) { + if (lab == null) + lab = toLAB(pixels[p]); + // find nearest cell + float maxerr = Float.MAX_VALUE; + // skip 0 0 0 + for (int ib = boxes.size(); --ib >= 1;) { + ColorCell b = boxes.get(ib); + err.sub2(lab, b.lab); + float d = err.lengthSquared(); + if (d < maxerr) { + maxerr = d; + app = b; + } + } + err.sub2(lab, app.lab); + + if (floydSteinberg) { + // dither + boolean notLastCol = (j < width - 1); + if (notLastCol) + addError(err, 7, pixelErr, p + 1); + if (notLastRow) { + if (j > 0) + addError(err, 3, pixelErr, p + width - 1); + addError(err, 5, pixelErr, p + width); + if (notLastCol) + addError(err, 1, pixelErr, p + width + 1); + } + } + } + pixels[p] = app.index; } - mask |= (mask <<= 1); - //if (Logger.debugging) } - ColorItem item = colorVector.get(nMax); - ht.put(Integer.valueOf(item.rgb), - item.acc = new AdaptiveColorCollection(item.rgb, index++)); - if (logging) - System.out.println("# GIF colors = " + ht.size()); - return ht; } + private void addError(P3 err, int f, P3[] pixelErr, int p) { + if (pixels[p] == backgroundColor) + return; + P3 errp = pixelErr[p]; + if (errp == null) + errp = pixelErr[p] = new P3(); + errp.scaleAdd2(f / 16f, err, errp); + } + + // protected void drawPt(int index, int i, int rgb, boolean isMain) { + // P3 pt = toLAB(rgb); + // System.out.println("draw id 'd" + index + "_" + i + "' width " + // + (isMain ? 1.0 : 0.2) + " " + pt + " color " + // + CU.colorPtFromInt(rgb, null) + " '" + index + "'"); + // } + + ///////////////////////// CIE L*a*b / XYZ / sRGB conversion methods ///////// + + + // these could be static, but that just makes for more JavaScript code + + protected P3 toLAB(int rgb) { + P3 lab = CU.colorPtFromInt(rgb, null); + rgbToXyz(lab, lab); + xyzToLab(lab, lab); + // normalize to 0-100 + lab.y = (lab.y + 86.185f) / (98.254f + 86.185f) * 100f; + lab.z = (lab.z + 107.863f) / (94.482f + 107.863f) * 100f; + return lab; + } + + protected P3 toRGB(P3 lab) { + P3 xyz = P3.newP(lab); + // normalized to 0-100 + xyz.y = xyz.y / 100f * (98.254f + 86.185f) - 86.185f; + xyz.z = xyz.z / 100f * (94.482f + 107.863f) - 107.863f; + labToXyz(xyz, xyz); + return xyzToRgb(xyz, xyz); + } + + private static M3 xyz2rgb; + private static M3 rgb2xyz; + + static { + rgb2xyz = M3.newA9(new float[] { 0.4124f, 0.3576f, 0.1805f, 0.2126f, + 0.7152f, 0.0722f, 0.0193f, 0.1192f, 0.9505f }); + + xyz2rgb = M3.newA9(new float[] { 3.2406f, -1.5372f, -0.4986f, -0.9689f, + 1.8758f, 0.0415f, 0.0557f, -0.2040f, 1.0570f }); + } + + private P3 rgbToXyz(P3 rgb, P3 xyz) { + // http://en.wikipedia.org/wiki/CIE_1931_color_space + // http://rsb.info.nih.gov/ij/plugins/download/Color_Space_Converter.java + if (xyz == null) + xyz = new P3(); + xyz.x = srgb(rgb.x); + xyz.y = srgb(rgb.y); + xyz.z = srgb(rgb.z); + rgb2xyz.rotate(xyz); + return xyz; + } + + private float srgb(float x) { + x /= 255; + return (float) (x <= 0.04045 ? x / 12.92 : Math.pow(((x + 0.055) / 1.055), + 2.4)) * 100; + } + + private P3 xyzToRgb(P3 xyz, P3 rgb) { + // http://en.wikipedia.org/wiki/CIE_1931_color_space + // http://rsb.info.nih.gov/ij/plugins/download/Color_Space_Converter.java + if (rgb == null) + rgb = new P3(); + rgb.setT(xyz); + rgb.scale(0.01f); + xyz2rgb.rotate(rgb); + rgb.x = clamp(sxyz(rgb.x), 0, 255); + rgb.y = clamp(sxyz(rgb.y), 0, 255); + rgb.z = clamp(sxyz(rgb.z), 0, 255); + return rgb; + } + + private float sxyz(float x) { + return (float) (x > 0.0031308f ? (1.055 * Math.pow(x, 1.0 / 2.4)) - 0.055 + : x * 12.92) * 255; + } + + private P3 xyzToLab(P3 xyz, P3 lab) { + // http://en.wikipedia.org/wiki/Lab_color_space + // http://rsb.info.nih.gov/ij/plugins/download/Color_Space_Converter.java + // Lab([0..100], [-86.185..98.254], [-107.863..94.482]) + // XYZn = D65 = {95.0429, 100.0, 108.8900}; + if (lab == null) + lab = new P3(); + float x = flab(xyz.x / 95.0429f); + float y = flab(xyz.y / 100); + float z = flab(xyz.z / 108.89f); + lab.x = (116 * y) - 16; + lab.y = 500 * (x - y); + lab.z = 200 * (y - z); + return lab; + } + + private float flab(float t) { + return (float) (t > 8.85645168E-3 /* (24/116)^3 */? Math.pow(t, + 0.333333333) : 7.78703704 /* 1/3*116/24*116/24 */* t + 0.137931034 /* 16/116 */ + ); + } + + private P3 labToXyz(P3 lab, P3 xyz) { + // http://en.wikipedia.org/wiki/Lab_color_space + // http://rsb.info.nih.gov/ij/plugins/download/Color_Space_Converter.java + // XYZn = D65 = {95.0429, 100.0, 108.8900}; + if (xyz == null) + xyz = new P3(); + + xyz.setT(lab); + float y = (xyz.x + 16) / 116; + float x = xyz.y / 500 + y; + float z = y - xyz.z / 200; + xyz.x = fxyz(x) * 95.0429f; + xyz.y = fxyz(y) * 100; + xyz.z = fxyz(z) * 108.89f; + + return xyz; + } + + private float fxyz(float t) { + return (float) (t > 0.206896552 /* (24/116) */? t * t * t + : 0.128418549 /* 3*24/116*24/116 */* (t - 0.137931034 /* 16/116 */)); + } + + private float clamp(float c, float min, float max) { + return Math.round(c < min ? min : c > max ? max : c); + } + + //static { + // P3 x; + // x = rgbToXyz(P3.new3(0,0,0), null); + // System.out.println("xyz="+x); + // x = xyzToLab(x, x); + // System.out.println("lab="+x); + // x = labToXyz(x, x); + // System.out.println(x); + // x = xyzToRgb(x, x); + // System.out.println(x); + // + // x = rgbToXyz(P3.new3(254,1,253), null); + // System.out.println("xyz="+x); + // x = xyzToLab(x, x); + // System.out.println("lab="+x); + // x = labToXyz(x, x); + // System.out.println(x); + // x = xyzToRgb(x, x); + // System.out.println(x); + // + // System.out.println(toRGB(toLAB(CU.colorPtToFFRGB(P3.new3(200,100,50))))); + //} + + ///////////////////////// GifEncoder writing methods //////////////////////// + /** - * Create final color table red green blue arrays and generate final - * colorHash. + * includes logical screen descriptor * - * @param colors - * @param colors256 - * @return map from all unique colors to a specific index + * @throws IOException */ - private Map<Integer, AdaptiveColorCollection> finalizeColorMap( - Lst<ColorItem> colors, - Map<Integer, AdaptiveColorCollection> colors256) { - int mapSize = 1 << bitsPerPixel; - red = new int[mapSize]; - green = new int[mapSize]; - blue = new int[mapSize]; - int nColors = colors.size(); - Map<Integer, AdaptiveColorCollection> ht = new Hashtable<Integer, AdaptiveColorCollection>(); - for (int i = 0; i < nColors; i++) { - ColorItem item = colors.get(i); - int rgb = item.rgb; - item.acc.addRgb(rgb, item.count); - ht.put(Integer.valueOf(rgb), item.acc); - } - for (AdaptiveColorCollection acc : colors256.values()) - acc.setRgb(); - return ht; + private void writeHeader() throws IOException { + putString("GIF89a"); + putWord(width); + putWord(height); + putByte(0); // no global color table -- using local instead + putByte(0); // no background + putByte(0); // no pixel aspect ratio given } private void writeGraphicControlExtension() { - if (transparentIndex != -1 || delayTime100ths >= 0) { + if (isTransparent || delayTime100ths >= 0) { putByte(0x21); // graphic control extension putByte(0xf9); // graphic control label putByte(4); // block size - int packedBytes = (transparentIndex == -1 ? 0 : 1) | (delayTime100ths > 0 ? 2 : 0); - putByte(packedBytes); + putByte((isTransparent ? 9 : 0) | (delayTime100ths > 0 ? 2 : 0)); // packed bytes putWord(delayTime100ths > 0 ? delayTime100ths : 0); - putByte(transparentIndex == -1 ? 0 : transparentIndex); + putByte(0); // transparent index putByte(0); // end-of-block } } -// see http://www.vurdalakov.net/misc/gif/netscape-looping-application-extension -// +---------------+ -// 0 | 0x21 | Extension Label -// +---------------+ -// 1 | 0xFF | Application Extension Label -// +---------------+ -// 2 | 0x0B | Block Size -// +---------------+ -// 3 | | -// +- -+ -// 4 | | -// +- -+ -// 5 | | -// +- -+ -// 6 | | -// +- NETSCAPE -+ Application Identifier (8 bytes) -// 7 | | -// +- -+ -// 8 | | -// +- -+ -// 9 | | -// +- -+ -// 10 | | -// +---------------+ -// 11 | | -// +- -+ -// 12 | 2.0 | Application Authentication Code (3 bytes) -// +- -+ -// 13 | | -// +===============+ --+ -// 14 | 0x03 | Sub-block Data Size | -// +---------------+ | -// 15 | 0x01 | Sub-block ID | -// +---------------+ | Application Data Sub-block -// 16 | | | -// +- -+ Loop Count (2 bytes) | -// 17 | | | -// +===============+ --+ -// 18 | 0x00 | Block Terminator -// +---------------+ + // see http://www.vurdalakov.net/misc/gif/netscape-looping-application-extension + // +---------------+ + // 0 | 0x21 | Extension Label + // +---------------+ + // 1 | 0xFF | Application Extension Label + // +---------------+ + // 2 | 0x0B | Block Size + // +---------------+ + // 3 | | + // +- -+ + // 4 | | + // +- -+ + // 5 | | + // +- -+ + // 6 | | + // +- NETSCAPE -+ Application Identifier (8 bytes) + // 7 | | + // +- -+ + // 8 | | + // +- -+ + // 9 | | + // +- -+ + // 10 | | + // +---------------+ + // 11 | | + // +- -+ + // 12 | 2.0 | Application Authentication Code (3 bytes) + // +- -+ + // 13 | | + // +===============+ --+ + // 14 | 0x03 | Sub-block Data Size | + // +---------------+ | + // 15 | 0x01 | Sub-block ID | + // +---------------+ | Application Data Sub-block + // 16 | | | + // +- -+ Loop Count (2 bytes) | + // 17 | | | + // +===============+ --+ + // 18 | 0x00 | Block Terminator + // +---------------+ private void writeNetscapeLoopExtension() { putByte(0x21); // graphic control extension putByte(0xff); // netscape loop extension putByte(0x0B); // block size putString("NETSCAPE2.0"); - putByte(3); - putByte(1); + putByte(3); + putByte(1); putWord(0); // loop indefinitely putByte(0); // end-of-block - + } private int initCodeSize; @@ -472,10 +811,13 @@ int packedFields = 0x80 | (interlaced ? 0x40 : 0) | (bitsPerPixel - 1); putByte(packedFields); int colorMapSize = 1 << bitsPerPixel; + P3 p = new P3(); for (int i = 0; i < colorMapSize; i++) { - putByte(red[i]); - putByte(green[i]); - putByte(blue[i]); + if (palette[i] != null) + p = palette[i]; + putByte((int) p.x); + putByte((int) p.y); + putByte((int) p.z); } putByte(initCodeSize = (bitsPerPixel <= 1 ? 2 : bitsPerPixel)); compress(); @@ -488,14 +830,14 @@ } ///// compression routines ///// - + private static final int EOF = -1; // Return the next pixel from the image private int nextPixel() { if (countDown-- == 0) return EOF; - int colorIndex = colorMap.get(Integer.valueOf(pixels[curpt])).index; + int colorIndex = pixels[curpt]; // Bump the current X position ++curx; if (curx == width) { @@ -506,26 +848,24 @@ if (interlaced) updateY(INTERLACE_PARAMS[pass], INTERLACE_PARAMS[pass + 4]); else - ++cury; + ++cury; } curpt = cury * width + curx; return colorIndex & 0xff; } - private static final int[] INTERLACE_PARAMS = { - 8, 8, 4, 2, - 4, 2, 1, 0}; + private static final int[] INTERLACE_PARAMS = { 8, 8, 4, 2, 4, 2, 1, 0 }; /** * - * Group 1 : Every 8th. row, starting with row 0. (Pass 1) - * - * Group 2 : Every 8th. row, starting with row 4. (Pass 2) - * - * Group 3 : Every 4th. row, starting with row 2. (Pass 3) - * - * Group 4 : Every 2nd. row, starting with row 1. (Pass 4) + * Group 1 : Every 8th. row, starting with row 0. (Pass 1) * + * Group 2 : Every 8th. row, starting with row 4. (Pass 2) + * + * Group 3 : Every 4th. row, starting with row 2. (Pass 3) + * + * Group 4 : Every 2nd. row, starting with row 1. (Pass 4) + * * @param yNext * @param yNew */ @@ -783,5 +1123,5 @@ bufPt = 0; } } - + } Modified: branches/v14_2/Jmol/src/javajs/img/ImageEncoder.java =================================================================== --- branches/v14_2/Jmol/src/javajs/img/ImageEncoder.java 2014-11-10 02:28:42 UTC (rev 20096) +++ branches/v14_2/Jmol/src/javajs/img/ImageEncoder.java 2014-11-10 02:35:52 UTC (rev 20097) @@ -108,8 +108,9 @@ protected int[] pixels; - protected void putString(String str) { - out.append(str); + protected void putString(String s) { + byte[] b = s.getBytes(); + out.write(b, 0, b.length); } protected void putByte(int b) { Modified: branches/v14_2/Jmol/src/javajs/util/CU.java =================================================================== --- branches/v14_2/Jmol/src/javajs/util/CU.java 2014-11-10 02:28:42 UTC (rev 20096) +++ branches/v14_2/Jmol/src/javajs/util/CU.java 2014-11-10 02:35:52 UTC (rev 20097) @@ -440,8 +440,8 @@ return 0xFF000000 | (red << 16) | (grn << 8) | blu; } - public final static P3 colorPtFromString(String colorName, P3 pt) { - return toRGBpt(getArgbFromString(colorName), pt); + public final static P3 colorPtFromString(String colorName) { + return colorPtFromInt(getArgbFromString(colorName), null); } public final static P3 colorPtFromInt(int color, P3 pt) { @@ -455,13 +455,6 @@ return colorTriadToFFRGB(pt.x, pt.y, pt.z); } - public final static P3 toRGBpt(int color, P3 pt) { - pt.x = (color >> 16) & 0xFF; - pt.y = (color >> 8) & 0xFF; - pt.z = color & 0xFF; - return pt; - } - public static void toRGB3f(int c, float[] f) { f[0] = ((c >> 16) & 0xFF) / 255f; // red f[1] = ((c >> 8) & 0xFF) / 255f; Modified: branches/v14_2/Jmol/src/org/jmol/modelset/BondCollection.java =================================================================== --- branches/v14_2/Jmol/src/org/jmol/modelset/BondCollection.java 2014-11-10 02:28:42 UTC (rev 20096) +++ branches/v14_2/Jmol/src/org/jmol/modelset/BondCollection.java 2014-11-10 02:35:52 UTC (rev 20097) @@ -49,8 +49,6 @@ protected JmolMolecule[] molecules; protected int moleculeCount; - private boolean haveWarned; - protected short defaultCovalentMad; private BS bsAromaticSingle; @@ -171,7 +169,7 @@ return bond; } - private Bond getOrAddBond(Atom atom, Atom atomOther, int order, short mad, + protected Bond getOrAddBond(Atom atom, Atom atomOther, int order, short mad, BS bsBonds, float energy, boolean overrideBonding) { int i; if (order == Edge.BOND_ORDER_NULL || order == Edge.BOND_ORDER_ANY) @@ -267,30 +265,6 @@ return (distance2 > maxAcceptable2 ? (short) 0 : (short) 1); } - protected boolean checkValencesAndBond(Atom atomA, Atom atomB, int order, short mad, - BS bsBonds) { - if (atomA.getCurrentBondCount() > JC.MAXIMUM_AUTO_BOND_COUNT - || atomB.getCurrentBondCount() > JC.MAXIMUM_AUTO_BOND_COUNT) { - if (!haveWarned) - Logger.warn("maximum auto bond count reached"); - haveWarned = true; - return false; - } - int formalChargeA = atomA.getFormalCharge(); - if (formalChargeA != 0) { - int formalChargeB = atomB.getFormalCharge(); - if ((formalChargeA < 0 && formalChargeB < 0) - || (formalChargeA > 0 && formalChargeB > 0)) - return false; - } - // don't connect differing altloc unless there are modulations - if (atomA.altloc != atomB.altloc - && atomA.altloc != '\0' && atomB.altloc != '\0' && getModulation(atomA.i) == null) - return false; - getOrAddBond(atomA, atomB, order, mad, bsBonds, 0, false); - return true; - } - protected void deleteAllBonds2() { vwr.setShapeProperty(JC.SHAPE_STICKS, "reset", null); for (int i = bondCount; --i >= 0;) { Modified: branches/v14_2/Jmol/src/org/jmol/modelset/ModelSet.java =================================================================== --- branches/v14_2/Jmol/src/org/jmol/modelset/ModelSet.java 2014-11-10 02:28:42 UTC (rev 20096) +++ branches/v14_2/Jmol/src/org/jmol/modelset/ModelSet.java 2014-11-10 02:35:52 UTC (rev 20097) @@ -663,14 +663,18 @@ int targetIndex = serialMap[targetSerial] - 1; if (sourceIndex < 0 || targetIndex < 0) continue; + Atom atomA = at[sourceIndex]; + Atom atomB = at[targetIndex]; if (bsExclude != null) { - if (at[sourceIndex].isHetero()) + if (atomA.isHetero()) bsExclude.set(sourceIndex); - if (at[targetIndex].isHetero()) + if (atomB.isHetero()) bsExclude.set(targetIndex); } - checkValencesAndBond(at[sourceIndex], at[targetIndex], order, - (order == Edge.BOND_H_REGULAR ? 1 : mad), null); + // don't connect differing altloc + if (atomA.altloc == atomB.altloc + || atomA.altloc == '\0' || atomB.altloc == '\0') + getOrAddBond(atomA, atomB, order, (order == Edge.BOND_H_REGULAR ? 1 : mad), null, 0, false); } } } @@ -2862,7 +2866,7 @@ atomNear.getBondingRadius(), iter.foundDistance2(), minBondDistance2, bondTolerance); if (order > 0 - && checkValencesAndBond(atom, atomNear, order, mad, bsBonds)) + && autoBondCheck(atom, atomNear, order, mad, bsBonds)) nNew++; } iter.release(); @@ -2872,6 +2876,32 @@ return nNew; } + private boolean maxBondWarned; + + private boolean autoBondCheck(Atom atomA, Atom atomB, int order, + short mad, BS bsBonds) { + if (atomA.getCurrentBondCount() > JC.MAXIMUM_AUTO_BOND_COUNT + || atomB.getCurrentBondCount() > JC.MAXIMUM_AUTO_BOND_COUNT) { + if (!maxBondWarned) + Logger.warn("maximum auto bond count reached"); + maxBondWarned = true; + return false; + } + int formalChargeA = atomA.getFormalCharge(); + if (formalChargeA != 0) { + int formalChargeB = atomB.getFormalCharge(); + if ((formalChargeA < 0 && formalChargeB < 0) + || (formalChargeA > 0 && formalChargeB > 0)) + return false; + } + // don't connect differing altloc unless there are modulations + if (atomA.altloc != atomB.altloc && atomA.altloc != '\0' + && atomB.altloc != '\0' && getModulation(atomA.i) == null) + return false; + getOrAddBond(atomA, atomB, order, mad, bsBonds, 0, false); + return true; + } + private int autoBond_Pre_11_9_24(BS bsA, BS bsB, BS bsExclude, BS bsBonds, short mad) { if (ac == 0) @@ -2945,7 +2975,7 @@ atomNear.getBondingRadius(), iter.foundDistance2(), minBondDistance2, bondTolerance); if (order > 0) { - if (checkValencesAndBond(atom, atomNear, order, mad, bsBonds)) + if (autoBondCheck(atom, atomNear, order, mad, bsBonds)) nNew++; } } Modified: branches/v14_2/Jmol/src/org/jmol/script/ScriptExpr.java =================================================================== --- branches/v14_2/Jmol/src/org/jmol/script/ScriptExpr.java 2014-11-10 02:28:42 UTC (rev 20096) +++ branches/v14_2/Jmol/src/org/jmol/script/ScriptExpr.java 2014-11-10 02:35:52 UTC (rev 20097) @@ -1801,7 +1801,7 @@ } break; case T.color: - CU.toRGBpt(vwr.getColorArgbOrGray(bond.colix), ptT); + CU.colorPtFromInt(vwr.getColorArgbOrGray(bond.colix), ptT); switch (minmaxtype) { case T.all: vout.addLast(P3.newP(ptT)); Modified: branches/v14_2/Jmol/src/org/jmol/script/ScriptMathProcessor.java =================================================================== --- branches/v14_2/Jmol/src/org/jmol/script/ScriptMathProcessor.java 2014-11-10 02:28:42 UTC (rev 20096) +++ branches/v14_2/Jmol/src/org/jmol/script/ScriptMathProcessor.java 2014-11-10 02:35:52 UTC (rev 20097) @@ -981,9 +981,7 @@ switch (x2.tok) { case T.string: case T.varray: - s = SV.sValue(x2); - pt = new P3(); - return addXPt(CU.colorPtFromString(s, pt)); + return addXPt(CU.colorPtFromString(SV.sValue(x2))); case T.integer: case T.decimal: return addXPt(vwr.getColorPointForPropertyValue(SV.fValue(x2))); Modified: branches/v14_2/Jmol/src/org/jmol/scriptext/CmdExt.java =================================================================== --- branches/v14_2/Jmol/src/org/jmol/scriptext/CmdExt.java 2014-11-10 02:28:42 UTC (rev 20096) +++ branches/v14_2/Jmol/src/org/jmol/scriptext/CmdExt.java 2014-11-10 02:35:52 UTC (rev 20097) @@ -497,6 +497,10 @@ fileName += ".gif"; int i = 2; boolean isRock = false; + if (tok == T.loop) { + looping = true; + tok = tokAt(++i); + } switch (tokAt(i)) { case T.rock: isRock = true; @@ -522,14 +526,12 @@ axis = "y"; boolean wf = vwr.g.waitForMoveTo; s = "set waitformoveto true;" + PT.rep(s, "Y", axis) + ";set waitformoveto " + wf; - s = "capture " + PT.esc(fileName) + " -1;" + s + ";capture;"; + s = "capture " + PT.esc(fileName) + " LOOP;" + s + ";capture;"; e.cmdScript(0, null, s); return; case T.decimal: case T.integer: endTime = floatParameter(2); - if (endTime < 0) - looping = true; break; } if (chk) @@ -6075,6 +6077,8 @@ || type.equals("FRAME") || type.equals("VIBRATION")) { type = (fileName != null && fileName.indexOf(".") >= 0 ? fileName .substring(fileName.lastIndexOf(".") + 1).toUpperCase() : "JPG"); + if (PT.isOneOf(type, ";PNGJ;PNGT;")) + fileName = fileName.substring(0, fileName.length() - 1); } if (type.equals("MNU")) { type = "MENU"; Modified: branches/v14_2/Jmol/src/org/jmol/scriptext/MathExt.java =================================================================== --- branches/v14_2/Jmol/src/org/jmol/scriptext/MathExt.java 2014-11-10 02:28:42 UTC (rev 20096) +++ branches/v14_2/Jmol/src/org/jmol/scriptext/MathExt.java 2014-11-10 02:35:52 UTC (rev 20097) @@ -295,17 +295,17 @@ if (args.length == 2 && colorScheme.equalsIgnoreCase("TOHSL")) return mp.addXPt(CU.rgbToHSL(P3.newP(args[1].tok == T.point3f ? SV .ptValue(args[1]) - : CU.colorPtFromString(args[1].asString(), new P3())), true)); + : CU.colorPtFromString(args[1].asString())), true)); if (args.length == 2 && colorScheme.equalsIgnoreCase("TORGB")) { P3 pt = P3.newP(args[1].tok == T.point3f ? SV.ptValue(args[1]) : CU - .colorPtFromString(args[1].asString(), new P3())); + .colorPtFromString(args[1].asString())); return mp.addXPt(args[1].tok == T.point3f ? CU.hslToRGB(pt) : pt); } if (args.length == 4 && (args[3].tok == T.on || args[3].tok == T.off)) { P3 pt1 = P3.newP(args[0].tok == T.point3f ? SV.ptValue(args[0]) : CU - .colorPtFromString(args[0].asString(), new P3())); + .colorPtFromString(args[0].asString())); P3 pt2 = P3.newP(args[1].tok == T.point3f ? SV.ptValue(args[1]) : CU - .colorPtFromString(args[1].asString(), new P3())); + .colorPtFromString(args[1].asString())); boolean usingHSL = (args[3].tok == T.on); if (usingHSL) { pt1 = CU.rgbToHSL(pt1, false); Modified: branches/v14_2/Jmol/src/org/jmol/viewer/Jmol.properties =================================================================== --- branches/v14_2/Jmol/src/org/jmol/viewer/Jmol.properties 2014-11-10 02:28:42 UTC (rev 20096) +++ branches/v14_2/Jmol/src/org/jmol/viewer/Jmol.properties 2014-11-10 02:35:52 UTC (rev 20097) @@ -4,8 +4,17 @@ # THIS IS THE RELEASE BRANCH # BUG FIXES ONLY, PLEASE -Jmol.___JmolVersion="14.2.7_2014.10.27" +Jmol.___JmolVersion="14.2.7_2014.11.09" +bug fix: GIF writer not properly handling large numbers of colors + -- use of CIE L*a*b for color quantification + -- nearly identical to GIMP + -- uses MEAN_cut (not MEDIAN_cut) + -- uses Floyd-Steinberg dithering + -- will not discolor background (as GIMP will do) + +JmolVersion="14.2.7_2014.10.27" + bug fix: up-arrow in console may not return command if contains unicode bug fix: antialiasing can subtly change background color Modified: branches/v14_2/Jmol/src/org/jmol/viewer/OutputManager.java =================================================================== --- branches/v14_2/Jmol/src/org/jmol/viewer/OutputManager.java 2014-11-10 02:28:42 UTC (rev 20096) +++ branches/v14_2/Jmol/src/org/jmol/viewer/OutputManager.java 2014-11-10 02:35:52 UTC (rev 20097) @@ -176,11 +176,12 @@ params.put("pngAppData", stateData); params.put("pngAppPrefix", "Jmol Type"); } - if (type.equals("PNGT")) - params.put("transparentColor", - Integer.valueOf(vwr.getBackgroundArgb())); - type = "PNG"; } + if (type.equals("PNGT") || type.equals("GIFT")) { + params.put("transparentColor", + Integer.valueOf(vwr.getBackgroundArgb())); + type = type.substring(0, 3); + } if (comment != null) params.put("comment", comment.length() == 0 ? Viewer.getJmolVersion() : comment); @@ -623,7 +624,6 @@ if (captureMode != null) { doCheck = false; // will be checked later mustRender = false; - type = "GIF"; } if (doCheck) fileName = getOutputFileNameFromDialog(fileName, quality); This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. ------------------------------------------------------------------------------ _______________________________________________ Jmol-commits mailing list Jmol-commits@lists.sourceforge.net https://lists.sourceforge.net/lists/listinfo/jmol-commits