/*
* 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
);
}
}