001 package net.databinder.components;
002
003 import java.awt.Color;
004 import java.awt.Font;
005 import java.awt.FontMetrics;
006 import java.awt.Graphics2D;
007 import java.awt.RenderingHints;
008 import java.awt.font.LineBreakMeasurer;
009 import java.awt.font.TextAttribute;
010 import java.awt.font.TextLayout;
011 import java.awt.image.BufferedImage;
012 import java.io.InputStream;
013 import java.text.AttributedCharacterIterator;
014 import java.text.AttributedString;
015 import java.util.LinkedList;
016 import java.util.List;
017 import java.util.regex.Matcher;
018 import java.util.regex.Pattern;
019
020 import org.apache.wicket.Application;
021 import org.apache.wicket.Component;
022 import org.apache.wicket.Resource;
023 import org.apache.wicket.ResourceReference;
024 import org.apache.wicket.SharedResources;
025 import org.apache.wicket.WicketRuntimeException;
026 import org.apache.wicket.markup.ComponentTag;
027 import org.apache.wicket.markup.html.image.Image;
028 import org.apache.wicket.markup.html.image.resource.RenderedDynamicImageResource;
029 import org.apache.wicket.model.IComponentInheritedModel;
030 import org.apache.wicket.model.IModel;
031 import org.apache.wicket.model.IWrapModel;
032 import org.apache.wicket.protocol.http.WebResponse;
033 import org.apache.wicket.util.string.Strings;
034
035 /*
036 * Databinder: a simple bridge from Wicket to Hibernate
037 * Copyright (C) 2006 Nathan Hamblen nathan@technically.us
038
039 * This library is free software; you can redistribute it and/or
040 * modify it under the terms of the GNU Lesser General Public
041 * License as published by the Free Software Foundation; either
042 * version 2.1 of the License, or (at your option) any later version.
043 *
044 * This library is distributed in the hope that it will be useful,
045 * but WITHOUT ANY WARRANTY; without even the implied warranty of
046 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
047 * Lesser General Public License for more details.
048 *
049 * You should have received a copy of the GNU Lesser General Public
050 * License along with this library; if not, write to the Free Software
051 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
052 */
053
054 /**
055 * Renders its model text into a PNG, using any typeface available to the JVM. The size of
056 * the image is determined by the model text and the characteristics of font selected. The
057 * default font is 14pt sans, plain black on a white background. The background may be set
058 * to null for alpha transparency, which will appear gray in outdated browsers. The image's
059 * alt attribute will be set to the model text, and width and height attributes will be
060 * set appropriately.
061 * <p> If told to use a shared image resource, RenderedLabel will add its image
062 * to the application's shared resources and reference it from a permanent, unique,
063 * browser-chacheable URL. Note that if users might request a shared resource before a
064 * page containing it has rendered (after a context reload, for example) you should load
065 * that resource using loadSharedResources() as the application is starting up.
066 * <p> This class is inspired by, and draws code from, Wicket's DefaultButtonImageResource. </p>
067 * @author Nathan Hamblen
068 * @see SharedResources
069 */
070 public class RenderedLabel extends Image {
071 private static final long serialVersionUID = 1L;
072
073 private static Font defaultFont = new Font("sans", Font.PLAIN, 14);
074 private static Color defaultColor = Color.BLACK;
075 private static Color defaultBackgroundColor = Color.WHITE;
076
077 private Font font = defaultFont;
078 private Color color = defaultColor;
079 private Color backgroundColor = defaultBackgroundColor;
080 private Integer maxWidth;
081 private boolean antiAliased = true;
082
083 /** If true, resource is shared across application with a permanent URL. */
084 private boolean isShared = false;
085 /** Hash of the most recently displayed label attributes. -1 is initial value, 0 for blank labels. */
086 private int labelHash = -1;
087
088 private RenderedTextImageResource resource;
089
090 /**
091 * Constructor to be used if model is derived from a compound property model.
092 * @param id Wicket id
093 */
094 public RenderedLabel(String id) {
095 super(id);
096 init();
097 }
098
099 /**
100 * Constructor for compound property model and shared resource pool.
101 * @param id Wicket id
102 * @param shareResource true to add to shared resource pool
103 */
104 public RenderedLabel(String id, boolean shareResource) {
105 this(id);
106 this.isShared = shareResource;
107 init();
108 }
109
110 /**
111 * Constructor with explicit model.
112 * @param id Wicket id
113 * @param model model for
114 */
115 public RenderedLabel(String id, IModel model) {
116 super(id, model);
117 init();
118 }
119
120 /**
121 * Constructor with explicit model.
122 * @param id Wicket id
123 * @param model model for
124 * @param shareResource true to add to shared resource pool
125 */
126 public RenderedLabel(String id, IModel model, boolean shareResource) {
127 this(id, model);
128 this.isShared = shareResource;
129 init();
130 }
131
132 /** Perform generic initialization. */
133 protected void init() {
134 setEscapeModelStrings(false);
135 }
136
137 @Override
138 protected void onBeforeRender() {
139 super.onBeforeRender();
140 int curHash = getLabelHash();
141 if (isShared) {
142 if (labelHash != curHash) {
143 String hash = Integer.toHexString(curHash);
144 SharedResources shared = getApplication().getSharedResources();
145 try { resource = (RenderedTextImageResource) shared.get(RenderedLabel.class, hash, null, null, false); }
146 catch (ClassCastException e) {
147 // was placeholder for missing PackageResourceReference
148 shared.remove(shared.resourceKey(RenderedLabel.class, hash, null, null));
149 }
150 if (resource == null)
151 shared.add(RenderedLabel.class, hash, null, null,
152 resource = newRenderedTextImageResource(true));
153 setImageResourceReference(new ResourceReference(RenderedLabel.class, hash));
154 }
155 } else {
156 if (resource == null)
157 setImageResource(resource = newRenderedTextImageResource(false));
158 else if (labelHash != curHash)
159 resource.setState(this);
160 }
161 resource.setCacheable(isShared);
162 labelHash = getLabelHash();
163 }
164
165 /**
166 * @return false if set to false or if model string is empty.
167 */
168 @Override
169 public boolean isVisible() {
170 return super.isVisible() && getModelObject() != null;
171 }
172
173 /**
174 * Adds image-specific attributes including width, height, and alternate text. A hash is appended
175 * to the source URL to trigger a reload whenever drawing attributes change.
176 */
177 @Override
178 protected void onComponentTag(ComponentTag tag) {
179 super.onComponentTag(tag);
180
181 if (!isShared) {
182 String url = tag.getAttributes().getString("src");
183 url = url + ((url.indexOf("?") >= 0) ? "&" : "?");
184 url = url + "wicket:antiCache=" + Integer.toHexString(labelHash);
185
186 tag.put("src", url);
187 }
188 resource.preload();
189
190 tag.put("width", resource.getWidth() );
191 tag.put("height", resource.getHeight() );
192
193 tag.put("alt", getModelObjectAsString());
194 }
195
196 protected int getLabelHash() {
197 return getLabelHash(getModelObjectAsString(), font, color, backgroundColor, maxWidth);
198 }
199
200 protected static int getLabelHash(String text, Font font, Color color, Color backgroundColor, Integer maxWidth) {
201 if (text == null) return 0;
202
203 int hash= text.hashCode() ^ font.hashCode() ^ color.hashCode();
204 if (backgroundColor != null)
205 hash ^= backgroundColor.hashCode();
206 if (maxWidth != null)
207 hash ^= maxWidth.hashCode();
208 return hash;
209 }
210
211 /** Restores compound model resolution that is disabled in the Image superclass. */
212 @Override
213 protected IModel initModel() {
214 // Search parents for CompoundPropertyModel
215 for (Component current = getParent(); current != null; current = current.getParent())
216 {
217 // Get model
218 // Dont call the getModel() that could initialize many inbetween completely useless models.
219 //IModel model = current.getModel();
220 IModel model = current.getModel();
221
222 if (model instanceof IWrapModel)
223 {
224 model = ((IWrapModel)model).getWrappedModel();
225 }
226
227 if (model instanceof IComponentInheritedModel)
228 {
229 // we turn off versioning as we share the model with another
230 // component that is the owner of the model (that component
231 // has to decide whether to version or not
232 setVersioned(false);
233
234 // return the shared inherited
235 model = ((IComponentInheritedModel)model).wrapOnInheritance(this);
236 return model;
237 }
238 }
239
240 // No model for this component!
241 return null;
242 }
243
244 /**
245 * Load shared resource into pool so it will be available even before a page using the
246 * rendered label is first rendered. May be needed if a page is cachable and the context
247 * is restarted, for example.
248 * @param text
249 * @param font uses default if null
250 * @param color uses default if null
251 * @param backgroundColor uses default if null
252 * @param maxWidth
253 */
254 public static void loadSharedResources(String text, Font font, Color color, Color backgroundColor, Integer maxWidth) {
255 loadSharedResources(new RenderedTextImageResource(), text, font, color, backgroundColor, maxWidth);
256 }
257
258 /**
259 * Utility method to load a specific instance of a the rendering shared resource.
260 */
261 protected static void loadSharedResources(RenderedTextImageResource res, String text, Font font, Color color, Color backgroundColor, Integer maxWidth) {
262 res.setCacheable(true);
263 res.backgroundColor = backgroundColor == null ? defaultBackgroundColor : backgroundColor;
264 res.color = color == null ? defaultColor : color;
265 res.font = font == null ? defaultFont : font;
266 res.maxWidth = maxWidth;
267 res.text = text;
268
269 String hash = Integer.toHexString(getLabelHash(text, font, color, backgroundColor, maxWidth));
270 SharedResources shared = Application.get().getSharedResources();
271
272 shared.add(RenderedLabel.class, hash, null, null, res);
273 }
274
275 /**
276 * Create a new image resource to render this label. Override in a subclass to use a different
277 * renderer.
278 * @param isShared is a shared, cacheable resource
279 * @return new instance of RenderedTextImageResource or subclass
280 */
281 protected RenderedTextImageResource newRenderedTextImageResource(boolean isShared) {
282 RenderedTextImageResource res = new RenderedTextImageResource();
283 res.setCacheable(isShared);
284 res.setState(this);
285 return res;
286 }
287
288 /**
289 * Inner class that renders the model text into an image resource.
290 */
291 public static class RenderedTextImageResource extends RenderedDynamicImageResource
292 {
293 protected Color backgroundColor;
294 protected Color color;
295 protected Font font;
296 protected Integer maxWidth;
297 protected String text;
298 protected boolean antiAliased;
299
300 protected RenderedTextImageResource() {
301 super(1, 1,"png"); // tiny default that will resize to fit text
302 setType(BufferedImage.TYPE_INT_ARGB); // allow alpha transparency
303 }
304
305 @Override
306 protected void setHeaders(WebResponse response) {
307 // don't set expire headers; if resource changes, its URL will change
308 }
309
310 public void setState(RenderedLabel label) {
311 backgroundColor = label.getBackgroundColor();
312 color = label.getColor();
313 font = label.getFont();
314 maxWidth = label.getMaxWidth();
315 text = label.getModelObjectAsString();
316 antiAliased = label.isAntiAliased();
317 invalidate();
318 }
319
320 /**
321 * Renders text into image. Will increase dimensions and return false if needed to accomodate
322 * text. Neither dimension will be decreased, unless the text in blank. Blank text is rendered
323 * as a 1 x 1 pixel square, with prior dimensions discarded.
324 */
325 protected boolean render(final Graphics2D graphics)
326 {
327 final int width = getWidth(), height = getHeight();
328
329 // draw background if not null, otherwise leave transparent
330 if (backgroundColor != null) {
331 graphics.setColor(backgroundColor);
332 graphics.fillRect(0, 0, width, height);
333 }
334
335 List<AttributedCharacterIterator> attributedLines = getAttributedLines();
336
337 // render as a 1x1 pixel if text is empty
338 if (attributedLines == null) {
339 if (width == 1 && height == 1)
340 return true;
341 setWidth(1);
342 setHeight(1);
343 return false;
344 }
345
346 graphics.setFont(font);
347 FontMetrics fontMetrics = graphics.getFontMetrics();
348
349 List<TextLayout> layouts = new LinkedList<TextLayout>();
350
351 float neededWidth = 0f;
352 for (AttributedCharacterIterator attributedIterator : attributedLines) {
353 if (maxWidth == null) {
354 TextLayout layout = new TextLayout(attributedIterator, graphics.getFontRenderContext());
355 if (layout.getBounds().getWidth() > neededWidth)
356 neededWidth = (float) layout.getBounds().getWidth();
357 layouts.add(layout);
358 }
359 else {
360 LineBreakMeasurer breaker = new LineBreakMeasurer(attributedIterator, graphics.getFontRenderContext());
361 TextLayout layout ;
362 while (null != (layout = breaker.nextLayout(maxWidth))) {
363 layouts.add(layout);
364 if (layout.getBounds().getWidth() > neededWidth)
365 neededWidth = Math.min(maxWidth, (float) layout.getBounds().getWidth());
366 }
367 }
368 }
369
370 float lineHeight = graphics.getFontMetrics().getHeight(),
371 neededHeight = layouts.size() * lineHeight;
372
373 if (neededWidth > width || neededHeight > height) {
374 setWidth(Math.max((int)Math.ceil(neededWidth), width));
375 setHeight(Math.max((int)Math.ceil(neededHeight), height));
376 return false;
377 }
378 // Turn on anti-aliasing
379 graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
380 antiAliased ? RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF);
381 graphics.setColor(color);
382
383 float y = lineHeight - fontMetrics.getMaxDescent();
384 for (TextLayout layout : layouts) {
385 layout.draw(graphics, 0f, y);
386 y += lineHeight;
387 }
388
389 return true;
390 }
391
392 /** @return String to be rendered with attributes (global font only in this base class). */
393 protected List<AttributedCharacterIterator> getAttributedLines() {
394 if (Strings.isEmpty(text))
395 return null;
396 AttributedString attributedText = new AttributedString(text);
397 attributedText.addAttribute(TextAttribute.FONT, font);
398 return splitAtNewlines(attributedText, text);
399 }
400
401 static List<AttributedCharacterIterator> splitAtNewlines(AttributedString attr, String plain) {
402 List<AttributedCharacterIterator> lines = new LinkedList<AttributedCharacterIterator>();
403 Pattern nl = Pattern.compile("\n");
404 Matcher m = nl.matcher(plain);
405 int last = 0;
406 while (m.find()) {
407 lines.add(attr.getIterator(null, last, m.end()));
408 last = m.end();
409 }
410 lines.add(attr.getIterator(null, last, plain.length()));
411 return lines;
412
413 }
414
415 /**
416 * Normally, image rendering is deferred until the resource is requested, but
417 * this method allows us to render the image when its markup is rendered. This way
418 * the model will not need to be reattached when we serve the image, and we can
419 * use the size information in the IMG tag.
420 */
421 public void preload() {
422 getImageData();
423 }
424 }
425
426 public Color getBackgroundColor() {
427 return backgroundColor;
428 }
429
430 /**
431 * Specify a background color to match the page. Specify null for a transparent background blended
432 * with the alpha channel, causing IE6 to display a gray background.
433 * @param backgroundColor color or null for transparent
434 * @return this for chaining
435 */
436 public RenderedLabel setBackgroundColor(Color backgroundColor) {
437 this.backgroundColor = backgroundColor;
438 return this;
439 }
440
441 public Color getColor() {
442 return color;
443 }
444
445 /** @param color Color to print text */
446 public RenderedLabel setColor(Color color) {
447 this.color = color;
448 return this;
449 }
450
451 public Font getFont() {
452 return font;
453 }
454
455 public RenderedLabel setFont(Font font) {
456 this.font = font;
457 return this;
458 }
459
460
461 public Integer getMaxWidth() {
462 return maxWidth;
463 }
464
465 /**
466 * Specify a maximum pixel width, causing longer renderings to wrap.
467 * @param maxWidth maximum width in pixels
468 * @return this, for chaining
469 */
470 public RenderedLabel setMaxWidth(Integer maxWidth) {
471 this.maxWidth = maxWidth;
472 return this;
473 }
474
475 /**
476 * Utility method for creating Font objects from resources.
477 * @param fontRes Resource containing a TrueType font descriptor.
478 * @return Plain, 16pt font derived from the resource.
479 */
480 public static Font fontForResource(Resource fontRes) {
481 try {
482 InputStream is = fontRes.getResourceStream().getInputStream();
483 Font font = Font.createFont(Font.TRUETYPE_FONT, is);
484 is.close();
485 return font.deriveFont(Font.PLAIN, 16);
486 } catch (Throwable e) {
487 throw new WicketRuntimeException("Error loading font resources", e);
488 }
489 }
490
491 public boolean isAntiAliased() {
492 return antiAliased;
493 }
494
495 public RenderedLabel setAntiAlias(boolean antiAlias) {
496 this.antiAliased = antiAlias;
497 return this;
498 }
499 }