/* * Copyright (C) 2011. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 or * version 2 as published by the Free Software Foundation. * * 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. */ package uk.me.parabola.imgfmt.app.typ; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import uk.me.parabola.imgfmt.app.BitWriter; import uk.me.parabola.imgfmt.app.ImgFileWriter; import uk.me.parabola.imgfmt.app.Writeable; /** * Holds colour information for elements in the typ file. * * The Colour information can relate to a bitmap or solid shapes. * * @author Steve Ratcliffe */ public class ColourInfo implements Writeable, AlphaAdder { private static final int S_NIGHT = 1; private static final int S_DAY_TRANSPARENT = 0x2; private static final int S_NIGHT_TRANSPARENT = 0x4; private static final int S_HAS_BITMAP = 0x8; private int numberOfColours; private int numberOfSolidColours; private boolean hasBitmap; private boolean hasBorder; private final List<RgbWithTag> colours = new ArrayList<RgbWithTag>(); private final Map<String, Integer> indexMap = new HashMap<String, Integer>(); private char width; private char height; private char charsPerPixel; private boolean simple = true; private char colourMode; /** * Add a colour for this element. * @param tag The xpm tag that represents the colour. * @param rgb The actual colour. */ public void addColour(String tag, Rgb rgb) { RgbWithTag cwt = new RgbWithTag(tag, rgb); colours.add(cwt); } /** * Add a transparent colour. Convenience routine. */ public void addTransparent(String colourTag) { addColour(colourTag, new Rgb(0, 0, 0, 0)); } public void setHasBitmap(boolean hasBitmap) { this.hasBitmap = hasBitmap; } /** * The colour scheme in use. This is a bitmask that has the following bits: * 0 - Has night colour * 1 - day background colour is transparent * 2 - night background colour is transparent * 3 - has bitmap * * If there is no night colour, then set the night background colour bit to be the same as * the day one. * * @return The colour scheme bitmask. The term colour scheme is historical, it doesn't really * describe it. */ public int getColourScheme() { if (numberOfColours == 0) numberOfColours = colours.size(); int scheme = 0; if (hasBitmap) scheme |= S_HAS_BITMAP; if (numberOfColours == 4) scheme |= S_NIGHT; if (!hasBitmap && !hasBorder && numberOfColours == 2) scheme |= S_NIGHT | S_DAY_TRANSPARENT | S_NIGHT_TRANSPARENT; if (numberOfColours < 2 || colours.get(1).isTransparent()) scheme |= S_DAY_TRANSPARENT; if (numberOfColours == 4 && (colours.get(3).isTransparent())) scheme |= S_NIGHT_TRANSPARENT; if ((scheme & S_NIGHT) == 0) if ((scheme & S_DAY_TRANSPARENT) != 0) scheme |= S_NIGHT_TRANSPARENT; return scheme; } /** * Get the number of bits per pixel that will be used in the written bitmap. * * This depends on the colour mode and number of colours to be represented. */ public int getBitsPerPixel() { if (simple) return 1; // number of colours includes the transparent pixel in colormode=0x10 so this // works for all colour modes. int nc = numberOfColours; if (nc == 0) return 24; else if (nc < 2) return 1; else if (nc < 4) return 2; else if (nc < 16) return 4; else return 8; } /** * Write out the colours only. */ public void write(ImgFileWriter writer) { if (colourMode == 0x20) { writeColours20(writer); } else { for (Rgb rgb : colours) { if (!rgb.isTransparent()) rgb.write(writer, (byte) 0x10); } } } /** * Write out the colours in the colormode=x20 case. */ private void writeColours20(ImgFileWriter writer) { BitWriter bw = new BitWriter(); for (Rgb rgb : colours) { bw.putn(rgb.getB(), 8); bw.putn(rgb.getG(), 8); bw.putn(rgb.getR(), 8); int alpha = 0xff - rgb.getA(); alpha = alphaRound4(alpha); bw.putn(alpha, 4); } writer.put(bw.getBytes(), 0, bw.getLength()); } /** * Round alpha value to four bits. * @param alpha The original alpha value eg 0xf0. * @return Rounded alpha to four bits eg 0xe. */ static int alphaRound4(int alpha) { int top = (alpha >> 4) & 0xf; int low = alpha & 0xf; int diff = low-top; if (diff > 8) top++; else if (diff < -8) top--; return top; } public int getIndex(String tag) { Integer ind = indexMap.get(tag); // If this is a simple bitmap (for line or polygon), then the foreground colour is // first and so has index 0, but we want the foreground to have index 1, so reverse. if (simple) ind = ~ind; return ind; } public void setWidth(int width) { this.width = (char) width; } public void setHeight(int height) { this.height = (char) height; } public void setNumberOfColours(int numberOfColours) { this.numberOfColours = numberOfColours; } public void setCharsPerPixel(int charsPerPixel) { this.charsPerPixel = (char) (charsPerPixel == 0 ? 1 : charsPerPixel); } public int getNumberOfColours() { return numberOfColours; } public int getNumberOfSColoursForCM() { if (colourMode == 0x10) return numberOfSolidColours; else return numberOfColours; } public int getCharsPerPixel() { return charsPerPixel; } public int getHeight() { return height; } public int getWidth() { return width; } public int getColourMode() { return colourMode; } public void setColourMode(int colourMode) { this.colourMode = (char) colourMode; } public void setSimple(boolean simple) { this.simple = simple; } public void setHasBorder(boolean hasBorder) { this.hasBorder = hasBorder; } /** * Replace the last pixel with a pixel with the same colour components and the given * alpha. * * This is used when the alpha value is specified separately to the colour values in the * input file. * @param alpha The alpha value to be added to the pixel. This is a real alpha, not a transparency. */ public void addAlpha(int alpha) { int last = colours.size(); RgbWithTag rgb = colours.get(last - 1); rgb = new RgbWithTag(rgb, alpha); colours.set(last - 1, rgb); } /** * Analyse the colour pallet and normalise it. * * Try to work out what is required from the supplied colour pallet and set the colour mode * and rearrange transparent pixels if necessary to be in the proper place. * * At the end we build the index from colour tag to pixel index. * * @param simple If this is a line or polygon. * @return A string describing the validation failure. */ public String analyseColours(boolean simple) { setSimple(simple); if (simple) { // There can be up to four colours, no partial transparency, and a max of one transparent pixel // in each of the day/night sections. if (numberOfColours > 4) return ("Too many colours for a line or polygon"); if (numberOfColours == 0) return "Line or polygon cannot have zero colours"; // Putting the transparent pixel first is common, so reverse if found if (colours.get(0).isTransparent()) { if (numberOfColours < 2) return "Only colour cannot be transparent for line or polygon"; swapColour(0, 1); } if (numberOfColours > 2 && colours.get(2).isTransparent()) { if (numberOfColours < 4) return "Only colour cannot be transparent for line or polygon"; swapColour(2, 3); } // There can only be one transparent pixel per colour pair if (numberOfColours > 1 && colours.get(0).isTransparent()) return "Both day foreground and background are transparent"; if (numberOfColours > 3 && colours.get(2).isTransparent()) return "Both night foreground and background are transparent"; } else { int transIndex = 0; // index of last transparent pixel, only used when there is only one int nTrans = 0; // completely transparent int nAlpha = 0; // partially transparent int count = 0; // total number of colours for (RgbWithTag rgb : colours) { if (rgb.isTransparent()) { nTrans++; transIndex = count; } if (rgb.getA() != 0xff && rgb.getA() != 0) nAlpha++; count++; } if (nAlpha > 0 || (count > 0 && count == nTrans)) { // If there is any partial transparency we need colour mode 0x20 // Also if there is only one pixel and it is transparent, since otherwise there would be zero // solid colours and that is a special case used to indicate a true colour pixmap. colourMode = 0x20; } else if (nTrans == 1) { colourMode = 0x10; // Ensure the transparent pixel is at the end RgbWithTag rgb = colours.remove(transIndex); colours.add(rgb); } } int count = 0; for (RgbWithTag rgb : colours) { indexMap.put(rgb.getTag(), count++); if (!rgb.isTransparent()) numberOfSolidColours++; } return null; } private void swapColour(int c1, int c2) { RgbWithTag tmp = colours.get(c1); colours.set(c1, colours.get(c2)); colours.set(c2, tmp); } }