001package jmri.util.swing;
002
003import java.awt.Color;
004import java.awt.Container;
005import java.awt.Dimension;
006import java.awt.Graphics;
007import java.awt.Graphics2D;
008import java.awt.RenderingHints;
009import java.awt.Window;
010import java.awt.event.ComponentEvent;
011import java.awt.event.ComponentListener;
012import java.awt.image.BufferedImage;
013import java.io.File;
014import java.io.IOException;
015
016import javax.swing.JLabel;
017import javax.swing.JPanel;
018
019import org.apache.batik.anim.dom.SAXSVGDocumentFactory;
020import org.apache.batik.transcoder.*;
021import org.apache.batik.transcoder.image.ImageTranscoder;
022import org.apache.batik.util.XMLResourceDescriptor;
023
024import net.coobird.thumbnailator.ThumbnailParameter;
025import net.coobird.thumbnailator.builders.ThumbnailParameterBuilder;
026import net.coobird.thumbnailator.filters.ImageFilter;
027import net.coobird.thumbnailator.tasks.io.FileImageSource;
028
029import org.slf4j.Logger;
030import org.slf4j.LoggerFactory;
031import org.w3c.dom.Document;
032
033/**
034 * A class extending JPanels to have a image display in a panel, supports<ul>
035 * <li>drag'n drop of image file</li>
036 * <li>can resize container</li>
037 * <li>can scale content to size</li>
038 * <li>respect aspect ratio by default (when resizing content)</li>
039 * </ul>
040 * (overrides paintComponent for performances)
041 *
042 * @author Lionel Jeanson - Copyright 2009
043 */
044public class ResizableImagePanel extends JPanel implements ComponentListener {
045
046    public static final String IMAGE_PATH = "imagePath";
047
048    private String _imagePath;
049    protected JLabel bgImg = null;
050    private BufferedImage image = null; // a place to store the original image if it is a pixel image
051    private Document svgImage = null;   // a place to store the original document if is a vector image (svg file)
052    private BufferedImage scaledImage = null;
053    private boolean _resizeContainer = false;
054    private boolean _respectAspectRatio = true;
055    static private Color backgroundColor = Color.BLACK;
056    boolean toResize = false;
057    final static Dimension SMALL_DIM = new Dimension(10, 10);
058
059    /**
060     * Default constructor.
061     */
062    public ResizableImagePanel() {
063        super();
064        super.setBackground(backgroundColor);
065        setVisible(false);
066    }
067
068    /**
069     * Constructor with initial image file path as parameter. Component will be
070     * (preferred) sized to image sized
071     *
072     *
073     * @param imagePath Path to image to display
074     */
075    public ResizableImagePanel(String imagePath) {
076        super();
077        super.setBackground(backgroundColor);
078        setImagePath(imagePath);
079    }
080
081    /**
082     * Constructor for ResizableImagePanel with forced initial size
083     *
084     * @param imagePath Path to image to display
085     * @param w         Panel width
086     * @param h         Panel height
087     */
088    public ResizableImagePanel(String imagePath, int w, int h) {
089        super();
090        setPreferredSize(new Dimension(w, h));
091        setSize(w, h);
092        super.setBackground(backgroundColor);
093        setImagePath(imagePath);
094    }
095
096    @Override
097    public void setBackground(Color bckCol) {
098        super.setBackground(bckCol);
099        setScaledImage();
100    }
101
102    /**
103     * Allows this ResizableImagePanel to force resize of its container
104     *
105     * @param b true if this instance can resize its container; false otherwise
106     */
107    public void setResizingContainer(boolean b) {
108        _resizeContainer = b;
109    }
110
111    /**
112     * Can this DnDImagePanel resize its container?
113     *
114     * @return true if container can be resized
115     */
116    public boolean isResizingContainer() {
117        return _resizeContainer;
118    }
119
120    /**
121     * Is this DnDImagePanel respecting aspect ratio when resizing content?
122     *
123     * @return true is aspect ratio is maintained
124     */
125    public boolean isRespectingAspectRatio() {
126        return _respectAspectRatio;
127    }
128
129    /**
130     * Allow this ResizableImagePanel to respect aspect ratio when resizing
131     * content.
132     *
133     * @param b true if aspect ratio should be respected; false otherwise
134     */
135    public void setRespectAspectRatio(boolean b) {
136        _respectAspectRatio = b;
137    }
138
139    /**
140     * Return current image file path
141     *
142     * @return The image path or "/" if no image is specified
143     */
144    public String getImagePath() {
145        return _imagePath;
146    }
147
148    /**
149     * Read pixel image and handle exif information if it exists in the file.
150     *
151     * @param file the image file
152     * @return the image
153     * @throws IOException in case of an I/O error
154     */
155    private BufferedImage readImage(File file) throws IOException {
156        ThumbnailParameterBuilder builder = new ThumbnailParameterBuilder();
157        builder.scale(1.0);
158        ThumbnailParameter param = builder.build();
159
160        FileImageSource fileImageSource = new FileImageSource(file);
161        fileImageSource.setThumbnailParameter(param);
162
163        BufferedImage img = fileImageSource.read();
164
165        // Perform the image filters
166        for (ImageFilter filter : param.getImageFilters()) {
167            img = filter.apply(img);
168        }
169
170        return img;
171    }
172
173  /**
174   * Read vector image
175   * Use the SAXSVGDocumentFactory to parse the given URI into a DOM.
176   *
177   * @param uri The path to the SVG file to read.
178   * @return A Document instance that represents the SVG file.
179   * @throws IOException The file could not be read.
180   */
181    private Document createSVGDocument( String uri ) throws IOException {
182      String parser = XMLResourceDescriptor.getXMLParserClassName();
183      SAXSVGDocumentFactory factory = new SAXSVGDocumentFactory( parser );
184      return factory.createDocument( uri );
185    }
186
187    /**
188     * Set image file path, display will be updated if passed value is null,
189     * blank image
190     *
191     * @param s path to image file
192     */
193    public void setImagePath(String s) {
194        String old = _imagePath;
195        if (s != null && !s.equals("")) {
196            _imagePath = s;
197        } else {
198            _imagePath = null;
199            image = null;
200            scaledImage = null;
201            svgImage = null;
202        }
203        log.debug("Image path is now : {}", _imagePath);
204        if (_imagePath != null) {
205            try {
206                File imf = new File(_imagePath);
207                if ( _imagePath.toUpperCase().endsWith(".SVG") ) {
208                    svgImage = createSVGDocument(imf.toURI().toString());
209                    image = null;
210                } else {
211                    svgImage = null;
212                    image = readImage(imf);
213                }
214            } catch (IOException ex) {
215                log.error("{} is not a valid image file.", _imagePath);
216                image = null;
217                svgImage = null;
218                scaledImage = null;
219            }
220        }
221        if (isResizingContainer()) {
222            resizeContainer();
223        }
224        setScaledImage();
225        setVisible(true);
226        repaint();
227        if (getParent() != null) {
228            getParent().repaint();
229        }
230        this.firePropertyChange(IMAGE_PATH, old, _imagePath);
231    }
232
233    //
234    // componentListener methods, for auto resizing and scaling
235    //
236    @Override
237    public void componentResized(ComponentEvent e) {
238        if (!(isResizingContainer())) {
239            if (e.getComponent().isVisible()) {
240                setSize(e.getComponent().getSize());
241                setPreferredSize(e.getComponent().getSize());
242                setScaledImage();
243                toResize = false;
244            } else {
245                toResize = true;
246            }
247        }
248        repaint();
249        if (getParent() != null) {
250            getParent().repaint();
251        }
252    }
253
254    @Override
255    public void componentMoved(ComponentEvent e) {
256    }
257
258    @Override
259    public void componentShown(ComponentEvent e) {
260        if (isResizingContainer()) {
261            resizeContainer();
262        } else {
263            if ((toResize) || (scaledImage == null)) {
264                setSize(e.getComponent().getSize());
265                setPreferredSize(e.getComponent().getSize());
266                setScaledImage();
267                toResize = false;
268            }
269        }
270    }
271
272    @Override
273    public void componentHidden(ComponentEvent e) {
274        log.debug("Component hidden");
275        if (isResizingContainer()) {
276            resizeContainer(SMALL_DIM);
277        }
278    }
279
280    private void resizeContainer(Dimension d) {
281        log.debug("Resizing container");
282        Container p1 = getParent();
283        if ((p1 != null) && (image != null)) {
284            setPreferredSize(d);
285            setSize(d);
286            p1.setPreferredSize(d);
287            p1.setSize(d);
288            Container c = getTopLevelAncestor();
289            if (c != null && c instanceof Window) {
290                ((Window) c).pack();
291            }
292        }
293    }
294
295    private void resizeContainer() {
296        if (scaledImage != null) {
297            resizeContainer(new Dimension(scaledImage.getWidth(null), scaledImage.getHeight(null)));
298        } else if (image != null) {
299            resizeContainer(new Dimension(image.getWidth(null), image.getHeight(null)));
300        }
301    }
302
303    //override paintComponent
304    @Override
305    public void paintComponent(Graphics g) {
306        super.paintComponent(g);
307        if (scaledImage != null) {
308            g.drawImage(scaledImage, 0, 0, this);
309        } else {
310            g.clearRect(0, 0, (int) getSize().getWidth(), (int) getSize().getHeight());
311        }
312    }
313
314    /**
315     * Get current scaled Image
316     *
317     * @return the image resized as specified
318     */
319    public BufferedImage getScaledImage() {
320        return scaledImage;
321    }
322
323    private void setScaledImage() {
324        if (image != null) {
325            if ((getSize().getWidth() != 0) && (getSize().getHeight() != 0)
326                    && ((getSize().getWidth() != image.getWidth(null)) || (getSize().getHeight() != image.getHeight(null)))) {
327                int newW = (int) getSize().getWidth();
328                int newH = (int) getSize().getHeight();
329                int new0x = 0;
330                int new0y = 0;
331                log.debug("Actually resizing image {} to {}x{}", this.getImagePath(), newW, newH);
332                scaledImage = new BufferedImage(newW, newH, image.getType() == 0 ? BufferedImage.TYPE_INT_ARGB : image.getType());
333                Graphics2D g = scaledImage.createGraphics();
334                g.setBackground(getBackground());
335                g.clearRect(0, 0, newW, newH);
336                g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
337                if (_respectAspectRatio) {
338                    if ((getSize().getWidth() / getSize().getHeight()) > ((double) image.getWidth(null) / (double) image.getHeight(null))) { // Fill on height
339                        newW = image.getWidth(null) * newH / image.getHeight(null);
340                        new0x = (int) (getSize().getWidth() - newW) / 2;
341                    } else { // Fill on width
342                        newH = image.getHeight(null) * newW / image.getWidth(null);
343                        new0y = (int) (getSize().getHeight() - newH) / 2;
344                    }
345                }
346                g.drawImage(image, new0x, new0y, new0x + newW, new0y + newH, 0, 0, image.getWidth(), image.getHeight(), this);
347                g.dispose();
348            } else {
349                scaledImage = image;
350            }
351        } else if (svgImage != null) {
352            MyTranscoder transcoder = new MyTranscoder();
353            TranscodingHints hints = new TranscodingHints();
354            hints.put(ImageTranscoder.KEY_WIDTH, (float) getSize().getWidth());
355            hints.put(ImageTranscoder.KEY_HEIGHT, (float) getSize().getHeight());
356            transcoder.setTranscodingHints(hints);
357            try {
358                transcoder.transcode(new TranscoderInput(svgImage), null);
359            } catch (TranscoderException ex) {
360                log.debug("Exception while transposing sbg : {}", ex.getMessage());
361            }
362            scaledImage = transcoder.getImage();
363        }
364    }
365
366    // to handle svg transformation to displayable images
367    private static class MyTranscoder extends ImageTranscoder {
368        private BufferedImage image = null;
369        @Override
370        public BufferedImage createImage(int w, int h) {
371            image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
372            return image;
373        }
374        public BufferedImage getImage() {
375            return image;
376        }
377        @Override
378        public void writeImage(BufferedImage bi, TranscoderOutput to) throws TranscoderException {
379            //not required here, do nothing
380        }
381    }
382
383    private final static Logger log = LoggerFactory.getLogger(ResizableImagePanel.class);
384}