001package jmri.jmrit.catalog; 002 003import java.awt.Component; 004import java.awt.Graphics2D; 005import java.awt.Image; 006import java.awt.MediaTracker; 007import java.awt.RenderingHints; 008import java.awt.geom.AffineTransform; 009import java.awt.image.BufferedImage; 010import java.awt.image.ColorModel; 011import java.awt.image.MemoryImageSource; 012import java.awt.image.PixelGrabber; 013import java.awt.image.RenderedImage; 014import java.io.ByteArrayOutputStream; 015import java.io.IOException; 016import java.io.InputStream; 017import java.net.URL; 018import java.util.Iterator; 019import javax.annotation.CheckForNull; 020import javax.imageio.IIOImage; 021import javax.imageio.ImageIO; 022import javax.imageio.ImageReader; 023import javax.imageio.ImageTypeSpecifier; 024import javax.imageio.ImageWriter; 025import javax.imageio.metadata.IIOMetadata; 026import javax.imageio.metadata.IIOMetadataNode; 027import javax.imageio.spi.ImageReaderSpi; 028import javax.imageio.stream.ImageInputStream; 029import javax.imageio.stream.ImageOutputStream; 030import javax.swing.ImageIcon; 031import jmri.jmrit.display.PositionableLabel; 032import jmri.util.FileUtil; 033import jmri.util.MathUtil; 034import org.slf4j.Logger; 035import org.slf4j.LoggerFactory; 036 037/** 038 * Extend an ImageIcon to remember the name from which it was created and 039 * provide rotation and scaling services. 040 * <p> 041 * We store both a "URL" for finding the file this was made from (so we can load 042 * this later), plus a shorter (localized) "name" for display in GUI. 043 * <p> 044 * These can be persisted by storing their name and rotation. 045 * 046 * @see jmri.jmrit.display.configurexml.PositionableLabelXml 047 * @author Bob Jacobsen Copyright 2002, 2008 048 * @author Pete Cressman Copyright (c) 2009, 2010 049 * 050 * Modified by Joe Comuzzi and Larry Allen to rotate animated GIFs 051 */ 052public class NamedIcon extends ImageIcon { 053 054 /** 055 * Create a NamedIcon that is a complete copy of an existing NamedIcon 056 * 057 * @param pOld Object to copy i.e. copy of the original icon, but NOT a 058 * complete copy of pOld (no transformations done) 059 */ 060 public NamedIcon(NamedIcon pOld) { 061 this(pOld.mURL, pOld.mName, pOld.mGifInfo); 062 } 063 064 /** 065 * Create a NamedIcon that is really a complete copy of an existing 066 * NamedIcon 067 * 068 * @param pOld Object to copy 069 * @param comp the container the new icon is embedded in 070 */ 071 public NamedIcon(NamedIcon pOld, Component comp) { 072 this(pOld.mURL, pOld.mName, pOld.mGifInfo); 073 setLoad(pOld._degrees, pOld._scale, comp); 074 setRotation(pOld.mRotation, comp); 075 } 076 077 /** 078 * Create a named icon that includes an image to be loaded from a URL. 079 * <p> 080 * The default access form is "file:", so a bare pathname to an icon file 081 * will also work for the URL argument. 082 * 083 * @param pUrl URL of image file to load 084 * @param pName Human-readable name for the icon 085 */ 086 public NamedIcon(String pUrl, String pName) { 087 this(pUrl, pName, null); 088 089 // See if this is a GIF file and if it is, see if it's animated. If it is, 090 // breakout the metadata and individual frames. Also collect the max sizes in case the 091 // frames aren't all the same. 092 try { 093 GIFMetadataImages gifState = new GIFMetadataImages(); 094 Iterator<ImageReader> rIter = ImageIO.getImageReadersByFormatName("gif"); 095 ImageReader gifReader = rIter.next(); 096 097 InputStream is = FileUtil.findInputStream(pUrl); 098 // findInputStream can return null, which has to be handled. 099 if (is == null) { 100 log.warn("NamedIcon can't scan {} for animated status", pUrl); 101 return; 102 } 103 104 ImageInputStream iis = ImageIO.createImageInputStream(is); 105 gifReader.setInput(iis, false); 106 107 ImageReaderSpi spiProv = gifReader.getOriginatingProvider(); 108 if (spiProv != null && spiProv.canDecodeInput(iis)) { 109 110 int numFrames = gifReader.getNumImages(true); 111 112 // No need to keep the GIF info if it's not animated, the old code works 113 // in that case. 114 if (numFrames > 1) { 115 gifState.mStreamMd = gifReader.getStreamMetadata(); 116 gifState.mFrames = new IIOImage[numFrames]; 117 gifState.mWidth = 0; 118 gifState.mHeight = 0; 119 for (int i = 0; i < numFrames; i++) { 120 gifState.mFrames[i] = gifReader.readAll(i, null); 121 RenderedImage image = gifState.mFrames[i].getRenderedImage(); 122 gifState.mHeight = Math.max(gifState.mHeight, image.getHeight()); 123 gifState.mWidth = Math.max(gifState.mWidth, image.getWidth()); 124 } 125 126 mGifInfo = gifState; 127 } 128 } 129 } catch (IOException ioe) { 130 // If we get an exception here it's probably because the image isn't really 131 // a GIF. Unfortunately, there's no guarantee that it is a GIF just because 132 // canDecodeInput returns true. 133 log.debug("Exception extracting GIF Info: ", ioe); 134 mGifInfo = null; 135 } 136 } 137 138 /** 139 * Create a named icon that includes an image to be loaded from a URL. 140 * <p> 141 * The default access form is "file:", so a bare pathname to an icon file 142 * will also work for the URL argument. 143 * 144 * @param pUrl URL of image file to load 145 * @param pName Human-readable name for the icon 146 * @param pGifState Breakdown of GIF Image metadata and frames 147 */ 148 public NamedIcon(String pUrl, String pName, GIFMetadataImages pGifState) { 149 super(substituteDefaultUrl(pUrl)); 150 URL u = FileUtil.findURL(pUrl); 151 if (u == null) { 152 log.warn("Could not load image from {} (file does not exist)", pUrl); 153 } 154 mDefaultImage = getImage(); 155 if (mDefaultImage == null) { 156 log.warn("Could not load image from {} (image is null)", pUrl); 157 } 158 mName = pName; 159 mGifInfo = pGifState; 160 mURL = FileUtil.getPortableFilename(pUrl); 161 mRotation = 0; 162 } 163 164 static private final String DEFAULTURL = "resources/icons/misc/X-red.gif"; 165 static private URL substituteDefaultUrl(String pUrl) { 166 URL url = FileUtil.findURL(pUrl, FileUtil.Location.ALL); 167 if (url == null) { 168 url = FileUtil.findURL(DEFAULTURL); 169 log.error("Did not find \"{}\" for NamedIcon, substitute {}", pUrl, url); 170 } 171 return url; 172 } 173 174 /** 175 * Create a named icon that includes an image to be loaded from a URL. 176 * 177 * @param pUrl String-form URL of image file to load 178 * @param pName Human-readable name for the icon 179 */ 180 public NamedIcon(URL pUrl, String pName) { 181 this(pUrl.toString(), pName); 182 } 183 184 185 /** 186 * Create a named icon from an Image. N.B. NamedIcon's create 187 * using this constructor can NOT be animated GIFs 188 * @param im Image to use 189 */ 190 public NamedIcon(Image im) { 191 super(im); 192 mDefaultImage = getImage(); 193 } 194 195 /** 196 * Find the NamedIcon corresponding to a file path. Understands the 197 * <a href="http://jmri.org/help/en/html/doc/Technical/FileNames.shtml">standard 198 * portable filename prefixes</a>. 199 * 200 * @param path The path to the file, either absolute or portable 201 * @return the desired icon with this same name as its path 202 */ 203 static public NamedIcon getIconByName(String path) { 204 if (path == null || path.isEmpty()) { 205 return null; 206 } 207 if (FileUtil.findURL(path) == null) { 208 return null; 209 } 210 return new NamedIcon(path, path); 211 } 212 213 /** 214 * Return the human-readable name of this icon. 215 * 216 * @return the name or null if not set 217 */ 218 @CheckForNull 219 public String getName() { 220 return mName; 221 } 222 223 /** 224 * Set the human-readable name for this icon. 225 * 226 * @param name the new name, can be null 227 */ 228 public void setName(@CheckForNull String name) { 229 mName = name; 230 } 231 232 /** 233 * Get the URL of this icon. 234 * 235 * @return the path to this icon in JMRI portable format or null if not set 236 */ 237 @CheckForNull 238 public String getURL() { 239 return mURL; 240 } 241 242 /** 243 * Set URL of original icon image. Setting this after initial construction 244 * does not change the icon. 245 * 246 * @param url the URL associated with this icon 247 */ 248 public void setURL(@CheckForNull String url) { 249 mURL = url; 250 } 251 252 /** 253 * Get the number of 90-degree rotations needed to properly display this 254 * icon. 255 * 256 * @return 0 (no rotation), 1 (rotated 90 degrees), 2 (180 degrees), or 3 257 * (270 degrees) 258 */ 259 public int getRotation() { 260 return mRotation; 261 } 262 263 /** 264 * Set the number of 90-degree rotations needed to properly display this 265 * icon. 266 * 267 * @param pRotation 0 (no rotation), 1 (rotated 90 degrees), 2 (180 268 * degrees), or 3 (270 degrees) 269 * @param comp the component containing this icon 270 */ 271 public void setRotation(int pRotation, Component comp) { 272 // don't transform a blinking icon, it will no longer blink! 273 if (pRotation == 0) { 274 return; 275 } 276 if (pRotation > 3) { 277 pRotation = 0; 278 } 279 if (pRotation < 0) { 280 pRotation = 3; 281 } 282 mRotation = pRotation; 283 setImage(createRotatedImage(mDefaultImage, comp, mRotation)); 284 _degrees = 0; 285 if (Math.abs(_scale - 1.0) > .00001) { 286 int w = (int) Math.ceil(_scale * getIconWidth()); 287 int h = (int) Math.ceil(_scale * getIconHeight()); 288 transformImage(w, h, _transformS, comp); 289 } 290 } 291 292 private String mName = null; 293 private String mURL = null; 294 private GIFMetadataImages mGifInfo = null; 295 private final Image mDefaultImage; 296 297 private static class GIFMetadataImages { 298 private int mHeight; 299 private int mWidth; 300 private IIOImage mFrames[] = null; 301 private IIOMetadata mStreamMd; 302 } 303 304 /* 305 public Image getOriginalImage() { 306 return mDefaultImage; 307 }*/ 308 309 /** 310 * Valid values are 311 * <ul> 312 * <li>0 - no rotation 313 * <li>1 - 90 degrees counter-clockwise 314 * <li>2 - 180 degrees counter-clockwise 315 * <li>3 - 270 degrees counter-clockwise 316 * </ul> 317 */ 318 int mRotation; 319 320 /** 321 * The following was based on a text-rotating applet from David Risner, 322 * available at http://www.risner.org/java/rotate_text.html 323 * Page unavailable as at April 2019 324 * 325 * @param pImage Image to transform 326 * @param pComponent Component containing the image, needed to obtain a 327 * MediaTracker to process the image consistently with 328 * display 329 * @param pRotation 0-3 number of 90-degree rotations needed 330 * @return new Image object containing the rotated input image 331 */ 332 public Image createRotatedImage(Image pImage, Component pComponent, int pRotation) { 333 log.debug("createRotatedImage: pRotation= {}, mRotation= {}", pRotation, mRotation); 334 if (pRotation == 0) { 335 return pImage; 336 } 337 338 MediaTracker mt = new MediaTracker(pComponent); 339 mt.addImage(pImage, 0); 340 try { 341 mt.waitForAll(); 342 } catch (InterruptedException ie) { 343 Thread.currentThread().interrupt(); // retain if needed later 344 } 345 346 int w = pImage.getWidth(null); 347 int h = pImage.getHeight(null); 348 349 int[] pixels = new int[w * h]; 350 PixelGrabber pg = new PixelGrabber(pImage, 0, 0, w, h, pixels, 0, w); 351 try { 352 pg.grabPixels(); 353 } catch (InterruptedException ie) { 354 } 355 int[] newPixels = new int[w * h]; 356 357 // transform the pixels 358 MemoryImageSource imageSource = null; 359 switch (pRotation) { 360 case 1: // 90 degrees 361 for (int y = 0; y < h; ++y) { 362 for (int x = 0; x < w; ++x) { 363 newPixels[x * h + y] = pixels[y * w + (w - 1 - x)]; 364 } 365 } 366 imageSource = new MemoryImageSource(h, w, 367 ColorModel.getRGBdefault(), newPixels, 0, h); 368 break; 369 case 2: // 180 degrees 370 for (int y = 0; y < h; ++y) { 371 for (int x = 0; x < w; ++x) { 372 newPixels[x * h + y] = pixels[(w - 1 - x) * h + (h - 1 - y)]; 373 } 374 } 375 imageSource = new MemoryImageSource(w, h, 376 ColorModel.getRGBdefault(), newPixels, 0, w); 377 break; 378 case 3: // 270 degrees 379 for (int y = 0; y < h; ++y) { 380 for (int x = 0; x < w; ++x) { 381 newPixels[x * h + y] = pixels[(h - 1 - y) * w + x]; 382 } 383 } 384 imageSource = new MemoryImageSource(h, w, 385 ColorModel.getRGBdefault(), newPixels, 0, h); 386 break; 387 default: 388 log.warn("Unhandled rotation code: {}", pRotation); 389 break; 390 } 391 392 Image myImage = pComponent.createImage(imageSource); 393 mt.addImage(myImage, 1); 394 try { 395 mt.waitForAll(); 396 } catch (InterruptedException ie) { 397 } 398 return myImage; 399 } 400 private int _degrees = 0; 401 private double _scale = 1.0; 402 private AffineTransform _transformS = new AffineTransform(); // scaling 403 private AffineTransform _transformF = new AffineTransform(); // Fliped or Mirrored 404 405 public int getDegrees() { 406 return _degrees; 407 } 408 409 public double getScale() { 410 return _scale; 411 } 412 413 public void setLoad(int d, double s, Component comp) { 414 if (d != 0 || s != 1.0) { 415 setImage(createRotatedImage(mDefaultImage, comp, 0)); 416 //mRotation = 3; 417 } 418 _scale = s; 419 _transformS = AffineTransform.getScaleInstance(s, s); 420 rotate(d, comp); 421 422 } 423 424 public void transformImage(int w, int h, AffineTransform t, Component comp) { 425 if (w <= 0 || h <= 0) { 426 if (comp instanceof jmri.jmrit.display.Positionable) { 427 log.debug("transformImage bad coords {}", 428 ((jmri.jmrit.display.Positionable) comp).getNameString()); 429 } 430 return; 431 } 432 if (mGifInfo == null) { 433 setImage(transformFrame(getImage(), w, h, t, comp)); 434 } else { 435 try { 436 String streamFormat = mGifInfo.mStreamMd.getNativeMetadataFormatName(); 437 IIOMetadataNode streamTree = (IIOMetadataNode) mGifInfo.mStreamMd.getAsTree(streamFormat); 438 IIOMetadataNode logicalScreenDesc = getNode("LogicalScreenDescriptor", streamTree); 439 logicalScreenDesc.setAttribute("logicalScreenWidth", "" + w); 440 logicalScreenDesc.setAttribute("logicalScreenHeight", "" + h); 441 442 ByteArrayOutputStream oStream = new ByteArrayOutputStream(); 443 Iterator<ImageWriter> wIter = ImageIO.getImageWritersByFormatName("gif"); 444 ImageWriter writer = wIter.next(); 445 ImageOutputStream ios = ImageIO.createImageOutputStream(oStream); 446 writer.setOutput(ios); 447 448 IIOMetadata newStreamMd = writer.getDefaultStreamMetadata(null); 449 newStreamMd.setFromTree(streamFormat, streamTree); 450 writer.prepareWriteSequence(newStreamMd); 451 for (int i = 0; i < mGifInfo.mFrames.length; i++) { 452 BufferedImage image = (BufferedImage) mGifInfo.mFrames[i].getRenderedImage(); 453 ImageTypeSpecifier imgType = new ImageTypeSpecifier(image); 454 IIOMetadata imageMd = mGifInfo.mFrames[i].getMetadata(); 455 456 BufferedImage bufIm = transformFrame(image, w, h, t, comp); 457 458 String imageFormat = imageMd.getNativeMetadataFormatName(); 459 IIOMetadataNode imageMdTree = (IIOMetadataNode) imageMd.getAsTree(imageFormat); 460 IIOMetadataNode imageDesc = getNode("ImageDescriptor", imageMdTree); 461 if (imageDesc != null) { 462 imageDesc.setAttribute("imageWidth", "" + w); 463 imageDesc.setAttribute("imageHeight", "" + h); 464 } 465 466 IIOMetadataNode colorTable = getNode("LocalColorTable", imageMdTree); 467 if (colorTable != null) { 468 imageMdTree.removeChild(colorTable); 469 } 470 471 IIOMetadata newImageMd = writer.getDefaultImageMetadata(imgType, null); 472 newImageMd.setFromTree(imageFormat, imageMdTree); 473 474 IIOImage newImage = new IIOImage(bufIm, null, newImageMd); 475 writer.writeToSequence(newImage, null); 476 } 477 writer.endWriteSequence(); 478 ios.close(); 479 480 ImageIcon imageIcon = new ImageIcon(oStream.toByteArray()); 481 setImage(imageIcon.getImage()); 482 } catch (IOException ioe) { 483 log.error("Exception rotating animated GIF Image: ", ioe); 484 } 485 } 486 } 487 488 /** 489 * Private method which transforms one frame of Image 490 * @param frame Image frame to transform 491 * @param w Width 492 * @param h Height 493 * @param t Affine Transform 494 * @param comp 495 * @return Transformed image 496 */ 497 private BufferedImage transformFrame(Image frame, int w, int h, AffineTransform t, Component comp) { 498 499 BufferedImage bufIm = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); 500 Graphics2D g2d = bufIm.createGraphics(); 501 g2d.setRenderingHint(RenderingHints.KEY_RENDERING, 502 RenderingHints.VALUE_RENDER_QUALITY); 503 g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 504 RenderingHints.VALUE_ANTIALIAS_ON); 505 g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, 506 RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY); 507// g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, // Turned off due to poor performance, see Issue #3850 and PR #3855 for background 508// RenderingHints.VALUE_INTERPOLATION_BICUBIC); 509 g2d.drawImage(frame, t, comp); 510 g2d.dispose(); 511 return bufIm; 512 } 513 514 /** 515 * Private method to manipulate DOM tree that represents image metadata. 516 * @param name Name of node we're searching for. 517 * @param root Plate to start search 518 * @return metadata node matching name 519 */ 520 private static IIOMetadataNode getNode(String name, IIOMetadataNode root) { 521 for (int i = 0; i < root.getLength(); i++) { 522 if (root.item(i).getNodeName().compareToIgnoreCase(name) == 0) { 523 return (IIOMetadataNode) root.item(i); 524 } 525 } 526 return null; 527 } 528 529 /* 530 void debugDraw(String op, Component c) { 531 jmri.jmrit.display.Positionable pos = (jmri.jmrit.display.Positionable)c; 532 java.awt.Rectangle r = c.getBounds(); 533 log.debug(pos.getNameString()+" "+op); 534 System.out.println("\tBounds at ("+r.x+", "+r.y+") width= "+r.width+", height= "+r.height); 535 System.out.println("\tLocation at ("+c.getX()+", "+c.getY()+") width= "+ 536 c.getWidth()+", height= "+c.getHeight()); 537 } 538 */ 539 /** 540 * Scale as a percentage. 541 * 542 * @param scale the scale to set the image 543 * @param comp the containing component 544 */ 545 /* public void scale(int s, Component comp) { //log.info("scale= "+s+", 546 * "+getDescription()); if (s<1) { return; } scale(s/100.0, comp); } 547 */ 548 public void scale(double scale, Component comp) { 549 _scale = scale; 550 _transformS = AffineTransform.getScaleInstance(scale, scale); 551 rotate(_degrees, comp); 552 } 553 554 /** 555 * Rotate from anchor point (upper left corner) and shift into place. 556 * 557 * @param degree the distance to rotate 558 * @param comp containing component 559 */ 560 public void rotate(int degree, Component comp) { 561 setImage(mDefaultImage); 562 563 mRotation = 0; 564 // this _always_ returns a value between 0 and 360... 565 // (and yes, it does work properly for negative numbers) 566 _degrees = MathUtil.wrap(degree, 0, 360); 567 568 if (_degrees == 0) { 569 if (Math.abs(_scale - 1.0) > .00001) { 570 int w = (int) Math.ceil(_scale * getIconWidth()); 571 int h = (int) Math.ceil(_scale * getIconHeight()); 572 transformImage(w, h, _transformS, comp); 573 } 574 return; 575 } 576 double rad = Math.toRadians(_degrees); 577 double w = getIconWidth(); 578 double h = getIconHeight(); 579 580 int width = (int) Math.ceil(Math.abs(h * _scale * Math.sin(rad)) + Math.abs(w * _scale * Math.cos(rad))); 581 int heigth = (int) Math.ceil(Math.abs(h * _scale * Math.cos(rad)) + Math.abs(w * _scale * Math.sin(rad))); 582 AffineTransform t; 583 584 if (_degrees < 90) { 585 t = AffineTransform.getTranslateInstance(h * Math.sin(rad), 0.0); 586 } else if (_degrees < 180) { 587 t = AffineTransform.getTranslateInstance(h * Math.sin(rad) - w * Math.cos(rad), -h * Math.cos(rad)); 588 } else if (_degrees < 270) { 589 t = AffineTransform.getTranslateInstance(-w * Math.cos(rad), -w * Math.sin(rad) - h * Math.cos(rad)); 590 } else /* if (_degrees < 360) */ { 591 t = AffineTransform.getTranslateInstance(0.0, -w * Math.sin(rad)); 592 } 593 594 if (Math.abs(_scale - 1.0) > .00001) { 595 t.preConcatenate(_transformS); 596 } 597 AffineTransform r = AffineTransform.getRotateInstance(rad); 598 t.concatenate(r); 599 transformImage(width, heigth, t, comp); 600 if (comp instanceof PositionableLabel) { 601 ((PositionableLabel) comp).setDegrees(_degrees); 602 } 603 } 604 605 /** 606 * Reduce this image size to within the given dimensions, with a limit on 607 * the reduction in size. 608 * 609 * @param width new width 610 * @param height new height 611 * @param limit limit on the reduction in size 612 * @return the scale by which this image was resized 613 */ 614 public double reduceTo(int width, int height, double limit) { 615 int w = getIconWidth(); 616 int h = getIconHeight(); 617 double scale = 1.0; 618 if (w > width) { 619 scale = ((double) width) / w; 620 } 621 if (h > height) { 622 scale = Math.min(scale, ((double) height) / h); 623 } 624 if (scale < 1) { // make a thumbnail 625 if (limit > 0.0) { 626 scale = Math.max(scale, limit); // but not too small 627 } 628// java.awt.Image im = getImage(); 629// im.getScaledInstance((int)Math.ceil(scale * w), (int)Math.ceil(scale * h), java.awt.Image.SCALE_DEFAULT); 630// setImage(im); 631 AffineTransform t = AffineTransform.getScaleInstance(scale, scale); 632 transformImage((int) Math.ceil(scale * w), (int) Math.ceil(scale * h), t, null); 633 } 634 return scale; 635 } 636 637 public final static int NOFLIP = 0X00; 638 public final static int HORIZONTALFLIP = 0X01; 639 public final static int VERTICALFLIP = 0X02; 640 641 public void flip(int flip, Component comp) { 642 if (flip == NOFLIP) { 643 setImage(mDefaultImage); 644 _transformF = new AffineTransform(); 645 _degrees = 0; 646 int w = (int) Math.ceil(_scale * getIconWidth()); 647 int h = (int) Math.ceil(_scale * getIconHeight()); 648 transformImage(w, h, _transformF, comp); 649 return; 650 } 651 int w = getIconWidth(); 652 int h = getIconHeight(); 653 if (flip == HORIZONTALFLIP) { 654 _transformF = AffineTransform.getScaleInstance(-1, 1); 655 _transformF.translate(-w, 0); 656 } else { 657 _transformF = AffineTransform.getScaleInstance(1, -1); 658 _transformF.translate(0, -h); 659 } 660 661 transformImage(w, h, _transformF, null); 662 } 663 664 private final static Logger log = LoggerFactory.getLogger(NamedIcon.class); 665 666}