Sunday, May 10, 2009

Java: POI + HWPF + CharacterProperties: Converting colors to RGB or Hex

I have been doing a little work with POI's HWPF lately. Right now I am still trying to grasp the basic structure of binary word documents and wade through the api. As the HWPF project is still in scratchpad mode, the documentation is sparse. So the task is a bit daunting to a novitiate, and after a brief look at the complexity of the Word file format specifications, I have an even greater appreciation for the work of POI developers.

Anyway, one of the first puzzles I ran into involved colors. I have been using the CharacterProperties class, and as its name implies, it stores the characteristics of a series of characters (such as text color, font size, bolding, etcetera). Unfortunately, reading or changing the text color was not as straight-forward as I thought.

CharacterProperties has at least two ways of setting the text color: setColor(int) and setIco24(int). The setColor method expects what is called an "ico". Apparently an "ico" is not a color, but an index representing an entry in a color palette. Looking at the source, that would seem to be the old 16 color palette from the days of Word 97.


case 0: // auto
return -1;
case 1: // black
return 0x000000;
case 2: // blue
return 0xFF0000;
case 3: // cyan
return 0xFFFF00;
case 4: // green
return 0x00FF00;
... etcetera


Obviously, that is too limiting so I took a look at the setIco24 method. That seemed to be right way to set the color for Word 2000+ documents which have 24 bit color. So I tried passing in a Color.getRGB() value, but no joy. The problem was Word represents colors (or COLORREF's) in 0X00BBGGRR (blue green red) form, and not the standard 0x00RRGGBB (red green blue). So that explained why my attempt to use an rgb color failed.

Since I am accustomed to working with rgb format, I decided to rig up a few convenience methods to make it easier to work with Word colors. One caveat, I was not sure how to determine the color represented by "Automatic". So for now the methods return black for "Auto".
  • rgbToIco24 - Converts an rgb value to a Word COLORREF
  • hexToIco24 - Converts a hexadecimal color to a Word COLORREF
  • ico24ToRGB - Returns the rgb values of a Word COLORREF
  • ico24ToHex - Returns rgb hexadecimal value of a Word COLORREF

Bear in mind I am still in the learning mode. So the usual disclaimers apply: These are just my impressions so far, not gospel. Anyway, the methods seem to be working well. They need further testing. But if the code holds up, maybe I will suggest it as an addition on the POI list. In case someone else finds it useful.


import java.awt.Color;

/**
* @author http://cfsearching@blogspot.com
*
* Convert CharacterProperties.getIco24() values to RGB or Hex
*
* Version:
* JDK 1.6.0_13
*/
public class ConvertIco24ToRGB {

/**
* Converts a CharacterProperties.getIco24() value into rgb format.
* Ico24 (or ColorRef) values, have the hexadecimal form: 0x00BBGGRR.
* (Applies to binary file formats only)
*
* Note, getIco24() returns -1 to represent "Auto". It is not clear how
* to determine that color value. So for now, the color "black" is used for "Auto".
*
* @param ico24 - Word colorRef value returned by CharacterProperties.getIco24()
* @return Array of color values [0] red, [1] green, [2] blue
*/
public static int[] ico24ToRGB(int ico24) {
int[] rgb = new int[3];

// If the colorRef is not "Auto", unpack the rgb values
if (ico24 != -1) {
rgb[0] = (ico24 >> 0) & 0xff; // red;
rgb[1] = (ico24 >> 8) & 0xff; // green
rgb[2] = (ico24 >> 16) & 0xff; // blue
}

return rgb;
}

/**
* Convenience method. Returns the ico24 value as as hexadecimal string, form: 0x00BBGGRR
* @param ico24 - Word colorRef value returned by CharacterProperties.getIco24()
* @return hexadecimal string in form: 0x00BBGGRR
*/
public static String ico24AsHex(int ico24) {
return Integer.toHexString(ico24);
}

/**
* Converts a CharacterProperties.getIco24() value to rgb hexadecimal format.
* Ico24 (or ColorRef) values, have the hexadecimal form: 0x00BBGGRR.
* (Applies to binary file formats only)
*
* @param ico24 - Word colorRef value returned by CharacterProperties.getIco24()
* @return hexadecimal color. Returns 0 if it cannot be determined
*/
public static String ico24ToHex(int ico24) {
int r = (ico24 >> 0) & 0xff; // red;
int g = (ico24 >> 8) & 0xff; // green
int b = (ico24 >> 16) & 0xff; // blue
int rgb = (r << 16) | (g << 8) | b;

// Find a better way to maintain leading zeroes
return Integer.toHexString(0xC000000 | rgb).substring(1);
}

/**
* Converts a six digit hexadecimal value (RRGGBB) to a CharacterProperties.setIco24(int)
* compatible value. Ico24 (or ColorRef) values, have the hexadecimal form: 0x00BBGGRR.
* (Applies to binary file formats only)
*
* @param hexColor - Word colorRef value returned by CharacterProperties.getIco24()
* @return colorRef value
*/
public static int hexToIco24(String hexColor) {
if (hexColor == null || hexColor.length() != 6) {
throw new IllegalArgumentException("hexColor must be 6 characters in length. Example: ffffff");
}
int r = Integer.parseInt(hexColor.substring(0, 2), 16);
int g = Integer.parseInt(hexColor.substring(2, 4), 16);
int b = Integer.parseInt(hexColor.substring(4, 6), 16);
return rgbToIco24(r, g, b);
}

/**
* Converts an rgb color to a Word colorRef value that should be compatible with
* CharacterProperties.setIco24(int). Ico24 (or ColorRef) values, have
* the hexadecimal form: 0x00BBGGRR. (Applies to binary file formats only)
*
* @param r red
* @param g green
* @param b blue
* @return ico24 (or ColorRef) value
*/
public static int rgbToIco24(int r, int g, int b)
{
return (0xff << 24) | (b << 16) | (g << 8) | r;
}



public static void main(String[] args) {
String inputPath = "input/blank.doc";
String outputPath = "output/InsertStyledTextA.doc";
inputPath = "output/InsertStyledText.doc";

try {
System.out.println("Test Color");
System.out.println("=======================");

Color c = Color.BLACK;
// Extract the hex and rgb values of the selected color
int r = c.getRed();
int g = c.getGreen() ;
int b = c.getBlue();
String hex = Integer.toHexString(c.getRGB() | 0x000000).substring(2);
System.out.printf("red=%d green=%d blue=%d | hex= %s \r", r, g, b, hex);


// Convert the rgb values to a ColorRef
int ico24 = rgbToIco24(r, g, b);
// Display parameters and result
System.out.printf("rgbToIco24(red=%d, green=%d, blue=%d) = %d \r", r, g, b, ico24);

// Convert the hex color to a ColorRef
ico24 = hexToIco24(hex);
System.out.printf("hexToIco24(hex=%s ) = %d \r", hex, ico24);

// Convert the ColorRef back to rgb
int[] rgb = ico24ToRGB(ico24);
System.out.printf("ico24ToRGB(ico24=%d) = red=%d, green=%d, blue=%d \r", ico24, rgb[0], rgb[1], rgb[2]);

// Convert the ColorRef back to hex
hex = ico24ToHex(ico24);
System.out.printf("ico24ToHex(ico24=%d) = hex=%6s \r", ico24, hex);

}
catch (Exception e)
{
e.printStackTrace();
}

}

}

4 comments:

jwhite1202 May 11, 2009 at 3:15 PM  

Thanks for posting this. I apologize for my ignorance, but what would I use this type of class for? It looks interesting, but not sure how I would use this code.

Thanks,

JW

cfSearching May 11, 2009 at 4:40 PM  

@JWhite,

No, it is an understandable question ;) I do not know if you have read my more recent entries, but to give some background:

