Skip to content

Commit 43d1d92

Browse files
committed
Rework ImageBanner Support
Refactor several aspects of the ImageBanner: - Extract a few new classes and methods from the previous code - Directly encode ANSI rather than using `${}` properties - Rework the scaling algorithm to prefer a fixed width - Allow ImageBanner and TextBanner to be used together - Rename several of the `banner.image` properties - Add support for a left hand margin - Add property meta-data See spring-projectsgh-4647
1 parent 60500ae commit 43d1d92

File tree

17 files changed

+603
-452
lines changed

17 files changed

+603
-452
lines changed
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
name: Phil
1+
name: Phil
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
${Ansi.GREEN} :: Sample application build with Spring Boot${spring-boot.formatted-version} ::${Ansi.DEFAULT}

spring-boot/src/main/java/org/springframework/boot/ImageBanner.java

+93-229
Original file line numberDiff line numberDiff line change
@@ -18,26 +18,24 @@
1818

1919
import java.awt.Color;
2020
import java.awt.Image;
21-
import java.awt.color.ColorSpace;
22-
2321
import java.awt.image.BufferedImage;
24-
22+
import java.io.IOException;
23+
import java.io.InputStream;
2524
import java.io.PrintStream;
2625

27-
import java.util.HashMap;
28-
import java.util.Map;
29-
import java.util.Map.Entry;
30-
3126
import javax.imageio.ImageIO;
3227

3328
import org.apache.commons.logging.Log;
3429
import org.apache.commons.logging.LogFactory;
3530

36-
import org.springframework.boot.ansi.AnsiPropertySource;
31+
import org.springframework.boot.ansi.AnsiBackground;
32+
import org.springframework.boot.ansi.AnsiColor;
33+
import org.springframework.boot.ansi.AnsiColors;
34+
import org.springframework.boot.ansi.AnsiElement;
35+
import org.springframework.boot.ansi.AnsiOutput;
36+
import org.springframework.boot.bind.RelaxedPropertyResolver;
3737
import org.springframework.core.env.Environment;
38-
import org.springframework.core.env.MutablePropertySources;
3938
import org.springframework.core.env.PropertyResolver;
40-
import org.springframework.core.env.PropertySourcesPropertyResolver;
4139
import org.springframework.core.io.Resource;
4240
import org.springframework.util.Assert;
4341

