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
This commit is contained in:
Craig Burke 2015-11-30 12:55:28 -05:00 committed by Phillip Webb
parent 6550bb4cf1
commit 58d77ec961
9 changed files with 565 additions and 0 deletions

View File

@ -0,0 +1,249 @@
/*
* 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;
}
}

View File

@ -140,6 +140,7 @@ import org.springframework.web.context.support.StandardServletEnvironment;
* @author Christian Dupuis
* @author Stephane Nicoll
* @author Jeremy Rickard
* @author Craig Burke
* @see #run(Object, String[])
* @see #run(Object[], String[])
* @see #SpringApplication(Object...)
@ -569,9 +570,27 @@ public class SpringApplication {
if (this.banner != null) {
return this.banner;
}
Resource image = getBannerImage(environment, resourceLoader);
if (image.exists()) {
return new ImageBanner(image);
}
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)
throws UnsupportedEncodingException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();

View File

@ -0,0 +1,239 @@
/*
* 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();
}
}

View File

@ -19,8 +19,10 @@ package org.springframework.boot;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
@ -66,6 +68,7 @@ import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.PropertySource;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.test.context.support.TestPropertySourceUtils;
import org.springframework.util.StringUtils;
@ -90,6 +93,7 @@ import static org.mockito.Mockito.verify;
* @author Christian Dupuis
* @author Stephane Nicoll
* @author Jeremy Rickard
* @author Craig Burke
*/
public class SpringApplicationTests {
@ -191,6 +195,32 @@ public class SpringApplicationTests {
.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
public void logsNoActiveProfiles() throws Exception {
SpringApplication application = new SpringApplication(ExampleConfig.class);
@ -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;
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 B

View File

@ -0,0 +1 @@
Foo Bar

Binary file not shown.

After

Width:  |  Height:  |  Size: 817 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 B