001/* 002 * Copyright 2012-2018 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.boot; 018 019import java.awt.Color; 020import java.awt.Image; 021import java.awt.image.BufferedImage; 022import java.io.IOException; 023import java.io.InputStream; 024import java.io.PrintStream; 025import java.util.Iterator; 026 027import javax.imageio.ImageIO; 028import javax.imageio.ImageReadParam; 029import javax.imageio.ImageReader; 030import javax.imageio.metadata.IIOMetadata; 031import javax.imageio.metadata.IIOMetadataNode; 032import javax.imageio.stream.ImageInputStream; 033 034import org.apache.commons.logging.Log; 035import org.apache.commons.logging.LogFactory; 036 037import org.springframework.boot.ansi.AnsiBackground; 038import org.springframework.boot.ansi.AnsiColor; 039import org.springframework.boot.ansi.AnsiColors; 040import org.springframework.boot.ansi.AnsiElement; 041import org.springframework.boot.ansi.AnsiOutput; 042import org.springframework.core.env.Environment; 043import org.springframework.core.io.Resource; 044import org.springframework.util.Assert; 045 046/** 047 * Banner implementation that prints ASCII art generated from an image resource 048 * {@link Resource}. 049 * 050 * @author Craig Burke 051 * @author Phillip Webb 052 * @author Madhura Bhave 053 * @author Raja Kolli 054 * @since 1.4.0 055 */ 056public class ImageBanner implements Banner { 057 058 private static final String PROPERTY_PREFIX = "spring.banner.image."; 059 060 private static final Log logger = LogFactory.getLog(ImageBanner.class); 061 062 private static final double[] RGB_WEIGHT = { 0.2126d, 0.7152d, 0.0722d }; 063 064 private static final char[] PIXEL = { ' ', '.', '*', ':', 'o', '&', '8', '#', '@' }; 065 066 private static final int LUMINANCE_INCREMENT = 10; 067 068 private static final int LUMINANCE_START = LUMINANCE_INCREMENT * PIXEL.length; 069 070 private final Resource image; 071 072 public ImageBanner(Resource image) { 073 Assert.notNull(image, "Image must not be null"); 074 Assert.isTrue(image.exists(), "Image must exist"); 075 this.image = image; 076 } 077 078 @Override 079 public void printBanner(Environment environment, Class<?> sourceClass, 080 PrintStream out) { 081 String headless = System.getProperty("java.awt.headless"); 082 try { 083 System.setProperty("java.awt.headless", "true"); 084 printBanner(environment, out); 085 } 086 catch (Throwable ex) { 087 logger.warn("Image banner not printable: " + this.image + " (" + ex.getClass() 088 + ": '" + ex.getMessage() + "')"); 089 logger.debug("Image banner printing failure", ex); 090 } 091 finally { 092 if (headless == null) { 093 System.clearProperty("java.awt.headless"); 094 } 095 else { 096 System.setProperty("java.awt.headless", headless); 097 } 098 } 099 } 100 101 private void printBanner(Environment environment, PrintStream out) 102 throws IOException { 103 int width = getProperty(environment, "width", Integer.class, 76); 104 int height = getProperty(environment, "height", Integer.class, 0); 105 int margin = getProperty(environment, "margin", Integer.class, 2); 106 boolean invert = getProperty(environment, "invert", Boolean.class, false); 107 Frame[] frames = readFrames(width, height); 108 for (int i = 0; i < frames.length; i++) { 109 if (i > 0) { 110 resetCursor(frames[i - 1].getImage(), out); 111 } 112 printBanner(frames[i].getImage(), margin, invert, out); 113 sleep(frames[i].getDelayTime()); 114 } 115 } 116 117 private <T> T getProperty(Environment environment, String name, Class<T> targetType, 118 T defaultValue) { 119 return environment.getProperty(PROPERTY_PREFIX + name, targetType, defaultValue); 120 } 121 122 private Frame[] readFrames(int width, int height) throws IOException { 123 try (InputStream inputStream = this.image.getInputStream()) { 124 try (ImageInputStream imageStream = ImageIO 125 .createImageInputStream(inputStream)) { 126 return readFrames(width, height, imageStream); 127 } 128 } 129 } 130 131 private Frame[] readFrames(int width, int height, ImageInputStream stream) 132 throws IOException { 133 Iterator<ImageReader> readers = ImageIO.getImageReaders(stream); 134 Assert.state(readers.hasNext(), "Unable to read image banner source"); 135 ImageReader reader = readers.next(); 136 try { 137 ImageReadParam readParam = reader.getDefaultReadParam(); 138 reader.setInput(stream); 139 int frameCount = reader.getNumImages(true); 140 Frame[] frames = new Frame[frameCount]; 141 for (int i = 0; i < frameCount; i++) { 142 frames[i] = readFrame(width, height, reader, i, readParam); 143 } 144 return frames; 145 } 146 finally { 147 reader.dispose(); 148 } 149 } 150 151 private Frame readFrame(int width, int height, ImageReader reader, int imageIndex, 152 ImageReadParam readParam) throws IOException { 153 BufferedImage image = reader.read(imageIndex, readParam); 154 BufferedImage resized = resizeImage(image, width, height); 155 int delayTime = getDelayTime(reader, imageIndex); 156 return new Frame(resized, delayTime); 157 } 158 159 private int getDelayTime(ImageReader reader, int imageIndex) throws IOException { 160 IIOMetadata metadata = reader.getImageMetadata(imageIndex); 161 IIOMetadataNode root = (IIOMetadataNode) metadata 162 .getAsTree(metadata.getNativeMetadataFormatName()); 163 IIOMetadataNode extension = findNode(root, "GraphicControlExtension"); 164 String attribute = (extension != null) ? extension.getAttribute("delayTime") 165 : null; 166 return (attribute != null) ? Integer.parseInt(attribute) * 10 : 0; 167 } 168 169 private static IIOMetadataNode findNode(IIOMetadataNode rootNode, String nodeName) { 170 if (rootNode == null) { 171 return null; 172 } 173 for (int i = 0; i < rootNode.getLength(); i++) { 174 if (rootNode.item(i).getNodeName().equalsIgnoreCase(nodeName)) { 175 return ((IIOMetadataNode) rootNode.item(i)); 176 } 177 } 178 return null; 179 } 180 181 private BufferedImage resizeImage(BufferedImage image, int width, int height) { 182 if (width < 1) { 183 width = 1; 184 } 185 if (height <= 0) { 186 double aspectRatio = (double) width / image.getWidth() * 0.5; 187 height = (int) Math.ceil(image.getHeight() * aspectRatio); 188 } 189 BufferedImage resized = new BufferedImage(width, height, 190 BufferedImage.TYPE_INT_RGB); 191 Image scaled = image.getScaledInstance(width, height, Image.SCALE_DEFAULT); 192 resized.getGraphics().drawImage(scaled, 0, 0, null); 193 return resized; 194 } 195 196 private void resetCursor(BufferedImage image, PrintStream out) { 197 int lines = image.getHeight() + 3; 198 out.print("\033[" + lines + "A\r"); 199 } 200 201 private void printBanner(BufferedImage image, int margin, boolean invert, 202 PrintStream out) { 203 AnsiElement background = invert ? AnsiBackground.BLACK : AnsiBackground.DEFAULT; 204 out.print(AnsiOutput.encode(AnsiColor.DEFAULT)); 205 out.print(AnsiOutput.encode(background)); 206 out.println(); 207 out.println(); 208 AnsiColor lastColor = AnsiColor.DEFAULT; 209 for (int y = 0; y < image.getHeight(); y++) { 210 for (int i = 0; i < margin; i++) { 211 out.print(" "); 212 } 213 for (int x = 0; x < image.getWidth(); x++) { 214 Color color = new Color(image.getRGB(x, y), false); 215 AnsiColor ansiColor = AnsiColors.getClosest(color); 216 if (ansiColor != lastColor) { 217 out.print(AnsiOutput.encode(ansiColor)); 218 lastColor = ansiColor; 219 } 220 out.print(getAsciiPixel(color, invert)); 221 } 222 out.println(); 223 } 224 out.print(AnsiOutput.encode(AnsiColor.DEFAULT)); 225 out.print(AnsiOutput.encode(AnsiBackground.DEFAULT)); 226 out.println(); 227 } 228 229 private char getAsciiPixel(Color color, boolean dark) { 230 double luminance = getLuminance(color, dark); 231 for (int i = 0; i < PIXEL.length; i++) { 232 if (luminance >= (LUMINANCE_START - (i * LUMINANCE_INCREMENT))) { 233 return PIXEL[i]; 234 } 235 } 236 return PIXEL[PIXEL.length - 1]; 237 } 238 239 private int getLuminance(Color color, boolean inverse) { 240 double luminance = 0.0; 241 luminance += getLuminance(color.getRed(), inverse, RGB_WEIGHT[0]); 242 luminance += getLuminance(color.getGreen(), inverse, RGB_WEIGHT[1]); 243 luminance += getLuminance(color.getBlue(), inverse, RGB_WEIGHT[2]); 244 return (int) Math.ceil((luminance / 0xFF) * 100); 245 } 246 247 private double getLuminance(int component, boolean inverse, double weight) { 248 return (inverse ? 0xFF - component : component) * weight; 249 } 250 251 private void sleep(int delay) { 252 try { 253 Thread.sleep(delay); 254 } 255 catch (InterruptedException ex) { 256 Thread.currentThread().interrupt(); 257 } 258 } 259 260 private static class Frame { 261 262 private final BufferedImage image; 263 264 private final int delayTime; 265 266 Frame(BufferedImage image, int delayTime) { 267 this.image = image; 268 this.delayTime = delayTime; 269 } 270 271 public BufferedImage getImage() { 272 return this.image; 273 } 274 275 public int getDelayTime() { 276 return this.delayTime; 277 } 278 279 } 280 281}