|
18 | 18 |
|
19 | 19 | import java.awt.Color;
|
20 | 20 | import java.awt.Image;
|
21 |
| -import java.awt.color.ColorSpace; |
22 |
| - |
23 | 21 | import java.awt.image.BufferedImage;
|
24 |
| - |
| 22 | +import java.io.IOException; |
| 23 | +import java.io.InputStream; |
25 | 24 | import java.io.PrintStream;
|
26 | 25 |
|
27 |
| -import java.util.HashMap; |
28 |
| -import java.util.Map; |
29 |
| -import java.util.Map.Entry; |
30 |
| - |
31 | 26 | import javax.imageio.ImageIO;
|
32 | 27 |
|
33 | 28 | import org.apache.commons.logging.Log;
|
34 | 29 | import org.apache.commons.logging.LogFactory;
|
35 | 30 |
|
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; |
37 | 37 | import org.springframework.core.env.Environment;
|
38 |
| -import org.springframework.core.env.MutablePropertySources; |
39 | 38 | import org.springframework.core.env.PropertyResolver;
|
40 |
| -import org.springframework.core.env.PropertySourcesPropertyResolver; |
41 | 39 | import org.springframework.core.io.Resource;
|
42 | 40 | import org.springframework.util.Assert;
|
43 | 41 |
|
|
46 | 44 | * {@link Resource}.
|
47 | 45 | *
|
48 | 46 | * @author Craig Burke
|
| 47 | + * @author Phillip Webb |
| 48 | + * @since 1.4.0 |
49 | 49 | */
|
50 | 50 | public class ImageBanner implements Banner {
|
51 | 51 |
|
52 | 52 | private static final Log log = LogFactory.getLog(ImageBanner.class);
|
53 | 53 |
|
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', '#', '@' }; |
57 | 57 |
|
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; |
61 | 59 |
|
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; |
64 | 63 |
|
65 | 64 | public ImageBanner(Resource image) {
|
66 | 65 | Assert.notNull(image, "Image must not be null");
|
67 | 66 | Assert.isTrue(image.exists(), "Image must exist");
|
68 | 67 | 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)); |
90 | 68 | }
|
91 | 69 |
|
92 | 70 | @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"); |
95 | 74 | try {
|
96 | 75 | 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); |
112 | 77 | }
|
113 | 78 | catch (Exception ex) {
|
114 | 79 | log.warn("Image banner not printable: " + this.image + " (" + ex.getClass()
|
115 | 80 | + ": '" + ex.getMessage() + "')", ex);
|
116 | 81 | }
|
117 | 82 | 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"); |
134 | 85 | }
|
135 | 86 | 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); |
144 | 88 | }
|
145 |
| - banner.append("${AnsiColor.DEFAULT}\n"); |
146 | 89 | }
|
147 |
| - |
148 |
| - return banner.toString(); |
149 | 90 | }
|
150 | 91 |
|
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); |
165 | 102 | }
|
166 | 103 |
|
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); |
177 | 109 | }
|
178 |
| - else { |
179 |
| - luminance = (RED_WEIGHT * red) + (GREEN_WEIGHT * green) |
180 |
| - + (BLUE_WEIGHT * blue); |
| 110 | + finally { |
| 111 | + inputStream.close(); |
181 | 112 | }
|
182 |
| - |
183 |
| - return (int) Math.ceil((luminance / 255.0d) * 100); |
184 | 113 | }
|
185 | 114 |
|
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; |
203 | 118 | }
|
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); |
215 | 122 | }
|
| 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; |
216 | 128 | }
|
217 | 129 |
|
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(); |
229 | 152 | }
|
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(); |
240 | 156 | }
|
241 | 157 |
|
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]; |
282 | 166 | }
|
283 | 167 |
|
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); |
298 | 174 | }
|
299 | 175 |
|
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; |
306 | 178 | }
|
307 | 179 |
|
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 |
| - } |
316 | 180 | }
|
0 commit comments