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}