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}