Skip to content

Commit d815920

Browse files
authored
feat: Full color support in CellFormat expressions (#797)
* feat: Full color support in cellformat expressions This change adds full support for named and indexed color in cell format expressions by adding in the standard color palette to CellFormatPart, from where the formatting information is free to propagate down the stack. For strict format compatibility, only the 8 basic named colors and 56 indexed colors should be allowed, but this change retains the assumed intended feature of the original code to also support the full set of colors specified in HSSFColor.HSSFColorPredefined. If this is undesirable, the dependency on HSSFColor should be dropped entirely. This change is required in order to properly support named and indexed colors as part of cell format expressions in products using POI. * Add some sanity tests * Rename testNamedColorsExist to testNamedColors * fix rebase corruption in CellFormatPart * fix indentation in TestCellFormat ..by converting tabs to four spaces * fix: replace .length() == 0 with .isEmpty for color name string * fix Javadoc name parameter in CellFormatPart.getColor() * chore: Tidy up CellFormatPart
1 parent bf4ee6a commit d815920

File tree

2 files changed

+206
-26
lines changed

2 files changed

+206
-26
lines changed

poi/src/main/java/org/apache/poi/ss/format/CellFormatPart.java

Lines changed: 83 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,10 @@ Licensed to the Apache Software Foundation (ASF) under one or more
2626

2727
import java.awt.*;
2828
import java.util.*;
29+
import java.util.List;
2930
import java.util.regex.Matcher;
3031
import java.util.regex.Pattern;
32+
import java.util.stream.Collectors;
3133

3234
import static org.apache.poi.ss.format.CellFormatter.quote;
3335

@@ -50,29 +52,13 @@ public class CellFormatPart {
5052
private static final Logger LOG = PoiLogManager.getLogger(CellFormatPart.class);
5153

5254
static final Map<String, Color> NAMED_COLORS;
55+
static final List<Color> INDEXED_COLORS;
5356

5457
private final Color color;
5558
private final CellFormatCondition condition;
5659
private final CellFormatter format;
5760
private final CellFormatType type;
5861

59-
static {
60-
NAMED_COLORS = new TreeMap<>(
61-
String.CASE_INSENSITIVE_ORDER);
62-
63-
for (HSSFColor.HSSFColorPredefined color : HSSFColor.HSSFColorPredefined.values()) {
64-
String name = color.name();
65-
short[] rgb = color.getTriplet();
66-
Color c = new Color(rgb[0], rgb[1], rgb[2]);
67-
NAMED_COLORS.put(name, c);
68-
if (name.indexOf('_') > 0)
69-
NAMED_COLORS.put(name.replace('_', ' '), c);
70-
if (name.indexOf("_PERCENT") > 0)
71-
NAMED_COLORS.put(name.replace("_PERCENT", "%").replace('_',
72-
' '), c);
73-
}
74-
}
75-
7662
/** Pattern for the color part of a cell format part. */
7763
public static final Pattern COLOR_PAT;
7864
/** Pattern for the condition part of a cell format part. */
@@ -103,15 +89,67 @@ public class CellFormatPart {
10389
public static final int SPECIFICATION_GROUP;
10490

10591
static {
92+
// Build indexed color list, in order, from 1 to 56
93+
Integer[] indexedColors = new Integer[] {
94+
0x000000, 0xFFFFFF, 0xFF0000, 0x00FF00, 0x0000FF, 0xFFFF00, 0xFF00FF, 0x00FFFF,
95+
0x800000, 0x008000, 0x000080, 0x808000, 0x800080, 0x008080, 0xC0C0C0, 0x808080,
96+
0x9999FF, 0x993366, 0xFFFFCC, 0xCCFFFF, 0x660066, 0xFF8080, 0x0066CC, 0xCCCCFF,
97+
0x000080, 0xFF00FF, 0xFFFF00, 0x00FFFF, 0x800080, 0x800000, 0x008080, 0x0000FF,
98+
0x00CCFF, 0xCCFFFF, 0xCCFFCC, 0xFFFF99, 0x99CCFF, 0xFF99CC, 0xCC99FF, 0xFFCC99,
99+
0x3366FF, 0x33CCCC, 0x99CC00, 0xFFCC00, 0xFF9900, 0xFF6600, 0x666699, 0x969696,
100+
0x003366, 0x339966, 0x003300, 0x333300, 0x993300, 0x993366, 0x333399, 0x333333
101+
};
102+
INDEXED_COLORS = Collections.unmodifiableList(
103+
Arrays.asList(indexedColors)
104+
.stream().map(Color::new)
105+
.collect(Collectors.toList()));
106+
107+
// Build initial named color map
108+
Map<String, Color> namedColors = new TreeMap<>(
109+
String.CASE_INSENSITIVE_ORDER);
110+
111+
// Retain compatibility with original implementation
112+
for (HSSFColor.HSSFColorPredefined color : HSSFColor.HSSFColorPredefined.values()) {
113+
String name = color.name();
114+
short[] rgb = color.getTriplet();
115+
Color c = new Color(rgb[0], rgb[1], rgb[2]);
116+
namedColors.put(name, c);
117+
if (name.indexOf('_') > 0)
118+
namedColors.put(name.replace('_', ' '), c);
119+
if (name.indexOf("_PERCENT") > 0)
120+
namedColors.put(name.replace("_PERCENT", "%").replace('_',
121+
' '), c);
122+
}
123+
124+
// Add missing color values and replace incorrectly defined standard colors
125+
// used in Excel, Google Sheets, etc. The first eight indexed colors correspond
126+
// exactly to named colors.
127+
namedColors.put("black", INDEXED_COLORS.get(0));
128+
namedColors.put("white", INDEXED_COLORS.get(1));
129+
namedColors.put("red", INDEXED_COLORS.get(2));
130+
namedColors.put("green", INDEXED_COLORS.get(3));
131+
namedColors.put("blue", INDEXED_COLORS.get(4));
132+
namedColors.put("yellow", INDEXED_COLORS.get(5));
133+
namedColors.put("magenta", INDEXED_COLORS.get(6));
134+
namedColors.put("cyan", INDEXED_COLORS.get(7));
135+
106136
// A condition specification
107137
String condition = "([<>=]=?|!=|<>) # The operator\n" +
108138
" \\s*(-?([0-9]+(?:\\.[0-9]*)?)|(\\.[0-9]*))\\s* # The constant to test against\n";
109139

110140
// A currency symbol / string, in a specific locale
111141
String currency = "(\\[\\$.{0,3}(-[0-9a-f]{3,4})?])";
112142

113-
String color =
114-
"\\[(black|blue|cyan|green|magenta|red|white|yellow|color [0-9]+)]";
143+
// Build the color code matching expression. We should match any named color
144+
// in the set as well as a string in the form of "Color 8" or "Color 15".
145+
String color = "\\[(";
146+
for (String key : namedColors.keySet()) {
147+
// Escape special characters in the color name
148+
color += key.replaceAll("([^a-zA-Z0-9])", "\\\\$1") + "|";
149+
}
150+
// Match the indexed color table (accept both e.g. COLOR2 and COLOR 2)
151+
// Both formats are accepted as input in other products
152+
color += "color\\s*[0-9]+)\\]";
115153

116154
// A number specification
117155
// Note: careful that in something like ##, that the trailing comma is not caught up in the integer part
@@ -159,6 +197,17 @@ public class CellFormatPart {
159197
CONDITION_OPERATOR_GROUP = findGroup(FORMAT_PAT, "[>=1]@", ">=");
160198
CONDITION_VALUE_GROUP = findGroup(FORMAT_PAT, "[>=1]@", "1");
161199
SPECIFICATION_GROUP = findGroup(FORMAT_PAT, "[Blue][>1]\\a ?", "\\a ?");
200+
201+
// Once patterns have been compiled, add indexed colors to
202+
// namedColors so they can be easily picked up by getColor().
203+
for (int i = 0; i < INDEXED_COLORS.size(); ++i) {
204+
namedColors.put("color" + (i + 1), INDEXED_COLORS.get(i));
205+
// Also support space between "color" and number.
206+
namedColors.put("color " + (i + 1), INDEXED_COLORS.get(i));
207+
}
208+
209+
// Store namedColors as NAMED_COLORS
210+
NAMED_COLORS = Collections.unmodifiableMap(namedColors);
162211
}
163212

164213
interface PartHandler {
@@ -250,12 +299,23 @@ private static int findGroup(Pattern pat, String str, String marker) {
250299
* @return The color specification or {@code null}.
251300
*/
252301
private static Color getColor(Matcher m) {
253-
String cdesc = m.group(COLOR_GROUP);
254-
if (cdesc == null || cdesc.isEmpty())
302+
return getColor(m.group(COLOR_GROUP));
303+
}
304+
305+
/**
306+
* Get the Color object matching a color name, or {@code null} if the
307+
* color name is not recognized.
308+
*
309+
* @param cname Color name, such as "red" or "Color 15"
310+
*
311+
* @return a Color object or {@code null}.
312+
*/
313+
static Color getColor(String cname) {
314+
if (cname == null || cname.isEmpty())
255315
return null;
256-
Color c = NAMED_COLORS.get(cdesc);
316+
Color c = NAMED_COLORS.get(cname);
257317
if (c == null) {
258-
LOG.warn("Unknown color: {}", quote(cdesc));
318+
LOG.warn("Unknown color: {}", quote(cname));
259319
}
260320
return c;
261321
}

poi/src/test/java/org/apache/poi/ss/format/TestCellFormat.java

Lines changed: 123 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Licensed to the Apache Software Foundation (ASF) under one or more
2020
import static org.junit.jupiter.api.Assertions.assertNotNull;
2121
import static org.junit.jupiter.api.Assertions.assertTrue;
2222

23+
import java.awt.Color;
2324
import java.io.IOException;
2425
import java.text.ParseException;
2526
import java.text.SimpleDateFormat;
@@ -33,6 +34,7 @@ Licensed to the Apache Software Foundation (ASF) under one or more
3334
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
3435
import org.apache.poi.hssf.util.HSSFColor;
3536
import org.apache.poi.ss.usermodel.Cell;
37+
import org.apache.poi.ss.usermodel.CellType;
3638
import org.apache.poi.ss.usermodel.DateUtil;
3739
import org.apache.poi.ss.usermodel.Row;
3840
import org.apache.poi.ss.usermodel.Sheet;
@@ -1041,11 +1043,129 @@ void testBug62865() {
10411043

10421044
@Test
10431045
void testNamedColors() {
1044-
assertTrue(CellFormatPart.NAMED_COLORS.size() >= HSSFColor.HSSFColorPredefined.values().length);
1045-
Stream.of("GREEN", "Green", "RED", "Red", "BLUE", "Blue", "YELLOW", "Yellow")
1046-
.map(CellFormatPart.NAMED_COLORS::get)
1046+
// Make sure we have all standard named colors defined
1047+
// and are returned as non-null regardless of case
1048+
Stream.of("black", "white", "red", "green", "blue", "yellow", "magenta", "cyan",
1049+
"Black", "White", "Red", "Green", "Blue", "Yellow", "Magenta", "Cyan",
1050+
"BLACK", "WHITE", "RED", "GREEN", "BLUE", "YELLOW", "MAGENTA", "CYAN")
1051+
.map(CellFormatPart::getColor)
10471052
.forEach(Assertions::assertNotNull);
10481053
}
1054+
1055+
@Test
1056+
void testIndexedColorsExist() {
1057+
// Make sure the standard indexed colors are returned correctly and regardless of case
1058+
for (int i = 0; i < 56; ++i) {
1059+
assertNotNull(CellFormatPart.getColor("Color " + (i + 1)));
1060+
assertNotNull(CellFormatPart.getColor("COLOR" + (i + 1)));
1061+
assertNotNull(CellFormatPart.getColor("color" + (i + 1)));
1062+
}
1063+
}
1064+
1065+
@Test
1066+
void verifyNamedColors() {
1067+
assertEquals(CellFormatPart.getColor("Black"), new Color(0x000000));
1068+
assertEquals(CellFormatPart.getColor("white"), new Color(0xFFFFFF));
1069+
assertEquals(CellFormatPart.getColor("RED"), new Color(0xFF0000));
1070+
assertEquals(CellFormatPart.getColor("Green"), new Color(0x00FF00));
1071+
assertEquals(CellFormatPart.getColor("blue"), new Color(0x0000FF));
1072+
assertEquals(CellFormatPart.getColor("YELLOW"), new Color(0xFFFF00));
1073+
assertEquals(CellFormatPart.getColor("Magenta"), new Color(0xFF00FF));
1074+
assertEquals(CellFormatPart.getColor("cyan"), new Color(0x00FFFF));
1075+
}
1076+
1077+
@Test
1078+
void verifyIndexedColors() {
1079+
assertEquals(CellFormatPart.getColor("Color1"), CellFormatPart.getColor("black"));
1080+
assertEquals(CellFormatPart.getColor("color2"), CellFormatPart.getColor("white"));
1081+
assertEquals(CellFormatPart.getColor("Color3"), CellFormatPart.getColor("red"));
1082+
assertEquals(CellFormatPart.getColor("color4"), CellFormatPart.getColor("green"));
1083+
assertEquals(CellFormatPart.getColor("Color5"), CellFormatPart.getColor("blue"));
1084+
assertEquals(CellFormatPart.getColor("color6"), CellFormatPart.getColor("yellow"));
1085+
assertEquals(CellFormatPart.getColor("Color7"), CellFormatPart.getColor("magenta"));
1086+
assertEquals(CellFormatPart.getColor("color8"), CellFormatPart.getColor("cyan"));
1087+
assertEquals(CellFormatPart.getColor("Color9"), new Color(0x800000));
1088+
assertEquals(CellFormatPart.getColor("color10"), new Color(0x008000));
1089+
assertEquals(CellFormatPart.getColor("Color11"), new Color(0x000080));
1090+
assertEquals(CellFormatPart.getColor("color12"), new Color(0x808000));
1091+
assertEquals(CellFormatPart.getColor("Color13"), new Color(0x800080));
1092+
assertEquals(CellFormatPart.getColor("color14"), new Color(0x008080));
1093+
assertEquals(CellFormatPart.getColor("Color15"), new Color(0xC0C0C0));
1094+
assertEquals(CellFormatPart.getColor("color16"), new Color(0x808080));
1095+
assertEquals(CellFormatPart.getColor("Color17"), new Color(0x9999FF));
1096+
assertEquals(CellFormatPart.getColor("COLOR18"), new Color(0x993366));
1097+
assertEquals(CellFormatPart.getColor("Color19"), new Color(0xFFFFCC));
1098+
assertEquals(CellFormatPart.getColor("color20"), new Color(0xCCFFFF));
1099+
assertEquals(CellFormatPart.getColor("Color21"), new Color(0x660066));
1100+
assertEquals(CellFormatPart.getColor("COLOR22"), new Color(0xFF8080));
1101+
assertEquals(CellFormatPart.getColor("Color23"), new Color(0x0066CC));
1102+
assertEquals(CellFormatPart.getColor("color24"), new Color(0xCCCCFF));
1103+
assertEquals(CellFormatPart.getColor("Color25"), new Color(0x000080));
1104+
assertEquals(CellFormatPart.getColor("color26"), new Color(0xFF00FF));
1105+
assertEquals(CellFormatPart.getColor("Color27"), new Color(0xFFFF00));
1106+
assertEquals(CellFormatPart.getColor("COLOR28"), new Color(0x00FFFF));
1107+
assertEquals(CellFormatPart.getColor("Color29"), new Color(0x800080));
1108+
assertEquals(CellFormatPart.getColor("color30"), new Color(0x800000));
1109+
assertEquals(CellFormatPart.getColor("Color31"), new Color(0x008080));
1110+
assertEquals(CellFormatPart.getColor("Color32"), new Color(0x0000FF));
1111+
assertEquals(CellFormatPart.getColor("Color33"), new Color(0x00CCFF));
1112+
assertEquals(CellFormatPart.getColor("Color34"), new Color(0xCCFFFF));
1113+
assertEquals(CellFormatPart.getColor("Color35"), new Color(0xCCFFCC));
1114+
assertEquals(CellFormatPart.getColor("Color36"), new Color(0xFFFF99));
1115+
assertEquals(CellFormatPart.getColor("Color37"), new Color(0x99CCFF));
1116+
assertEquals(CellFormatPart.getColor("Color38"), new Color(0xFF99CC));
1117+
assertEquals(CellFormatPart.getColor("Color39"), new Color(0xCC99FF));
1118+
assertEquals(CellFormatPart.getColor("Color40"), new Color(0xFFCC99));
1119+
assertEquals(CellFormatPart.getColor("Color41"), new Color(0x3366FF));
1120+
assertEquals(CellFormatPart.getColor("Color42"), new Color(0x33CCCC));
1121+
assertEquals(CellFormatPart.getColor("Color43"), new Color(0x99CC00));
1122+
assertEquals(CellFormatPart.getColor("Color44"), new Color(0xFFCC00));
1123+
assertEquals(CellFormatPart.getColor("Color45"), new Color(0xFF9900));
1124+
assertEquals(CellFormatPart.getColor("Color46"), new Color(0xFF6600));
1125+
assertEquals(CellFormatPart.getColor("Color47"), new Color(0x666699));
1126+
assertEquals(CellFormatPart.getColor("Color48"), new Color(0x969696));
1127+
assertEquals(CellFormatPart.getColor("Color49"), new Color(0x003366));
1128+
assertEquals(CellFormatPart.getColor("Color50"), new Color(0x339966));
1129+
assertEquals(CellFormatPart.getColor("Color51"), new Color(0x003300));
1130+
assertEquals(CellFormatPart.getColor("Color52"), new Color(0x333300));
1131+
assertEquals(CellFormatPart.getColor("Color53"), new Color(0x993300));
1132+
assertEquals(CellFormatPart.getColor("Color54"), new Color(0x993366));
1133+
assertEquals(CellFormatPart.getColor("Color55"), new Color(0x333399));
1134+
assertEquals(CellFormatPart.getColor("Color56"), new Color(0x333333));
1135+
}
1136+
1137+
@Test
1138+
void testColorsInWorkbook() throws IOException {
1139+
// Create a workbook, row and cell to test with
1140+
try (Workbook wb = new HSSFWorkbook()) {
1141+
Sheet sheet = wb.createSheet();
1142+
Row row = sheet.createRow(0);
1143+
Cell cell = row.createCell(0);
1144+
CellFormatResult result;
1145+
CellFormat cf = CellFormat.getInstance(
1146+
"[GREEN]#,##0.0;[RED]\\(#,##0.0\\);[COLOR22]\"===\";[COLOR 8]\\\"@\\\"");
1147+
1148+
cell.setCellValue(100.0);
1149+
result = cf.apply(cell);
1150+
assertEquals("100.0", result.text);
1151+
assertEquals(result.textColor, CellFormatPart.getColor("color 4"));
1152+
1153+
cell.setCellValue(-50.0);
1154+
result = cf.apply(cell);
1155+
assertEquals("(50.0)", result.text);
1156+
assertEquals(result.textColor, CellFormatPart.getColor("red"));
1157+
1158+
cell.setCellValue("foo");
1159+
result = cf.apply(cell);
1160+
assertEquals("\"foo\"", result.text);
1161+
assertEquals(result.textColor, CellFormatPart.getColor("cyan"));
1162+
1163+
cell.setCellValue(0.0);
1164+
result = cf.apply(cell);
1165+
assertEquals("===", result.text);
1166+
assertEquals(result.textColor, CellFormatPart.getColor("color 22"));
1167+
}
1168+
}
10491169

10501170
@Test
10511171
void testElapsedSecondsRound() {

0 commit comments

Comments
 (0)