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    }