@@ -46,271 +44,137 @@
4644
* {@link Resource}.
4745
*
4846
* @author Craig Burke
47+
* @author Phillip Webb
48+
* @since 1.4.0
4949
*/
5050
public class ImageBanner implements Banner {
5151

5252
private static final Log log = LogFactory.getLog(ImageBanner.class);
5353

54-
private static final double RED_WEIGHT = 0.2126d;
55-
private static final double GREEN_WEIGHT = 0.7152d;
56-
private static final double BLUE_WEIGHT = 0.0722d;
54+
private static final double[] RGB_WEIGHT = { 0.2126d, 0.7152d, 0.0722d };
55+
56+
private static final char[] PIXEL = { ' ', '.', '*', ':', 'o', '&', '8', '#', '@' };
5757

58-
private static final int DEFAULT_MAX_WIDTH = 72;
59-
private static final double DEFAULT_ASPECT_RATIO = 0.5d;
60-
private static final boolean DEFAULT_DARK = false;
58+
private static final int LUMINANCE_INCREMENT = 10;
6159

62-
private Resource image;
63-
private Map<String, Color> colors = new HashMap<String, Color>();
60+
private static final int LUMINANCE_START = LUMINANCE_INCREMENT * PIXEL.length;
61+
62+
private final Resource image;
6463

6564
public ImageBanner(Resource image) {
6665
Assert.notNull(image, "Image must not be null");
6766
Assert.isTrue(image.exists(), "Image must exist");
6867
this.image = image;
69-
colorsInit();
70-
}
71-
72-
private void colorsInit() {
73-
this.colors.put("BLACK", new Color(0, 0, 0));
74-
this.colors.put("RED", new Color(170, 0, 0));
75-
this.colors.put("GREEN", new Color(0, 170, 0));
76-
this.colors.put("YELLOW", new Color(170, 85, 0));
77-
this.colors.put("BLUE", new Color(0, 0, 170));
78-
this.colors.put("MAGENTA", new Color(170, 0, 170));
79-
this.colors.put("CYAN", new Color(0, 170, 170));
80-
this.colors.put("WHITE", new Color(170, 170, 170));
81-
82-
this.colors.put("BRIGHT_BLACK", new Color(85, 85, 85));
83-
this.colors.put("BRIGHT_RED", new Color(255, 85, 85));
84-
this.colors.put("BRIGHT_GREEN", new Color(85, 255, 85));
85-
this.colors.put("BRIGHT_YELLOW", new Color(255, 255, 85));
86-
this.colors.put("BRIGHT_BLUE", new Color(85, 85, 255));
87-
this.colors.put("BRIGHT_MAGENTA", new Color(255, 85, 255));
88-
this.colors.put("BRIGHT_CYAN", new Color(85, 255, 255));
89-
this.colors.put("BRIGHT_WHITE", new Color(255, 255, 255));
9068
}
9169

9270
@Override
93-
public void printBanner(Environment environment, Class<?> sourceClass, PrintStream out) {
94-
String headlessProperty = System.getProperty("java.awt.headless");
71+
public void printBanner(Environment environment, Class<?> sourceClass,
72+
PrintStream out) {
73+
String headless = System.getProperty("java.awt.headless");
9574
try {
9675
System.setProperty("java.awt.headless", "true");
97-
BufferedImage sourceImage = ImageIO.read(this.image.getInputStream());
98-
99-
int maxWidth = environment.getProperty("banner.image.max-width",
100-
Integer.class, DEFAULT_MAX_WIDTH);
101-
Double aspectRatio = environment.getProperty("banner.image.aspect-ratio",
102-
Double.class, DEFAULT_ASPECT_RATIO);
103-
boolean invert = environment.getProperty("banner.image.dark", Boolean.class,
104-
DEFAULT_DARK);
105-
106-
BufferedImage resizedImage = resizeImage(sourceImage, maxWidth, aspectRatio);
107-
String banner = imageToBanner(resizedImage, invert);
108-
109-
PropertyResolver ansiResolver = getAnsiResolver();
110-
banner = ansiResolver.resolvePlaceholders(banner);
111-
out.println(banner);
76+
printBanner(environment, out);
11277
}
11378
catch (Exception ex) {
11479
log.warn("Image banner not printable: " + this.image + " (" + ex.getClass()
11580
+ ": '" + ex.getMessage() + "')", ex);
11681
}
11782
finally {
118-
System.setProperty("java.awt.headless", headlessProperty);
119-
}
120-
}
121-
122-
private PropertyResolver getAnsiResolver() {
123-
MutablePropertySources sources = new MutablePropertySources();
124-
sources.addFirst(new AnsiPropertySource("ansi", true));
125-
return new PropertySourcesPropertyResolver(sources);
126-
}
127-
128-
private String imageToBanner(BufferedImage image, boolean dark) {
129-
StringBuilder banner = new StringBuilder();
130-
131-
for (int y = 0; y < image.getHeight(); y++) {
132-
if (dark) {
133-
banner.append("${AnsiBackground.BLACK}");
83+
if (headless == null) {
84+
System.clearProperty("java.awt.headless");
13485
}
13586
else {
136-
banner.append("${AnsiBackground.DEFAULT}");
137-
}
138-
for (int x = 0; x < image.getWidth(); x++) {
139-
Color color = new Color(image.getRGB(x, y), false);
140-
banner.append(getFormatString(color, dark));
141-
}
142-
if (dark) {
143-
banner.append("${AnsiBackground.DEFAULT}");
87+
System.setProperty("java.awt.headless", headless);
14488
}
145-
banner.append("${AnsiColor.DEFAULT}\n");
14689
}
147-
148-
return banner.toString();
14990
}
15091

151-
protected String getFormatString(Color color, boolean dark) {
152-
String matchedColorName = null;
153-
Double minColorDistance = null;
154-
155-
for (Entry<String, Color> colorOption : this.colors.entrySet()) {
156-
double distance = getColorDistance(color, colorOption.getValue());
157-
158-
if (minColorDistance == null || distance < minColorDistance) {
159-
minColorDistance = distance;
160-
matchedColorName = colorOption.getKey();
161-
}
162-
}
163-
164-
return "${AnsiColor." + matchedColorName + "}" + getAsciiCharacter(color, dark);
92+
private void printBanner(Environment environment, PrintStream out)
93+
throws IOException {
94+
PropertyResolver properties = new RelaxedPropertyResolver(environment,
95+
"banner.image.");
96+
int width = properties.getProperty("width", Integer.class, 76);
97+
int heigth = properties.getProperty("height", Integer.class, 0);
98+
int margin = properties.getProperty("margin", Integer.class, 2);
99+
boolean invert = properties.getProperty("invert", Boolean.class, false);
100+
BufferedImage image = readImage(width, heigth);
101+
printBanner(image, margin, invert, out);
165102
}
166103

167-
private static int getLuminance(Color color, boolean inverse) {
168-
double red = color.getRed();
169-
double green = color.getGreen();
170-
double blue = color.getBlue();
171-
172-
double luminance;
173-
174-
if (inverse) {
175-
luminance = (RED_WEIGHT * (255.0d - red)) + (GREEN_WEIGHT * (255.0d - green))
176-
+ (BLUE_WEIGHT * (255.0d - blue));
104+
private BufferedImage readImage(int width, int heigth) throws IOException {
105+
InputStream inputStream = this.image.getInputStream();
106+
try {
107+
BufferedImage image = ImageIO.read(inputStream);
108+
return resizeImage(image, width, heigth);
177109
}
178-
else {
179-
luminance = (RED_WEIGHT * red) + (GREEN_WEIGHT * green)
180-
+ (BLUE_WEIGHT * blue);
110+
finally {
111+
inputStream.close();
181112
}
182-
183-
return (int) Math.ceil((luminance / 255.0d) * 100);
184113
}
185114

186-
private static char getAsciiCharacter(Color color, boolean dark) {
187-
double luminance = getLuminance(color, dark);
188-
189-
if (luminance >= 90) {
190-
return ' ';
191-
}
192-
else if (luminance >= 80) {
193-
return '.';
194-
}
195-
else if (luminance >= 70) {
196-
return '*';
197-
}
198-
else if (luminance >= 60) {
199-
return ':';
200-
}
201-
else if (luminance >= 50) {
202-
return 'o';
115+
private BufferedImage resizeImage(BufferedImage image, int width, int height) {
116+
if (width < 1) {
117+
width = 1;
203118
}
204-
else if (luminance >= 40) {
205-
return '&';
206-
}
207-
else if (luminance >= 30) {
208-
return '8';
209-
}
210-
else if (luminance >= 20) {
211-
return '#';
212-
}
213-
else {
214-
return '@';
119+
if (height <= 0) {
120+
double aspectRatio = (double) width / image.getWidth() * 0.5;
121+
height = (int) Math.ceil(image.getHeight() * aspectRatio);
215122
}
123+
BufferedImage resized = new BufferedImage(width, height,
124+
BufferedImage.TYPE_INT_RGB);
125+
Image scaled = image.getScaledInstance(width, height, Image.SCALE_DEFAULT);
126+
resized.getGraphics().drawImage(scaled, 0, 0, null);
127+
return resized;
216128
}
217129

218-
private static BufferedImage resizeImage(BufferedImage sourceImage, int maxWidth,
219-
double aspectRatio) {
220-
int width;
221-
double resizeRatio;
222-
if (sourceImage.getWidth() > maxWidth) {
223-
resizeRatio = (double) maxWidth / (double) sourceImage.getWidth();
224-
width = maxWidth;
225-
}
226-
else {
227-
resizeRatio = 1.0d;
228-
width = sourceImage.getWidth();
130+
private void printBanner(BufferedImage image, int margin, boolean invert,
131+
PrintStream out) {
132+
AnsiElement background = (invert ? AnsiBackground.BLACK : AnsiBackground.DEFAULT);
133+
out.print(AnsiOutput.encode(AnsiColor.DEFAULT));
134+
out.print(AnsiOutput.encode(background));
135+
out.println();
136+
out.println();
137+
AnsiColor lastColor = AnsiColor.DEFAULT;
138+
for (int y = 0; y < image.getHeight(); y++) {
139+
for (int i = 0; i < margin; i++) {
140+
out.print(" ");
141+
}
142+
for (int x = 0; x < image.getWidth(); x++) {
143+
Color color = new Color(image.getRGB(x, y), false);
144+
AnsiColor ansiColor = AnsiColors.getClosest(color);
145+
if (ansiColor != lastColor) {
146+
out.print(AnsiOutput.encode(ansiColor));
147+
lastColor = ansiColor;
148+
}
149+
out.print(getAsciiPixel(color, invert));
150+
}
151+
out.println();
229152
}
230-
231-
int height = (int) (Math.ceil(resizeRatio * aspectRatio
232-
* (double) sourceImage.getHeight()));
233-
Image image = sourceImage.getScaledInstance(width, height, Image.SCALE_DEFAULT);
234-
235-
BufferedImage resizedImage = new BufferedImage(image.getWidth(null),
236-
image.getHeight(null), BufferedImage.TYPE_INT_RGB);
237-
238-
resizedImage.getGraphics().drawImage(image, 0, 0, null);
239-
return resizedImage;
153+
out.print(AnsiOutput.encode(AnsiColor.DEFAULT));
154+
out.print(AnsiOutput.encode(AnsiBackground.DEFAULT));
155+
out.println();
240156
}
241157

242-
/**
243-
* Computes the CIE94 distance between two colors.
244-
*
245-
* Contributed by michael-simons
246-
* (original implementation https://github.com/michael-simons/dfx-mosaic/blob/public/src/main/java/de/dailyfratze/mosaic/images/CIE94ColorDistance.java)
247-
*
248-
* @param color1 the first color
249-
* @param color2 the second color
250-
* @return the distance between the colors
251-
*/
252-
private static double getColorDistance(final Color color1, final Color color2) {
253-
// Convert to L*a*b* color space
254-
float[] lab1 = toLab(color1);
255-
float[] lab2 = toLab(color2);
256-
257-
// Make it more readable
258-
double L1 = lab1[0];
259-
double a1 = lab1[1];
260-
double b1 = lab1[2];
261-
double L2 = lab2[0];
262-
double a2 = lab2[1];
263-
double b2 = lab2[2];
264-
265-
// CIE94 coefficients for graphic arts
266-
double kL = 1;
267-
double K1 = 0.045;
268-
double K2 = 0.015;
269-
// Weighting factors
270-
double sl = 1.0;
271-
double kc = 1.0;
272-
double kh = 1.0;
273-
274-
// See http://en.wikipedia.org/wiki/Color_difference#CIE94
275-
double c1 = Math.sqrt(a1 * a1 + b1 * b1);
276-
double deltaC = c1 - Math.sqrt(a2 * a2 + b2 * b2);
277-
double deltaA = a1 - a2;
278-
double deltaB = b1 - b2;
279-
double deltaH = Math.sqrt(Math.max(0.0, deltaA * deltaA + deltaB * deltaB - deltaC * deltaC));
280-
281-
return Math.sqrt(Math.max(0.0, Math.pow((L1 - L2) / (kL * sl), 2) + Math.pow(deltaC / (kc * (1 + K1 * c1)), 2) + Math.pow(deltaH / (kh * (1 + K2 * c1)), 2.0)));
158+
private char getAsciiPixel(Color color, boolean dark) {
159+
double luminance = getLuminance(color, dark);
160+
for (int i = 0; i < PIXEL.length; i++) {
161+
if (luminance >= (LUMINANCE_START - (i * LUMINANCE_INCREMENT))) {
162+
return PIXEL[i];
163+
}
164+
}
165+
return PIXEL[PIXEL.length - 1];
282166
}
283167

284-
/**
285-
* Returns the CIE L*a*b* values of this color.
286-
*
287-
* Implements the forward transformation described in
288-
* https://en.wikipedia.org/wiki/Lab_color_space
289-
*
290-
* @param color the color to convert
291-
* @return the xyz color components
292-
*/
293-
static float[] toLab(Color color) {
294-
float[] xyz = color.getColorComponents(
295-
ColorSpace.getInstance(ColorSpace.CS_CIEXYZ), null);
296-
297-
return xyzToLab(xyz);
168+
private int getLuminance(Color color, boolean inverse) {
169+
double luminance = 0.0;
170+
luminance += getLuminance(color.getRed(), inverse, RGB_WEIGHT[0]);
171+
luminance += getLuminance(color.getGreen(), inverse, RGB_WEIGHT[1]);
172+
luminance += getLuminance(color.getBlue(), inverse, RGB_WEIGHT[2]);
173+
return (int) Math.ceil((luminance / 0xFF) * 100);
298174
}
299175

300-
static float[] xyzToLab(float[] colorvalue) {
301-
double l = f(colorvalue[1]);
302-
double L = 116.0 * l - 16.0;
303-
double a = 500.0 * (f(colorvalue[0]) - l);
304-
double b = 200.0 * (l - f(colorvalue[2]));
305-
return new float[]{(float) L, (float) a, (float) b};
176+
private double getLuminance(int component, boolean inverse, double weight) {
177+
return (inverse ? 0xFF - component : component) * weight;
306178
}
307179

308-
private static double f(double t) {
309-
if (t > 216.0 / 24389.0) {
310-
return Math.cbrt(t);
311-
}
312-
else {
313-
return (1.0 / 3.0) * Math.pow(29.0 / 6.0, 2) * t + (4.0 / 29.0);
314-
}
315-
}
316180
}

0 commit comments

Comments
 (0)