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}