I have been looking into options for manipulating MS Word documents from ColdFusion. One of those options is POI and there HWPF (Horrible Word Processing Format) project. So I have been testing the basic functions, one of which is adding colored text to a document.

As I mentioned in the entry, Word documents use a funky color format. So if you want to create blue or red text, you need to know how to say that color in MS-Word-speak ;-) That is the purpose of the methods above. They will translate a normal rgb or hex color into an MS Word compatible color (ie 24 bit "colorRef").

But anyway, back to your question. You probably would not use it directly from ColdFusion (though you could do that). Most likely you would translate it to a ColdFusion function. I was intending to do that, but have not gotten around to it yet. I will probably post an example later that shows the functions in CF action.

BTW, most POI examples out there are strictly java. So it is often easier to test in java first, with Eclipse. Then translate the working code to ColdFusion. As I seem to get a lot of hits from java users. I figured the java code might be useful to someone else.

Leigh

cfSearching May 11, 2009 at 5:05 PM  

I just noticed the blogger garbled the formatting too. That certainly did not help either ;)

jwhite1202 May 12, 2009 at 7:20 AM  

Thanks for the quick explanation. It's good to know that people are writing about this type of stuff. I am always looking for ways to expand my knowledge of development beyond CF when I can (without having to totally give CF up).

  © Blogger templates The Professional Template by Ourblogtemplates.com 2008

Header image adapted from atomicjeep