/** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 com.apachecon.memories.speechbubble; import java.awt.Color; import java.awt.Font; import java.awt.FontMetrics; import java.awt.GradientPaint; import java.awt.Graphics2D; import java.awt.Point; import java.awt.Polygon; import java.awt.image.BufferedImage; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; import java.util.Random; import javax.imageio.ImageIO; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class SpeechBubble { private static final Logger LOG = LoggerFactory.getLogger(SpeechBubble.class); // TODO: add some colors from the ASF feather palette private static final Color[] GRADIENTS = { Color.CYAN, Color.MAGENTA, Color.GREEN, Color.YELLOW }; private static final Random RAND = new Random(); private static final int MAX_WIDTH = 194; private static final int MAX_HEIGHT = 130; private static final int OUT_PADDING = 8; private static final int BUBBLE_WIDTH = MAX_WIDTH - 2 * OUT_PADDING; private static final int BUBBLE_HEIGHT = 84; private static final int IN_PADDING = 4; private static final int ARROW_HEIGHT = 12; private static final int ARROW_WIDTH = 8; private static final int ARROW_DELTA = 40; private static final int ICON_SIZE = 40; private static final Font DEF_FONT = new Font(Font.SERIF, Font.PLAIN, 12); private Font font; private BufferedImage twitterLogo; public SpeechBubble() { this(MAX_WIDTH, MAX_HEIGHT, DEF_FONT); } public SpeechBubble(int width, int height, Font font) { this.setFont(font != null ? font : DEF_FONT); } public Font getFont() { return font; } public void setFont(Font font) { this.font = font; } public BufferedImage getTwitterLogo() { if (twitterLogo == null) { InputStream in = getClass().getResourceAsStream("/img/twitter-logo.png"); try { BufferedImage logo = ImageIO.read(in); twitterLogo = new BufferedImage(ICON_SIZE, ICON_SIZE, BufferedImage.TYPE_INT_RGB); Graphics2D g = twitterLogo.createGraphics(); g.drawImage(logo, 0, 0, ICON_SIZE, ICON_SIZE, null); g.dispose(); } catch (IOException e) { // shouldn't happen } } return twitterLogo; } public void setTwitterLogo(BufferedImage twitterLogo) { this.twitterLogo = twitterLogo; } /** * Format message as a collection of lines to display to fit in a given * width * * @param text, the message to be displayed * @param g2d, the graphics context * @param width, the width of the bubble * @return array of lines to be displayed */ public List<String> formatText(String text, FontMetrics fm) { if (text == null) { LOG.warn("No text to format"); return null; } int lineCount = BUBBLE_HEIGHT / fm.getHeight(); List<String> lines = new ArrayList<String>(lineCount); String[] tokens = text.split(" "); String line = ""; int lineWidth = 0; // Use a dummy Image to calculate FontMetrics for (String word : tokens) { int w = fm.charsWidth((word + " ").toCharArray(), 0, word.length() + 1); lineWidth += w; if (lineWidth > BUBBLE_WIDTH) { if (lines.size() >= lineCount - 1) { line += "..."; break; } lines.add(line); line = ""; lineWidth = w; } line += word + " "; } lines.add(line); return lines; } /** * Paint speech bubble * * @param lines, the message to be displayed as collection of lines * @param g2d, the graphics context * @param width, the width of the bubble * @return array of lines to be displayed */ public void paintBubble(List<String> lines, Graphics2D graphics) { // Add the twitter logo BufferedImage logo = getTwitterLogo(); if (logo != null) { graphics.drawImage(logo, MAX_WIDTH - ICON_SIZE - OUT_PADDING, MAX_HEIGHT - ICON_SIZE, null); } graphics.setPaint(new GradientPaint(OUT_PADDING, OUT_PADDING, randomGradient(), OUT_PADDING, BUBBLE_HEIGHT - OUT_PADDING, Color.WHITE)); // First thing is to draw the bubble with a thin black outline int arc = IN_PADDING * 4; graphics.fillRoundRect(OUT_PADDING, OUT_PADDING, BUBBLE_WIDTH, BUBBLE_HEIGHT, arc, arc); graphics.setColor(Color.BLACK); graphics.drawRoundRect(OUT_PADDING, OUT_PADDING, BUBBLE_WIDTH, BUBBLE_HEIGHT, arc, arc); // With an arror at the bottom // TODO: add orientation, maybe graphics.setColor(Color.WHITE); Point arrowLeft = new Point(OUT_PADDING + BUBBLE_WIDTH - ARROW_DELTA - ARROW_WIDTH, OUT_PADDING + BUBBLE_HEIGHT); Point arrowRight = new Point(OUT_PADDING + BUBBLE_WIDTH - ARROW_DELTA, OUT_PADDING + BUBBLE_HEIGHT); Point arrowBottom = new Point(OUT_PADDING + BUBBLE_WIDTH - ARROW_DELTA, OUT_PADDING + BUBBLE_HEIGHT + ARROW_HEIGHT); Polygon arrow = new Polygon(new int[] {arrowLeft.x, arrowRight.x, arrowBottom.x}, new int[] {arrowLeft.y, arrowRight.y, arrowBottom.y}, 3); graphics.fillPolygon(arrow); graphics.setColor(Color.BLACK); graphics.drawLine(arrowLeft.x, arrowLeft.y, arrowBottom.x, arrowBottom.y); graphics.drawLine(arrowRight.x, arrowRight.y, arrowBottom.x, arrowBottom.y); graphics.setColor(Color.BLACK); int offset = OUT_PADDING + IN_PADDING; FontMetrics fm = graphics.getFontMetrics(); for (int i = 0; i < lines.size(); i++) { graphics.drawString(lines.get(i), offset, offset + (i + 1) * fm.getHeight()); } } /** * Generate image for speech bubble! * * @param g, the graphics object * @param s, the string to draw * @param f, the font we shall draw with * @param x, x-coordinate of the bubble relative to the arrow * @param y, y-coordinate of the bubble relative to the arrow * * Note: JPEG format does not support transparency so we need to * use TYPE_INT_RGB and not TYPE_INT_ARGB (i.e. no alpha). */ public BufferedImage generateBubbleImage(String s) { // Need a temporary image to process text lines BufferedImage image = new BufferedImage(BUBBLE_WIDTH, BUBBLE_HEIGHT, BufferedImage.TYPE_INT_RGB); FontMetrics fm = image.createGraphics().getFontMetrics(); List<String> lines = this.formatText(s, fm); image = new BufferedImage(MAX_WIDTH, MAX_HEIGHT, BufferedImage.TYPE_INT_RGB); Graphics2D graphics = image.createGraphics(); graphics.setColor(Color.WHITE); graphics.fillRect(0, 0, MAX_WIDTH, MAX_HEIGHT); paintBubble(lines, graphics); graphics.dispose(); return image; } public static Color randomGradient() { return GRADIENTS[RAND.nextInt(GRADIENTS.length)]; } }