package net.databinder.components; import java.awt.Color; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.font.LineBreakMeasurer; import java.awt.font.TextAttribute; import java.awt.font.TextLayout; import java.awt.image.BufferedImage; import java.io.InputStream; import java.text.AttributedCharacterIterator; import java.text.AttributedString; import java.util.LinkedList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.wicket.Application; import org.apache.wicket.Component; import org.apache.wicket.Resource; import org.apache.wicket.ResourceReference; import org.apache.wicket.SharedResources; import org.apache.wicket.WicketRuntimeException; import org.apache.wicket.markup.ComponentTag; import org.apache.wicket.markup.html.image.Image; import org.apache.wicket.markup.html.image.resource.RenderedDynamicImageResource; import org.apache.wicket.model.IComponentInheritedModel; import org.apache.wicket.model.IModel; import org.apache.wicket.model.IWrapModel; import org.apache.wicket.protocol.http.WebResponse; import org.apache.wicket.util.string.Strings; /* * Databinder: a simple bridge from Wicket to Hibernate * Copyright (C) 2006 Nathan Hamblen nathan@technically.us * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ /** * Renders its model text into a PNG, using any typeface available to the JVM. The size of * the image is determined by the model text and the characteristics of font selected. The * default font is 14pt sans, plain black on a white background. The background may be set * to null for alpha transparency, which will appear gray in outdated browsers. The image's * alt attribute will be set to the model text, and width and height attributes will be * set appropriately. * <p> If told to use a shared image resource, RenderedLabel will add its image * to the application's shared resources and reference it from a permanent, unique, * browser-chacheable URL. Note that if users might request a shared resource before a * page containing it has rendered (after a context reload, for example) you should load * that resource using loadSharedResources() as the application is starting up. * <p> This class is inspired by, and draws code from, Wicket's DefaultButtonImageResource. </p> * @author Nathan Hamblen * @see SharedResources */ public class RenderedLabel extends Image { private static final long serialVersionUID = 1L; private static Font defaultFont = new Font("sans", Font.PLAIN, 14); private static Color defaultColor = Color.BLACK; private static Color defaultBackgroundColor = Color.WHITE; private Font font = defaultFont; private Color color = defaultColor; private Color backgroundColor = defaultBackgroundColor; private Integer maxWidth; private boolean antiAliased = true; /** If true, resource is shared across application with a permanent URL. */ private boolean isShared = false; /** Hash of the most recently displayed label attributes. -1 is initial value, 0 for blank labels. */ private int labelHash = -1; private RenderedTextImageResource resource; /** * Constructor to be used if model is derived from a compound property model. * @param id Wicket id */ public RenderedLabel(String id) { super(id); init(); } /** * Constructor for compound property model and shared resource pool. * @param id Wicket id * @param shareResource true to add to shared resource pool */ public RenderedLabel(String id, boolean shareResource) { this(id); this.isShared = shareResource; init(); } /** * Constructor with explicit model. * @param id Wicket id * @param model model for */ public RenderedLabel(String id, IModel model) { super(id, model); init(); } /** * Constructor with explicit model. * @param id Wicket id * @param model model for * @param shareResource true to add to shared resource pool */ public RenderedLabel(String id, IModel model, boolean shareResource) { this(id, model); this.isShared = shareResource; init(); } /** Perform generic initialization. */ protected void init() { setEscapeModelStrings(false); } @Override protected void onBeforeRender() { super.onBeforeRender(); int curHash = getLabelHash(); if (isShared) { if (labelHash != curHash) { String hash = Integer.toHexString(curHash); SharedResources shared = getApplication().getSharedResources(); try { resource = (RenderedTextImageResource) shared.get(RenderedLabel.class, hash, null, null, false); } catch (ClassCastException e) { // was placeholder for missing PackageResourceReference shared.remove(shared.resourceKey(RenderedLabel.class, hash, null, null)); } if (resource == null) shared.add(RenderedLabel.class, hash, null, null, resource = newRenderedTextImageResource(true)); setImageResourceReference(new ResourceReference(RenderedLabel.class, hash)); } } else { if (resource == null) setImageResource(resource = newRenderedTextImageResource(false)); else if (labelHash != curHash) resource.setState(this); } resource.setCacheable(isShared); labelHash = getLabelHash(); } /** * @return false if set to false or if model string is empty. */ @Override public boolean isVisible() { return super.isVisible() && getDefaultModelObject() != null; } /** * Adds image-specific attributes including width, height, and alternate text. A hash is appended * to the source URL to trigger a reload whenever drawing attributes change. */ @Override protected void onComponentTag(ComponentTag tag) { super.onComponentTag(tag); if (!isShared) { String url = tag.getAttributes().getString("src"); url = url + ((url.indexOf("?") >= 0) ? "&" : "?"); url = url + "wicket:antiCache=" + Integer.toHexString(labelHash); tag.put("src", url); } resource.preload(); tag.put("width", resource.getWidth() ); tag.put("height", resource.getHeight() ); tag.put("alt", getDefaultModelObjectAsString()); } protected int getLabelHash() { return getLabelHash(getDefaultModelObjectAsString(), font, color, backgroundColor, maxWidth); } protected static int getLabelHash(String text, Font font, Color color, Color backgroundColor, Integer maxWidth) { if (text == null) return 0; int hash= text.hashCode() ^ font.hashCode() ^ color.hashCode(); if (backgroundColor != null) hash ^= backgroundColor.hashCode(); if (maxWidth != null) hash ^= maxWidth.hashCode(); return hash; } /** Restores compound model resolution that is disabled in the Image superclass. */ @Override protected IModel<?> initModel() { // Search parents for CompoundPropertyModel for (Component current = getParent(); current != null; current = current.getParent()) { // Get model // Dont call the getModel() that could initialize many inbetween completely useless models. //IModel model = current.getModel(); IModel model = current.getDefaultModel(); if (model instanceof IWrapModel) { model = ((IWrapModel)model).getWrappedModel(); } if (model instanceof IComponentInheritedModel) { // we turn off versioning as we share the model with another // component that is the owner of the model (that component // has to decide whether to version or not setVersioned(false); // return the shared inherited model = ((IComponentInheritedModel)model).wrapOnInheritance(this); return model; } } // No model for this component! return null; } /** * Load shared resource into pool so it will be available even before a page using the * rendered label is first rendered. May be needed if a page is cachable and the context * is restarted, for example. * @param text * @param font uses default if null * @param color uses default if null * @param backgroundColor uses default if null * @param maxWidth */ public static void loadSharedResources(String text, Font font, Color color, Color backgroundColor, Integer maxWidth) { loadSharedResources(new RenderedTextImageResource(), text, font, color, backgroundColor, maxWidth); } /** * Utility method to load a specific instance of a the rendering shared resource. */ protected static void loadSharedResources(RenderedTextImageResource res, String text, Font font, Color color, Color backgroundColor, Integer maxWidth) { res.setCacheable(true); res.backgroundColor = backgroundColor == null ? defaultBackgroundColor : backgroundColor; res.color = color == null ? defaultColor : color; res.font = font == null ? defaultFont : font; res.maxWidth = maxWidth; res.text = text; String hash = Integer.toHexString(getLabelHash(text, font, color, backgroundColor, maxWidth)); SharedResources shared = Application.get().getSharedResources(); shared.add(RenderedLabel.class, hash, null, null, res); } /** * Create a new image resource to render this label. Override in a subclass to use a different * renderer. * @param isShared is a shared, cacheable resource * @return new instance of RenderedTextImageResource or subclass */ protected RenderedTextImageResource newRenderedTextImageResource(boolean isShared) { RenderedTextImageResource res = new RenderedTextImageResource(); res.setCacheable(isShared); res.setState(this); return res; } /** * Inner class that renders the model text into an image resource. */ public static class RenderedTextImageResource extends RenderedDynamicImageResource { protected Color backgroundColor; protected Color color; protected Font font; protected Integer maxWidth; protected String text; protected boolean antiAliased; protected RenderedTextImageResource() { super(1, 1,"png"); // tiny default that will resize to fit text setType(BufferedImage.TYPE_INT_ARGB); // allow alpha transparency } @Override protected void setHeaders(WebResponse response) { // don't set expire headers; if resource changes, its URL will change } public void setState(RenderedLabel label) { backgroundColor = label.getBackgroundColor(); color = label.getColor(); font = label.getFont(); maxWidth = label.getMaxWidth(); text = label.getDefaultModelObjectAsString(); antiAliased = label.isAntiAliased(); invalidate(); } /** * Renders text into image. Will increase dimensions and return false if needed to accomodate * text. Neither dimension will be decreased, unless the text in blank. Blank text is rendered * as a 1 x 1 pixel square, with prior dimensions discarded. */ protected boolean render(final Graphics2D graphics) { final int width = getWidth(), height = getHeight(); // draw background if not null, otherwise leave transparent if (backgroundColor != null) { graphics.setColor(backgroundColor); graphics.fillRect(0, 0, width, height); } List<AttributedCharacterIterator> attributedLines = getAttributedLines(); // render as a 1x1 pixel if text is empty if (attributedLines == null) { if (width == 1 && height == 1) return true; setWidth(1); setHeight(1); return false; } graphics.setFont(font); FontMetrics fontMetrics = graphics.getFontMetrics(); List<TextLayout> layouts = new LinkedList<TextLayout>(); float neededWidth = 0f; for (AttributedCharacterIterator attributedIterator : attributedLines) { if (maxWidth == null) { TextLayout layout = new TextLayout(attributedIterator, graphics.getFontRenderContext()); if (layout.getBounds().getWidth() > neededWidth) neededWidth = (float) layout.getBounds().getWidth(); layouts.add(layout); } else { LineBreakMeasurer breaker = new LineBreakMeasurer(attributedIterator, graphics.getFontRenderContext()); TextLayout layout ; while (null != (layout = breaker.nextLayout(maxWidth))) { layouts.add(layout); if (layout.getBounds().getWidth() > neededWidth) neededWidth = Math.min(maxWidth, (float) layout.getBounds().getWidth()); } } } float lineHeight = graphics.getFontMetrics().getHeight(), neededHeight = layouts.size() * lineHeight; if (neededWidth > width || neededHeight > height) { setWidth(Math.max((int)Math.ceil(neededWidth), width)); setHeight(Math.max((int)Math.ceil(neededHeight), height)); return false; } // Turn on anti-aliasing graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, antiAliased ? RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF); graphics.setColor(color); float y = lineHeight - fontMetrics.getMaxDescent(); for (TextLayout layout : layouts) { layout.draw(graphics, 0f, y); y += lineHeight; } return true; } /** @return String to be rendered with attributes (global font only in this base class). */ protected List<AttributedCharacterIterator> getAttributedLines() { if (Strings.isEmpty(text)) return null; AttributedString attributedText = new AttributedString(text); attributedText.addAttribute(TextAttribute.FONT, font); return splitAtNewlines(attributedText, text); } static List<AttributedCharacterIterator> splitAtNewlines(AttributedString attr, String plain) { List<AttributedCharacterIterator> lines = new LinkedList<AttributedCharacterIterator>(); Pattern nl = Pattern.compile("\n"); Matcher m = nl.matcher(plain); int last = 0; while (m.find()) { lines.add(attr.getIterator(null, last, m.end())); last = m.end(); } lines.add(attr.getIterator(null, last, plain.length())); return lines; } /** * Normally, image rendering is deferred until the resource is requested, but * this method allows us to render the image when its markup is rendered. This way * the model will not need to be reattached when we serve the image, and we can * use the size information in the IMG tag. */ public void preload() { getImageData(); } } public Color getBackgroundColor() { return backgroundColor; } /** * Specify a background color to match the page. Specify null for a transparent background blended * with the alpha channel, causing IE6 to display a gray background. * @param backgroundColor color or null for transparent * @return this for chaining */ public RenderedLabel setBackgroundColor(Color backgroundColor) { this.backgroundColor = backgroundColor; return this; } public Color getColor() { return color; } /** @param color Color to print text */ public RenderedLabel setColor(Color color) { this.color = color; return this; } public Font getFont() { return font; } public RenderedLabel setFont(Font font) { this.font = font; return this; } public Integer getMaxWidth() { return maxWidth; } /** * Specify a maximum pixel width, causing longer renderings to wrap. * @param maxWidth maximum width in pixels * @return this, for chaining */ public RenderedLabel setMaxWidth(Integer maxWidth) { this.maxWidth = maxWidth; return this; } /** * Utility method for creating Font objects from resources. * @param fontRes Resource containing a TrueType font descriptor. * @return Plain, 16pt font derived from the resource. */ public static Font fontForResource(Resource fontRes) { try { InputStream is = fontRes.getResourceStream().getInputStream(); Font font = Font.createFont(Font.TRUETYPE_FONT, is); is.close(); return font.deriveFont(Font.PLAIN, 16); } catch (Throwable e) { throw new WicketRuntimeException("Error loading font resources", e); } } public boolean isAntiAliased() { return antiAliased; } public RenderedLabel setAntiAlias(boolean antiAlias) { this.antiAliased = antiAlias; return this; } }