6 changed files with 34 additions and 623 deletions
@ -1,312 +0,0 @@
@@ -1,312 +0,0 @@
|
||||
/* |
||||
* Copyright 2012-2019 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 |
||||
* |
||||
* https://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.IOException; |
||||
import java.io.InputStream; |
||||
import java.io.PrintStream; |
||||
import java.util.Iterator; |
||||
|
||||
import javax.imageio.ImageIO; |
||||
import javax.imageio.ImageReadParam; |
||||
import javax.imageio.ImageReader; |
||||
import javax.imageio.metadata.IIOMetadata; |
||||
import javax.imageio.metadata.IIOMetadataNode; |
||||
import javax.imageio.stream.ImageInputStream; |
||||
|
||||
import org.apache.commons.logging.Log; |
||||
import org.apache.commons.logging.LogFactory; |
||||
|
||||
import org.springframework.boot.ansi.AnsiBackground; |
||||
import org.springframework.boot.ansi.AnsiColor; |
||||
import org.springframework.boot.ansi.AnsiColors; |
||||
import org.springframework.boot.ansi.AnsiColors.BitDepth; |
||||
import org.springframework.boot.ansi.AnsiElement; |
||||
import org.springframework.boot.ansi.AnsiOutput; |
||||
import org.springframework.core.env.Environment; |
||||
import org.springframework.core.io.Resource; |
||||
import org.springframework.core.log.LogMessage; |
||||
import org.springframework.util.Assert; |
||||
|
||||
/** |
||||
* Banner implementation that prints ASCII art generated from an image resource |
||||
* {@link Resource}. |
||||
* |
||||
* @author Craig Burke |
||||
* @author Phillip Webb |
||||
* @author Madhura Bhave |
||||
* @author Raja Kolli |
||||
* @since 1.4.0 |
||||
*/ |
||||
public class ImageBanner implements Banner { |
||||
|
||||
private static final String PROPERTY_PREFIX = "spring.banner.image."; |
||||
|
||||
private static final Log logger = LogFactory.getLog(ImageBanner.class); |
||||
|
||||
private static final double[] RGB_WEIGHT = { 0.2126d, 0.7152d, 0.0722d }; |
||||
|
||||
private final Resource image; |
||||
|
||||
public ImageBanner(Resource image) { |
||||
Assert.notNull(image, "Image must not be null"); |
||||
Assert.isTrue(image.exists(), "Image must exist"); |
||||
this.image = image; |
||||
} |
||||
|
||||
@Override |
||||
public void printBanner(Environment environment, Class<?> sourceClass, PrintStream out) { |
||||
String headless = System.getProperty("java.awt.headless"); |
||||
try { |
||||
System.setProperty("java.awt.headless", "true"); |
||||
printBanner(environment, out); |
||||
} |
||||
catch (Throwable ex) { |
||||
logger.warn(LogMessage.format("Image banner not printable: %s (%s: '%s')", this.image, ex.getClass(), |
||||
ex.getMessage())); |
||||
logger.debug("Image banner printing failure", ex); |
||||
} |
||||
finally { |
||||
if (headless == null) { |
||||
System.clearProperty("java.awt.headless"); |
||||
} |
||||
else { |
||||
System.setProperty("java.awt.headless", headless); |
||||
} |
||||
} |
||||
} |
||||
|
||||
private void printBanner(Environment environment, PrintStream out) throws IOException { |
||||
int width = getProperty(environment, "width", Integer.class, 76); |
||||
int height = getProperty(environment, "height", Integer.class, 0); |
||||
int margin = getProperty(environment, "margin", Integer.class, 2); |
||||
boolean invert = getProperty(environment, "invert", Boolean.class, false); |
||||
BitDepth bitDepth = getBitDepthProperty(environment); |
||||
PixelMode pixelMode = getPixelModeProperty(environment); |
||||
Frame[] frames = readFrames(width, height); |
||||
for (int i = 0; i < frames.length; i++) { |
||||
if (i > 0) { |
||||
resetCursor(frames[i - 1].getImage(), out); |
||||
} |
||||
printBanner(frames[i].getImage(), margin, invert, bitDepth, pixelMode, out); |
||||
sleep(frames[i].getDelayTime()); |
||||
} |
||||
} |
||||
|
||||
private BitDepth getBitDepthProperty(Environment environment) { |
||||
Integer bitDepth = getProperty(environment, "bitdepth", Integer.class, null); |
||||
return (bitDepth != null) ? BitDepth.of(bitDepth) : BitDepth.FOUR; |
||||
} |
||||
|
||||
private PixelMode getPixelModeProperty(Environment environment) { |
||||
String pixelMode = getProperty(environment, "pixelmode", String.class, null); |
||||
return (pixelMode != null) ? PixelMode.valueOf(pixelMode.trim().toUpperCase()) : PixelMode.TEXT; |
||||
} |
||||
|
||||
private <T> T getProperty(Environment environment, String name, Class<T> targetType, T defaultValue) { |
||||
return environment.getProperty(PROPERTY_PREFIX + name, targetType, defaultValue); |
||||
} |
||||
|
||||
private Frame[] readFrames(int width, int height) throws IOException { |
||||
try (InputStream inputStream = this.image.getInputStream()) { |
||||
try (ImageInputStream imageStream = ImageIO.createImageInputStream(inputStream)) { |
||||
return readFrames(width, height, imageStream); |
||||
} |
||||
} |
||||
} |
||||
|
||||
private Frame[] readFrames(int width, int height, ImageInputStream stream) throws IOException { |
||||
Iterator<ImageReader> readers = ImageIO.getImageReaders(stream); |
||||
Assert.state(readers.hasNext(), "Unable to read image banner source"); |
||||
ImageReader reader = readers.next(); |
||||
try { |
||||
ImageReadParam readParam = reader.getDefaultReadParam(); |
||||
reader.setInput(stream); |
||||
int frameCount = reader.getNumImages(true); |
||||
Frame[] frames = new Frame[frameCount]; |
||||
for (int i = 0; i < frameCount; i++) { |
||||
frames[i] = readFrame(width, height, reader, i, readParam); |
||||
} |
||||
return frames; |
||||
} |
||||
finally { |
||||
reader.dispose(); |
||||
} |
||||
} |
||||
|
||||
private Frame readFrame(int width, int height, ImageReader reader, int imageIndex, ImageReadParam readParam) |
||||
throws IOException { |
||||
BufferedImage image = reader.read(imageIndex, readParam); |
||||
BufferedImage resized = resizeImage(image, width, height); |
||||
int delayTime = getDelayTime(reader, imageIndex); |
||||
return new Frame(resized, delayTime); |
||||
} |
||||
|
||||
private int getDelayTime(ImageReader reader, int imageIndex) throws IOException { |
||||
IIOMetadata metadata = reader.getImageMetadata(imageIndex); |
||||
IIOMetadataNode root = (IIOMetadataNode) metadata.getAsTree(metadata.getNativeMetadataFormatName()); |
||||
IIOMetadataNode extension = findNode(root, "GraphicControlExtension"); |
||||
String attribute = (extension != null) ? extension.getAttribute("delayTime") : null; |
||||
return (attribute != null) ? Integer.parseInt(attribute) * 10 : 0; |
||||
} |
||||
|
||||
private static IIOMetadataNode findNode(IIOMetadataNode rootNode, String nodeName) { |
||||
if (rootNode == null) { |
||||
return null; |
||||
} |
||||
for (int i = 0; i < rootNode.getLength(); i++) { |
||||
if (rootNode.item(i).getNodeName().equalsIgnoreCase(nodeName)) { |
||||
return ((IIOMetadataNode) rootNode.item(i)); |
||||
} |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
private BufferedImage resizeImage(BufferedImage image, int width, int height) { |
||||
if (width < 1) { |
||||
width = 1; |
||||
} |
||||
if (height <= 0) { |
||||
double aspectRatio = (double) width / image.getWidth() * 0.5; |
||||
height = (int) Math.ceil(image.getHeight() * aspectRatio); |
||||
} |
||||
BufferedImage resized = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); |
||||
Image scaled = image.getScaledInstance(width, height, Image.SCALE_DEFAULT); |
||||
resized.getGraphics().drawImage(scaled, 0, 0, null); |
||||
return resized; |
||||
} |
||||
|
||||
private void resetCursor(BufferedImage image, PrintStream out) { |
||||
int lines = image.getHeight() + 3; |
||||
out.print("\033[" + lines + "A\r"); |
||||
} |
||||
|
||||
private void printBanner(BufferedImage image, int margin, boolean invert, BitDepth bitDepth, PixelMode pixelMode, |
||||
PrintStream out) { |
||||
AnsiElement background = invert ? AnsiBackground.BLACK : AnsiBackground.DEFAULT; |
||||
out.print(AnsiOutput.encode(AnsiColor.DEFAULT)); |
||||
out.print(AnsiOutput.encode(background)); |
||||
out.println(); |
||||
out.println(); |
||||
AnsiElement lastColor = AnsiColor.DEFAULT; |
||||
AnsiColors colors = new AnsiColors(bitDepth); |
||||
for (int y = 0; y < image.getHeight(); y++) { |
||||
for (int i = 0; i < margin; i++) { |
||||
out.print(" "); |
||||
} |
||||
for (int x = 0; x < image.getWidth(); x++) { |
||||
Color color = new Color(image.getRGB(x, y), false); |
||||
AnsiElement ansiColor = colors.findClosest(color); |
||||
if (ansiColor != lastColor) { |
||||
out.print(AnsiOutput.encode(ansiColor)); |
||||
lastColor = ansiColor; |
||||
} |
||||
out.print(getAsciiPixel(color, invert, pixelMode)); |
||||
} |
||||
out.println(); |
||||
} |
||||
out.print(AnsiOutput.encode(AnsiColor.DEFAULT)); |
||||
out.print(AnsiOutput.encode(AnsiBackground.DEFAULT)); |
||||
out.println(); |
||||
} |
||||
|
||||
private char getAsciiPixel(Color color, boolean dark, PixelMode pixelMode) { |
||||
char[] pixels = pixelMode.getPixels(); |
||||
int increment = (10 / pixels.length) * 10; |
||||
int start = increment * pixels.length; |
||||
double luminance = getLuminance(color, dark); |
||||
for (int i = 0; i < pixels.length; i++) { |
||||
if (luminance >= (start - (i * increment))) { |
||||
return pixels[i]; |
||||
} |
||||
} |
||||
return pixels[pixels.length - 1]; |
||||
} |
||||
|
||||
private int getLuminance(Color color, boolean inverse) { |
||||
double luminance = 0.0; |
||||
luminance += getLuminance(color.getRed(), inverse, RGB_WEIGHT[0]); |
||||
luminance += getLuminance(color.getGreen(), inverse, RGB_WEIGHT[1]); |
||||
luminance += getLuminance(color.getBlue(), inverse, RGB_WEIGHT[2]); |
||||
return (int) Math.ceil((luminance / 0xFF) * 100); |
||||
} |
||||
|
||||
private double getLuminance(int component, boolean inverse, double weight) { |
||||
return (inverse ? 0xFF - component : component) * weight; |
||||
} |
||||
|
||||
private void sleep(int delay) { |
||||
try { |
||||
Thread.sleep(delay); |
||||
} |
||||
catch (InterruptedException ex) { |
||||
Thread.currentThread().interrupt(); |
||||
} |
||||
} |
||||
|
||||
private static class Frame { |
||||
|
||||
private final BufferedImage image; |
||||
|
||||
private final int delayTime; |
||||
|
||||
Frame(BufferedImage image, int delayTime) { |
||||
this.image = image; |
||||
this.delayTime = delayTime; |
||||
} |
||||
|
||||
BufferedImage getImage() { |
||||
return this.image; |
||||
} |
||||
|
||||
int getDelayTime() { |
||||
return this.delayTime; |
||||
} |
||||
|
||||
} |
||||
|
||||
/** |
||||
* Pixel modes supported by the image banner. |
||||
*/ |
||||
public enum PixelMode { |
||||
|
||||
/** |
||||
* Use text chars for pixels. |
||||
*/ |
||||
TEXT(' ', '.', '*', ':', 'o', '&', '8', '#', '@'), |
||||
|
||||
/** |
||||
* Use unicode block chars for pixels. |
||||
*/ |
||||
BLOCK(' ', '\u2591', '\u2592', '\u2593', '\u2588'); |
||||
|
||||
private char[] pixels; |
||||
|
||||
PixelMode(char... pixels) { |
||||
this.pixels = pixels; |
||||
} |
||||
|
||||
char[] getPixels() { |
||||
return this.pixels; |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -1,217 +0,0 @@
@@ -1,217 +0,0 @@
|
||||
/* |
||||
* Copyright 2012-2019 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 |
||||
* |
||||
* https://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.io.UnsupportedEncodingException; |
||||
|
||||
import org.junit.jupiter.api.AfterEach; |
||||
import org.junit.jupiter.api.BeforeEach; |
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import org.springframework.boot.ansi.Ansi8BitColor; |
||||
import org.springframework.boot.ansi.AnsiBackground; |
||||
import org.springframework.boot.ansi.AnsiColor; |
||||
import org.springframework.boot.ansi.AnsiOutput; |
||||
import org.springframework.boot.ansi.AnsiOutput.Enabled; |
||||
import org.springframework.core.env.ConfigurableEnvironment; |
||||
import org.springframework.core.io.ClassPathResource; |
||||
import org.springframework.mock.env.MockEnvironment; |
||||
import org.springframework.test.context.support.TestPropertySourceUtils; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
||||
/** |
||||
* Tests for {@link ImageBanner}. |
||||
* |
||||
* @author Craig Burke |
||||
* @author Phillip Webb |
||||
*/ |
||||
class ImageBannerTests { |
||||
|
||||
private static final char HIGH_LUMINANCE_CHARACTER = ' '; |
||||
|
||||
private static final char LOW_LUMINANCE_CHARACTER = '@'; |
||||
|
||||
private static final String INVERT_TRUE = "spring.banner.image.invert=true"; |
||||
|
||||
@BeforeEach |
||||
void setup() { |
||||
AnsiOutput.setEnabled(AnsiOutput.Enabled.ALWAYS); |
||||
} |
||||
|
||||
@AfterEach |
||||
void cleanup() { |
||||
AnsiOutput.setEnabled(Enabled.DETECT); |
||||
} |
||||
|
||||
@Test |
||||
void printBannerShouldResetForegroundAndBackground() { |
||||
String banner = printBanner("black-and-white.gif"); |
||||
String expected = AnsiOutput.encode(AnsiColor.DEFAULT) + AnsiOutput.encode(AnsiBackground.DEFAULT); |
||||
assertThat(banner).startsWith(expected); |
||||
} |
||||
|
||||
@Test |
||||
void printBannerWhenInvertedShouldResetForegroundAndBackground() { |
||||
String banner = printBanner("black-and-white.gif", INVERT_TRUE); |
||||
String expected = AnsiOutput.encode(AnsiColor.DEFAULT) + AnsiOutput.encode(AnsiBackground.BLACK); |
||||
assertThat(banner).startsWith(expected); |
||||
} |
||||
|
||||
@Test |
||||
void printBannerShouldPrintWhiteAsBrightWhiteHighLuminance() { |
||||
String banner = printBanner("black-and-white.gif"); |
||||
String expected = AnsiOutput.encode(AnsiColor.BRIGHT_WHITE) + HIGH_LUMINANCE_CHARACTER; |
||||
assertThat(banner).contains(expected); |
||||
} |
||||
|
||||
@Test |
||||
void printBannerWhenInvertedShouldPrintWhiteAsBrightWhiteLowLuminance() { |
||||
String banner = printBanner("black-and-white.gif", INVERT_TRUE); |
||||
String expected = AnsiOutput.encode(AnsiColor.BRIGHT_WHITE) + LOW_LUMINANCE_CHARACTER; |
||||
assertThat(banner).contains(expected); |
||||
} |
||||
|
||||
@Test |
||||
void printBannerShouldPrintBlackAsBlackLowLuminance() { |
||||
String banner = printBanner("black-and-white.gif"); |
||||
String expected = AnsiOutput.encode(AnsiColor.BLACK) + LOW_LUMINANCE_CHARACTER; |
||||
assertThat(banner).contains(expected); |
||||
} |
||||
|
||||
@Test |
||||
void printBannerWhenInvertedShouldPrintBlackAsBlackHighLuminance() { |
||||
String banner = printBanner("black-and-white.gif", INVERT_TRUE); |
||||
String expected = AnsiOutput.encode(AnsiColor.BLACK) + HIGH_LUMINANCE_CHARACTER; |
||||
assertThat(banner).contains(expected); |
||||
} |
||||
|
||||
@Test |
||||
void printBannerWhenShouldPrintAllColors() { |
||||
String banner = printBanner("colors.gif"); |
||||
for (AnsiColor color : AnsiColor.values()) { |
||||
if (color != AnsiColor.DEFAULT) { |
||||
assertThat(banner).contains(AnsiOutput.encode(color)); |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
void printBannerShouldRenderGradient() { |
||||
AnsiOutput.setEnabled(AnsiOutput.Enabled.NEVER); |
||||
String banner = printBanner("gradient.gif", "spring.banner.image.width=10", "spring.banner.image.margin=0"); |
||||
assertThat(banner).contains("@#8&o:*. "); |
||||
} |
||||
|
||||
@Test |
||||
void printBannerShouldCalculateHeight() { |
||||
String banner = printBanner("large.gif", "spring.banner.image.width=20"); |
||||
assertThat(getBannerHeight(banner)).isEqualTo(10); |
||||
} |
||||
|
||||
@Test |
||||
void printBannerWhenHasHeightPropertyShouldSetHeight() { |
||||
String banner = printBanner("large.gif", "spring.banner.image.width=20", "spring.banner.image.height=30"); |
||||
assertThat(getBannerHeight(banner)).isEqualTo(30); |
||||
} |
||||
|
||||
@Test |
||||
void printBannerShouldCapWidthAndCalculateHeight() { |
||||
AnsiOutput.setEnabled(AnsiOutput.Enabled.NEVER); |
||||
String banner = printBanner("large.gif", "spring.banner.image.margin=0"); |
||||
assertThat(getBannerWidth(banner)).isEqualTo(76); |
||||
assertThat(getBannerHeight(banner)).isEqualTo(37); |
||||
} |
||||
|
||||
@Test |
||||
void printBannerShouldPrintMargin() { |
||||
AnsiOutput.setEnabled(AnsiOutput.Enabled.NEVER); |
||||
String banner = printBanner("large.gif"); |
||||
String[] lines = banner.split(System.lineSeparator()); |
||||
for (int i = 2; i < lines.length - 1; i++) { |
||||
assertThat(lines[i]).startsWith(" @"); |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
void printBannerWhenHasMarginPropertyShouldPrintSizedMargin() { |
||||
AnsiOutput.setEnabled(AnsiOutput.Enabled.NEVER); |
||||
String banner = printBanner("large.gif", "spring.banner.image.margin=4"); |
||||
String[] lines = banner.split(System.lineSeparator()); |
||||
for (int i = 2; i < lines.length - 1; i++) { |
||||
assertThat(lines[i]).startsWith(" @"); |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
void printBannerWhenAnimatesShouldPrintAllFrames() { |
||||
AnsiOutput.setEnabled(AnsiOutput.Enabled.NEVER); |
||||
String banner = printBanner("animated.gif"); |
||||
String[] lines = banner.split(System.lineSeparator()); |
||||
int frames = 138; |
||||
int linesPerFrame = 36; |
||||
assertThat(banner).contains("\r"); |
||||
assertThat(lines).hasSize(frames * linesPerFrame - 1); |
||||
} |
||||
|
||||
@Test |
||||
void printBannerWhenBitDepthIs8ShouldUseColors() { |
||||
String banner = printBanner("colors.gif", "spring.banner.image.bitdepth=8"); |
||||
assertThat(banner).contains(AnsiOutput.encode(Ansi8BitColor.foreground(124))); |
||||
assertThat(banner).contains(AnsiOutput.encode(Ansi8BitColor.foreground(130))); |
||||
assertThat(banner).contains(AnsiOutput.encode(Ansi8BitColor.foreground(19))); |
||||
assertThat(banner).contains(AnsiOutput.encode(Ansi8BitColor.foreground(127))); |
||||
assertThat(banner).contains(AnsiOutput.encode(Ansi8BitColor.foreground(37))); |
||||
} |
||||
|
||||
@Test |
||||
void printBannerWhenPixelModeIsBlockShouldRenderBlocks() { |
||||
AnsiOutput.setEnabled(AnsiOutput.Enabled.NEVER); |
||||
String banner = printBanner("gradient.gif", "spring.banner.image.width=6", "spring.banner.image.margin=0", |
||||
"spring.banner.image.pixelmode=block"); |
||||
assertThat(banner).inHexadecimal().contains("\u2588\u2593\u2592\u2591 "); |
||||
} |
||||
|
||||
private int getBannerHeight(String banner) { |
||||
return banner.split(System.lineSeparator()).length - 3; |
||||
} |
||||
|
||||
private int getBannerWidth(String banner) { |
||||
int width = 0; |
||||
for (String line : banner.split(System.lineSeparator())) { |
||||
width = Math.max(width, line.length()); |
||||
} |
||||
return width; |
||||
} |
||||
|
||||
private String printBanner(String path, String... properties) { |
||||
ImageBanner banner = new ImageBanner(new ClassPathResource(path, getClass())); |
||||
ConfigurableEnvironment environment = new MockEnvironment(); |
||||
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(environment, properties); |
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(); |
||||
try { |
||||
banner.printBanner(environment, getClass(), new PrintStream(out, false, "UTF-8")); |
||||
return out.toString("UTF-8"); |
||||
} |
||||
catch (UnsupportedEncodingException ex) { |
||||
throw new IllegalStateException(ex); |
||||
} |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue