Commit 58d77ec9 authored by Craig Burke's avatar Craig Burke Committed by Phillip Webb

Support image based banners

Add ImageBanner class that generates color ASCII art based on an image
file (banner.gif, banner.jpg or banner.png).

See gh-4647
parent 6550bb4c
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot;
import java.awt.Color;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.io.PrintStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import javax.imageio.ImageIO;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.ansi.AnsiPropertySource;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertyResolver;
import org.springframework.core.env.PropertySourcesPropertyResolver;
import org.springframework.core.io.Resource;
import org.springframework.util.Assert;
/**
* Banner implementation that prints ASCII art generated from an image resource
* {@link Resource}.
*
* @author Craig Burke
*/
public class ImageBanner implements Banner {
private static final Log log = LogFactory.getLog(ImageBanner.class);
private static final double RED_WEIGHT = 0.2126d;
private static final double GREEN_WEIGHT = 0.7152d;
private static final double BLUE_WEIGHT = 0.0722d;
private static final int DEFAULT_MAX_WIDTH = 72;
private static final double DEFAULT_ASPECT_RATIO = 0.5d;
private static final boolean DEFAULT_DARK = false;
private Resource image;
private Map<String, Color> colors = new HashMap<String, Color>();
public ImageBanner(Resource image) {
Assert.notNull(image, "Image must not be null");
Assert.isTrue(image.exists(), "Image must exist");
this.image = image;
colorsInit();
}
private void colorsInit() {
this.colors.put("BLACK", new Color(0, 0, 0));
this.colors.put("RED", new Color(170, 0, 0));
this.colors.put("GREEN", new Color(0, 170, 0));
this.colors.put("YELLOW", new Color(170, 85, 0));
this.colors.put("BLUE", new Color(0, 0, 170));
this.colors.put("MAGENTA", new Color(170, 0, 170));
this.colors.put("CYAN", new Color(0, 170, 170));
this.colors.put("WHITE", new Color(170, 170, 170));
this.colors.put("BRIGHT_BLACK", new Color(85, 85, 85));
this.colors.put("BRIGHT_RED", new Color(255, 85, 85));
this.colors.put("BRIGHT_GREEN", new Color(85, 255, 85));
this.colors.put("BRIGHT_YELLOW", new Color(255, 255, 85));
this.colors.put("BRIGHT_BLUE", new Color(85, 85, 255));
this.colors.put("BRIGHT_MAGENTA", new Color(255, 85, 255));
this.colors.put("BRIGHT_CYAN", new Color(85, 255, 255));
this.colors.put("BRIGHT_WHITE", new Color(255, 255, 255));
}
@Override
public void printBanner(Environment environment, Class<?> sourceClass, PrintStream out) {
String headlessProperty = System.getProperty("java.awt.headless");
try {
System.setProperty("java.awt.headless", "true");
BufferedImage sourceImage = ImageIO.read(this.image.getInputStream());
int maxWidth = environment.getProperty("banner.image.max-width",
Integer.class, DEFAULT_MAX_WIDTH);
Double aspectRatio = environment.getProperty("banner.image.aspect-ratio",
Double.class, DEFAULT_ASPECT_RATIO);
boolean invert = environment.getProperty("banner.image.dark", Boolean.class,
DEFAULT_DARK);
BufferedImage resizedImage = resizeImage(sourceImage, maxWidth, aspectRatio);
String banner = imageToBanner(resizedImage, invert);
PropertyResolver ansiResolver = getAnsiResolver();
banner = ansiResolver.resolvePlaceholders(banner);
out.println(banner);
}
catch (Exception ex) {
log.warn("Image banner not printable: " + this.image + " (" + ex.getClass()
+ ": '" + ex.getMessage() + "')", ex);
}
finally {
System.setProperty("java.awt.headless", headlessProperty);
}
}
private PropertyResolver getAnsiResolver() {
MutablePropertySources sources = new MutablePropertySources();
sources.addFirst(new AnsiPropertySource("ansi", true));
return new PropertySourcesPropertyResolver(sources);
}
private String imageToBanner(BufferedImage image, boolean dark) {
StringBuilder banner = new StringBuilder();
for (int y = 0; y < image.getHeight(); y++) {
if (dark) {
banner.append("${AnsiBackground.BLACK}");
}
else {
banner.append("${AnsiBackground.DEFAULT}");
}
for (int x = 0; x < image.getWidth(); x++) {
Color color = new Color(image.getRGB(x, y), false);
banner.append(getFormatString(color, dark));
}
if (dark) {
banner.append("${AnsiBackground.DEFAULT}");
}
banner.append("${AnsiColor.DEFAULT}\n");
}
return banner.toString();
}
protected String getFormatString(Color color, boolean dark) {
String matchedColorName = null;
Double minColorDistance = null;
for (Entry<String, Color> colorOption : this.colors.entrySet()) {
double distance = getColorDistance(color, colorOption.getValue());
if (minColorDistance == null || distance < minColorDistance) {
minColorDistance = distance;
matchedColorName = colorOption.getKey();
}
}
return "${AnsiColor." + matchedColorName + "}" + getAsciiCharacter(color, dark);
}
private static int getLuminance(Color color, boolean inverse) {
double red = color.getRed();
double green = color.getGreen();
double blue = color.getBlue();
double luminance;
if (inverse) {
luminance = (RED_WEIGHT * (255.0d - red)) + (GREEN_WEIGHT * (255.0d - green))
+ (BLUE_WEIGHT * (255.0d - blue));
}
else {
luminance = (RED_WEIGHT * red) + (GREEN_WEIGHT * green)
+ (BLUE_WEIGHT * blue);
}
return (int) Math.ceil((luminance / 255.0d) * 100);
}
private static char getAsciiCharacter(Color color, boolean dark) {
double luminance = getLuminance(color, dark);
if (luminance >= 90) {
return ' ';
}
else if (luminance >= 80) {
return '.';
}
else if (luminance >= 70) {
return '*';
}
else if (luminance >= 60) {
return ':';
}
else if (luminance >= 50) {
return 'o';
}
else if (luminance >= 40) {
return '&';
}
else if (luminance >= 30) {
return '8';
}
else if (luminance >= 20) {
return '#';
}
else {
return '@';
}
}
private static double getColorDistance(Color color1, Color color2) {
double redDelta = (color1.getRed() - color2.getRed()) * RED_WEIGHT;
double greenDelta = (color1.getGreen() - color2.getGreen()) * GREEN_WEIGHT;
double blueDelta = (color1.getBlue() - color2.getBlue()) * BLUE_WEIGHT;
return Math.pow(redDelta, 2.0d) + Math.pow(greenDelta, 2.0d)
+ Math.pow(blueDelta, 2.0d);
}
private static BufferedImage resizeImage(BufferedImage sourceImage, int maxWidth,
double aspectRatio) {
int width;
double resizeRatio;
if (sourceImage.getWidth() > maxWidth) {
resizeRatio = (double) maxWidth / (double) sourceImage.getWidth();
width = maxWidth;
}
else {
resizeRatio = 1.0d;
width = sourceImage.getWidth();
}
int height = (int) (Math.ceil(resizeRatio * aspectRatio
* (double) sourceImage.getHeight()));
Image image = sourceImage.getScaledInstance(width, height, Image.SCALE_DEFAULT);
BufferedImage resizedImage = new BufferedImage(image.getWidth(null),
image.getHeight(null), BufferedImage.TYPE_INT_RGB);
resizedImage.getGraphics().drawImage(image, 0, 0, null);
return resizedImage;
}
}
...@@ -140,6 +140,7 @@ import org.springframework.web.context.support.StandardServletEnvironment; ...@@ -140,6 +140,7 @@ import org.springframework.web.context.support.StandardServletEnvironment;
* @author Christian Dupuis * @author Christian Dupuis
* @author Stephane Nicoll * @author Stephane Nicoll
* @author Jeremy Rickard * @author Jeremy Rickard
* @author Craig Burke
* @see #run(Object, String[]) * @see #run(Object, String[])
* @see #run(Object[], String[]) * @see #run(Object[], String[])
* @see #SpringApplication(Object...) * @see #SpringApplication(Object...)
...@@ -569,9 +570,27 @@ public class SpringApplication { ...@@ -569,9 +570,27 @@ public class SpringApplication {
if (this.banner != null) { if (this.banner != null) {
return this.banner; return this.banner;
} }
Resource image = getBannerImage(environment, resourceLoader);
if (image.exists()) {
return new ImageBanner(image);
}
return DEFAULT_BANNER; return DEFAULT_BANNER;
} }
private Resource getBannerImage(Environment environment, ResourceLoader resourceLoader) {
String imageLocation = environment.getProperty("banner.image", "banner.gif");
Resource image = resourceLoader.getResource(imageLocation);
if (!image.exists()) {
image = resourceLoader.getResource("banner.jpg");
}
if (!image.exists()) {
image = resourceLoader.getResource("banner.png");
}
return image;
}
private String createStringFromBanner(Banner banner, Environment environment) private String createStringFromBanner(Banner banner, Environment environment)
throws UnsupportedEncodingException { throws UnsupportedEncodingException {
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream();
......
/*
* Copyright 2012-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.util.HashMap;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import org.springframework.boot.ansi.AnsiBackground;
import org.springframework.boot.ansi.AnsiColor;
import org.springframework.boot.ansi.AnsiElement;
import org.springframework.boot.ansi.AnsiOutput;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.mock.env.MockEnvironment;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link ImageBanner}.
*
* @author Craig Burke
*/
public class ImageBannerTests {
private static final String IMAGE_BANNER_BLACK_AND_WHITE = "banners/black-and-white.gif";
private static final String IMAGE_BANNER_LARGE = "banners/large.gif";
private static final String IMAGE_BANNER_ALL_COLORS = "banners/colors.gif";
private static final String IMAGE_BANNER_GRADIENT = "banners/gradient.gif";
private static final String BACKGROUND_DEFAULT_ANSI = getAnsiOutput(AnsiBackground.DEFAULT);
private static final String BACKGROUND_DARK_ANSI = getAnsiOutput(AnsiBackground.BLACK);
private static final char HIGH_LUMINANCE_CHARACTER = ' ';
private static final char LOW_LUMINANCE_CHARACTER = '@';
private static Map<String, Object> properties;
@Before
public void setup() {
AnsiOutput.setEnabled(AnsiOutput.Enabled.ALWAYS);
properties = new HashMap<String, Object>();
}
@Test
public void renderDefaultBackground() {
String banner = printBanner(IMAGE_BANNER_BLACK_AND_WHITE);
assertThat(banner, startsWith(BACKGROUND_DEFAULT_ANSI));
}
@Test
public void renderDarkBackground() {
setDark(true);
String banner = printBanner(IMAGE_BANNER_BLACK_AND_WHITE);
assertThat(banner, startsWith(BACKGROUND_DARK_ANSI));
}
@Test
public void renderWhiteCharactersWithColors() {
String banner = printBanner(IMAGE_BANNER_BLACK_AND_WHITE);
String expectedFirstLine = getAnsiOutput(AnsiColor.BRIGHT_WHITE)
+ HIGH_LUMINANCE_CHARACTER;
assertThat(banner, containsString(expectedFirstLine));
}
@Test
public void renderWhiteCharactersOnDarkBackground() {
setDark(true);
String banner = printBanner(IMAGE_BANNER_BLACK_AND_WHITE);
String expectedFirstLine = getAnsiOutput(AnsiColor.BRIGHT_WHITE)
+ LOW_LUMINANCE_CHARACTER;
assertThat(banner, containsString(expectedFirstLine));
}
@Test
public void renderBlackCharactersOnDefaultBackground() {
String banner = printBanner(IMAGE_BANNER_BLACK_AND_WHITE);
String blackCharacter = getAnsiOutput(AnsiColor.BLACK) + LOW_LUMINANCE_CHARACTER;
assertThat(banner, containsString(blackCharacter));
}
@Test
public void renderBlackCharactersOnDarkBackground() {
setDark(true);
String banner = printBanner(IMAGE_BANNER_BLACK_AND_WHITE);
String blackCharacter = getAnsiOutput(AnsiColor.BLACK) + HIGH_LUMINANCE_CHARACTER;
assertThat(banner, containsString(blackCharacter));
}
@Test
public void renderBannerWithAllColors() {
String banner = printBanner(IMAGE_BANNER_ALL_COLORS);
assertThat("Banner contains BLACK", banner,
containsString(getAnsiOutput(AnsiColor.BLACK)));
assertThat("Banner contains RED", banner,
containsString(getAnsiOutput(AnsiColor.RED)));
assertThat("Banner contains GREEN", banner,
containsString(getAnsiOutput(AnsiColor.GREEN)));
assertThat("Banner contains YELLOW", banner,
containsString(getAnsiOutput(AnsiColor.YELLOW)));
assertThat("Banner contains BLUE", banner,
containsString(getAnsiOutput(AnsiColor.BLUE)));
assertThat("Banner contains MAGENTA", banner,
containsString(getAnsiOutput(AnsiColor.MAGENTA)));
assertThat("Banner contains CYAN", banner,
containsString(getAnsiOutput(AnsiColor.CYAN)));
assertThat("Banner contains WHITE", banner,
containsString(getAnsiOutput(AnsiColor.WHITE)));
assertThat("Banner contains BRIGHT_BLACK", banner,
containsString(getAnsiOutput(AnsiColor.BRIGHT_BLACK)));
assertThat("Banner contains BRIGHT_RED", banner,
containsString(getAnsiOutput(AnsiColor.BRIGHT_RED)));
assertThat("Banner contains BRIGHT_GREEN", banner,
containsString(getAnsiOutput(AnsiColor.BRIGHT_GREEN)));
assertThat("Banner contains BRIGHT_YELLOW", banner,
containsString(getAnsiOutput(AnsiColor.BRIGHT_YELLOW)));
assertThat("Banner contains BRIGHT_BLUE", banner,
containsString(getAnsiOutput(AnsiColor.BRIGHT_BLUE)));
assertThat("Banner contains BRIGHT_MAGENTA", banner,
containsString(getAnsiOutput(AnsiColor.BRIGHT_MAGENTA)));
assertThat("Banner contains BRIGHT_CYAN", banner,
containsString(getAnsiOutput(AnsiColor.BRIGHT_CYAN)));
assertThat("Banner contains BRIGHT_WHITE", banner,
containsString(getAnsiOutput(AnsiColor.BRIGHT_WHITE)));
}
@Test
public void renderSimpleGradient() {
AnsiOutput.setEnabled(AnsiOutput.Enabled.NEVER);
String banner = printBanner(IMAGE_BANNER_GRADIENT);
String expectedResult = "@#8&o:*. ";
assertThat(banner, startsWith(expectedResult));
}
@Test
public void renderBannerWithDefaultAspectRatio() {
String banner = printBanner(IMAGE_BANNER_BLACK_AND_WHITE);
int bannerHeight = getBannerHeight(banner);
assertThat(bannerHeight, equalTo(2));
}
@Test
public void renderBannerWithCustomAspectRatio() {
setAspectRatio(1.0d);
String banner = printBanner(IMAGE_BANNER_BLACK_AND_WHITE);
int bannerHeight = getBannerHeight(banner);
assertThat(bannerHeight, equalTo(4));
}
@Test
public void renderLargeBanner() {
String banner = printBanner(IMAGE_BANNER_LARGE);
int bannerWidth = getBannerWidth(banner);
assertThat(bannerWidth, equalTo(72));
}
@Test
public void renderLargeBannerWithACustomWidth() {
setMaxWidth(60);
String banner = printBanner(IMAGE_BANNER_LARGE);
int bannerWidth = getBannerWidth(banner);
assertThat(bannerWidth, equalTo(60));
}
private int getBannerHeight(String banner) {
return banner.split("\n").length;
}
private int getBannerWidth(String banner) {
String strippedBanner = banner.replaceAll("\u001B\\[.*?m", "");
String firstLine = strippedBanner.split("\n")[0];
return firstLine.length();
}
private static String getAnsiOutput(AnsiElement ansi) {
return "\u001B[" + ansi.toString() + "m";
}
private void setDark(boolean dark) {
properties.put("banner.image.dark", dark);
}
private void setMaxWidth(int maxWidth) {
properties.put("banner.image.max-width", maxWidth);
}
private void setAspectRatio(double aspectRatio) {
properties.put("banner.image.aspect-ratio", aspectRatio);
}
private String printBanner(String imagePath) {
Resource image = new ClassPathResource(imagePath);
ImageBanner banner = new ImageBanner(image);
ConfigurableEnvironment environment = new MockEnvironment();
environment.getPropertySources().addLast(
new MapPropertySource("testConfig", properties));
ByteArrayOutputStream out = new ByteArrayOutputStream();
banner.printBanner(environment, getClass(), new PrintStream(out));
return out.toString();
}
}
...@@ -19,8 +19,10 @@ package org.springframework.boot; ...@@ -19,8 +19,10 @@ package org.springframework.boot;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
...@@ -66,6 +68,7 @@ import org.springframework.core.env.MapPropertySource; ...@@ -66,6 +68,7 @@ import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.PropertySource; import org.springframework.core.env.PropertySource;
import org.springframework.core.env.StandardEnvironment; import org.springframework.core.env.StandardEnvironment;
import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.ResourceLoader;
import org.springframework.test.context.support.TestPropertySourceUtils; import org.springframework.test.context.support.TestPropertySourceUtils;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
...@@ -90,6 +93,7 @@ import static org.mockito.Mockito.verify; ...@@ -90,6 +93,7 @@ import static org.mockito.Mockito.verify;
* @author Christian Dupuis * @author Christian Dupuis
* @author Stephane Nicoll * @author Stephane Nicoll
* @author Jeremy Rickard * @author Jeremy Rickard
* @author Craig Burke
*/ */
public class SpringApplicationTests { public class SpringApplicationTests {
...@@ -191,6 +195,32 @@ public class SpringApplicationTests { ...@@ -191,6 +195,32 @@ public class SpringApplicationTests {
.startsWith(String.format("Running a Test!%n%n123456")); .startsWith(String.format("Running a Test!%n%n123456"));
} }
@Test
public void textBannerTakesPrecedence() throws Exception {
SpringApplication application = new SpringApplication(ExampleConfig.class);
BannerResourceLoaderStub resourceLoader = new BannerResourceLoaderStub();
resourceLoader.addResource("banner.gif", "banners/black-and-white.gif");
resourceLoader.addResource("banner.txt", "banners/foobar.txt");
application.setWebEnvironment(false);
application.setResourceLoader(resourceLoader);
application.run();
assertThat(this.output.toString()).startsWith("Foo Bar");
}
@Test
public void imageBannerLoads() throws Exception {
SpringApplication application = new SpringApplication(ExampleConfig.class);
BannerResourceLoaderStub resourceLoader = new BannerResourceLoaderStub();
resourceLoader.addResource("banner.gif", "banners/black-and-white.gif");
application.setWebEnvironment(false);
application.setResourceLoader(resourceLoader);
application.run();
assertThat(this.output.toString()).startsWith("@");
}
@Test @Test
public void logsNoActiveProfiles() throws Exception { public void logsNoActiveProfiles() throws Exception {
SpringApplication application = new SpringApplication(ExampleConfig.class); SpringApplication application = new SpringApplication(ExampleConfig.class);
...@@ -1089,4 +1119,31 @@ public class SpringApplicationTests { ...@@ -1089,4 +1119,31 @@ public class SpringApplicationTests {
} }
} }
private static class BannerResourceLoaderStub extends DefaultResourceLoader {
private Map<String, String> resources = new HashMap<String, String>();
Resource notFoundResource;
BannerResourceLoaderStub() {
this.notFoundResource = super.getResource("classpath:foo/bar/foobar");
assert !this.notFoundResource.exists();
}
public void addResource(String file, String realPath) {
this.resources.put(file, realPath);
}
@Override
public Resource getResource(String s) {
if (this.resources.containsKey(s)) {
return super.getResource("classpath:" + this.resources.get(s));
}
else {
return this.notFoundResource;
}
}
}
} }
Foo Bar
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment