com.mucommander.ui.viewer.image.ImageViewer.java Source code

Java tutorial

Introduction

Here is the source code for com.mucommander.ui.viewer.image.ImageViewer.java

Source

/*
 * This file is part of muCommander, http://www.mucommander.com
 * Copyright (C) 2002-2012 Maxence Bernard
 *
 * muCommander is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * muCommander is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.mucommander.ui.viewer.image;

import java.awt.*;
import java.awt.event.*;
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import javax.imageio.ImageIO;
import javax.imageio.spi.IIORegistry;
import javax.swing.*;

import com.mucommander.commons.file.AbstractFile;
import com.mucommander.commons.runtime.OsFamily;
import com.mucommander.text.Translator;
import com.mucommander.ui.dialog.InformationDialog;
import com.mucommander.ui.helper.MenuToolkit;
import com.mucommander.ui.helper.MnemonicHelper;
import com.mucommander.ui.theme.ColorChangedEvent;
import com.mucommander.ui.theme.FontChangedEvent;
import com.mucommander.ui.theme.Theme;
import com.mucommander.ui.theme.ThemeListener;
import com.mucommander.ui.theme.ThemeManager;
import com.mucommander.ui.viewer.FileFrame;
import com.mucommander.ui.viewer.FileViewer;
import net.sf.image4j.codec.ico.ICODecoder;
import org.apache.batik.transcoder.Transcoder;
import org.apache.batik.transcoder.TranscoderInput;
import org.apache.batik.transcoder.TranscoderOutput;
import org.apache.batik.transcoder.image.PNGTranscoder;
import org.apache.sanselan.ImageReadException;
import org.apache.sanselan.formats.pnm.PNMImageParser;
import org.apache.sanselan.formats.psd.PsdImageParser;
import org.apache.sanselan.formats.tiff.TiffImageParser;

//import org.apache.commons.imaging.Imaging;

/**
 * A simple image viewer, capable of displaying <code>PNG</code>, <code>GIF</code> and <code>JPEG</code> images. 
 *
 * @author Maxence Bernard, Arik Hadas, Oleg Trifonov
 */
class ImageViewer extends FileViewer implements ActionListener {

    private static final Cursor CURSOR_WAIT = new Cursor(Cursor.WAIT_CURSOR);
    private static final Cursor CURSOR_DEFAULT = Cursor.getDefaultCursor();
    private static final Cursor CURSOR_CROSS = new Cursor(Cursor.CROSSHAIR_CURSOR);

    private BufferedImage image;
    private BufferedImage scaledImage;
    private double zoomFactor;

    /** Menu bar */
    // Menus //
    private JMenu controlsMenu;
    // Items //
    private JMenuItem prevImageItem;
    private JMenuItem nextImageItem;
    private JMenuItem zoomInItem;
    private JMenuItem zoomOutItem;

    private ImageViewerImpl imageViewerImpl;
    private List<AbstractFile> filesInDirectory;
    private int indexInDirectory = -1;

    private StatusBar statusBar;

    private boolean waitCursorMode = false;
    private boolean hasTransparentPixels;

    /**
     * Unknown swing issue = MouseMovement events doesn't received on windows show.
     * To fix it we make windows resize and undo it after start.
     */
    private boolean mouseMovementIssueFixed = false;

    static {
        IIORegistry registry = IIORegistry.getDefaultInstance();
        registry.registerServiceProvider(new com.realityinteractive.imageio.tga.TGAImageReaderSpi());
    }

    public ImageViewer() {
        imageViewerImpl = new ImageViewerImpl();

        setComponentToPresent(imageViewerImpl);

        // create Go menu
        MnemonicHelper menuMnemonicHelper = new MnemonicHelper();
        controlsMenu = MenuToolkit.addMenu(Translator.get("image_viewer.controls_menu"), menuMnemonicHelper, null);

        nextImageItem = MenuToolkit.addMenuItem(controlsMenu, Translator.get("image_viewer.next_image"),
                menuMnemonicHelper, KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0), this);
        prevImageItem = MenuToolkit.addMenuItem(controlsMenu, Translator.get("image_viewer.previous_image"),
                menuMnemonicHelper, KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0), this);
        controlsMenu.add(new JSeparator());
        if (OsFamily.getCurrent() != OsFamily.MAC_OS_X) {
            zoomInItem = MenuToolkit.addMenuItem(controlsMenu, Translator.get("image_viewer.zoom_in"),
                    menuMnemonicHelper, KeyStroke.getKeyStroke(KeyEvent.VK_ADD, 0), this);
            zoomOutItem = MenuToolkit.addMenuItem(controlsMenu, Translator.get("image_viewer.zoom_out"),
                    menuMnemonicHelper, KeyStroke.getKeyStroke(KeyEvent.VK_SUBTRACT, 0), this);
        } else {
            zoomInItem = MenuToolkit.addMenuItem(controlsMenu, Translator.get("image_viewer.zoom_in"),
                    menuMnemonicHelper, KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0), this);
            zoomOutItem = MenuToolkit.addMenuItem(controlsMenu, Translator.get("image_viewer.zoom_out"),
                    menuMnemonicHelper, KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0), this);
        }
    }

    @Override
    public JMenuBar getMenuBar() {
        JMenuBar menuBar = super.getMenuBar();

        menuBar.add(controlsMenu);
        setMainKeyListener(imageViewerImpl, menuBar);

        return menuBar;
    }

    @Override
    protected StatusBar getStatusBar() {
        if (statusBar == null) {
            statusBar = new StatusBar();
        }
        return statusBar;
    }

    @Override
    protected void saveStateOnClose() {

    }

    @Override
    protected void restoreStateOnStartup() {

    }

    private synchronized void loadImage(AbstractFile file) throws IOException, ImageReadException {
        setFrameCursor(CURSOR_WAIT);

        statusBar.setFileSize(file.getSize());
        statusBar.setDateTime(file.getDate());
        int imageWidth, imageHeight;
        this.scaledImage = null;
        final String ext = file.getExtension().toLowerCase();
        if ("scr".equals(ext) && file.getSize() == ZxSpectrumScrImage.SCR_IMAGE_FILE_SIZE) {
            this.image = ZxSpectrumScrImage.load(file.getInputStream());
            statusBar.setImageBpp(4);
        } else if ("psd".equals(ext)) {
            this.image = new PsdImageParser().getBufferedImage(loadFile(file), null);
        } else if ("tif".equals(ext) || "tiff".equals(ext)) {
            this.image = new TiffImageParser().getBufferedImage(loadFile(file), null);
        } else if ("ico".equals(ext)) {
            this.image = ICODecoder.read(file.getInputStream()).get(0);
            //this.image = (BufferedImage) (new IcoImageParser().getAllBufferedImages(loadFile(file)).get(0));
        } else if ("pnm".equals(ext) || "pbm".equals(ext) || "pgm".equals(ext) || "ppm".equals(ext)) {
            // TODO pBm raw format reading error
            this.image = (BufferedImage) (new PNMImageParser().getAllBufferedImages(loadFile(file)).get(0));
        } else if ("svg".equals(ext)) {
            this.image = transcodeSVGDocument(file, 0, 0);
        } else {
            this.image = ImageIO.read(file.getInputStream());
            statusBar.setImageBpp(image.getColorModel().getPixelSize());
        }
        imageWidth = image.getWidth();
        imageHeight = image.getHeight();
        this.hasTransparentPixels = image.getColorModel().hasAlpha();

        statusBar.setImageSize(imageWidth, imageHeight);

        this.zoomFactor = 1.0;
        Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();

        double zoomFactorX = 1.0 * screen.width / imageWidth;
        double zoomFactorY = 1.0 * screen.height / imageHeight;
        zoomFactor = Math.min(zoomFactorX, zoomFactorY);
        if (zoomFactor > 1.0) {
            zoomFactor = 1.0;
        }

        zoom(zoomFactor);
        fixMouseMovementEventsIssue();

        checkNextPrev();
        setFrameCursor(CURSOR_DEFAULT);
    }

    private static byte[] loadFile(AbstractFile file) throws IOException {
        InputStream is = file.getInputStream();
        byte[] data = new byte[(int) file.getSize()];
        int readTotal = 0;
        try {
            while (readTotal < data.length) {
                int bytesRead = is.read(data, readTotal, data.length - readTotal);
                if (bytesRead < 0) {
                    break;
                }
                readTotal += bytesRead;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        if (is != null) {
            try {
                is.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return data;
    }

    private void setFrameCursor(Cursor cursor) {
        if (cursor == CURSOR_WAIT) {
            waitCursorMode = true;
        } else if (cursor == CURSOR_DEFAULT) {
            waitCursorMode = false;
        } else if (cursor == CURSOR_CROSS && waitCursorMode) {
            return;
        }
        if (getFrame().getCursor() != cursor) {
            getFrame().setCursor(cursor);
        }
    }

    private synchronized void zoom(double factor) {
        setFrameCursor(CURSOR_WAIT);

        final int srcWidth = image.getWidth(null);
        final int srcHeight = image.getHeight(null);
        final int scaledWidth = (int) (srcWidth * factor);
        final int scaledHeight = (int) (srcHeight * factor);

        if (factor != 1.0) {
            AbstractFile file = filesInDirectory.get(indexInDirectory);
            if ("svg".equalsIgnoreCase(file.getExtension())) {
                try {
                    this.scaledImage = transcodeSVGDocument(file, scaledWidth, scaledHeight);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            } else {
                this.scaledImage = new BufferedImage(scaledWidth, scaledHeight, BufferedImage.TYPE_INT_ARGB);
                AffineTransform at = new AffineTransform();
                at.scale(factor, factor);
                AffineTransformOp scaleOp = new AffineTransformOp(at, AffineTransformOp.TYPE_NEAREST_NEIGHBOR);
                this.scaledImage = scaleOp.filter(this.image, this.scaledImage);
            }
        } else {
            this.scaledImage = image;
        }

        statusBar.setZoom(factor);
        checkZoom();
        setFrameCursor(CURSOR_DEFAULT);
    }

    private void fixMouseMovementEventsIssue() {
        if (mouseMovementIssueFixed) {
            return;
        }
        mouseMovementIssueFixed = true;
        Runnable task = new Runnable() {
            public void run() {
                try {
                    int w = getFrame().getWidth();
                    int h = getFrame().getHeight();
                    getFrame().setSize(w, h - 1);
                    getFrame().setSize(w, h);
                } catch (Exception e) {
                }
            }
        };
        Executors.newSingleThreadScheduledExecutor().schedule(task, 1000, TimeUnit.MILLISECONDS);
    }

    private void updateFrame() {
        FileFrame frame = getFrame();

        // Revalidate, pack and repaint should be called in this order
        frame.setTitle(this.getTitle());
        imageViewerImpl.revalidate();
        //frame.pack();
        frame.getContentPane().repaint();

    }

    private void checkZoom() {
        Dimension d = Toolkit.getDefaultToolkit().getScreenSize();

        zoomInItem.setEnabled(zoomFactor < 1.0 || (2 * zoomFactor * image.getWidth(null) < d.width
                && 2 * zoomFactor * image.getHeight(null) < d.height));

        zoomOutItem.setEnabled(zoomFactor > 1.0
                || (zoomFactor / 2 * image.getWidth(null) > 160 && zoomFactor / 2 * image.getHeight(null) > 120));
    }

    private void checkNextPrev() {
        prevImageItem.setEnabled(getPrevFileIndex() >= 0);
        nextImageItem.setEnabled(getNextFileIndex() >= 0);
    }

    ///////////////////////////////
    // FileViewer implementation //
    ///////////////////////////////

    @Override
    public void show(AbstractFile file) throws IOException {
        if (filesInDirectory == null) {
            filesInDirectory = new ArrayList<>();
            AbstractFile ls[] = file.getParent().ls();
            ImageFactory imageFactory = new ImageFactory();
            for (AbstractFile f : ls) {
                if (imageFactory.canViewFile(f)) {
                    filesInDirectory.add(f);
                }
            }
            for (int i = 0; i < filesInDirectory.size(); i++) {
                AbstractFile f = filesInDirectory.get(i);
                if (f.equals(file)) {
                    indexInDirectory = i;
                    break;
                }
            }
            statusBar.setFileNumber(indexInDirectory + 1, filesInDirectory.size());
        }
        try {
            loadImage(file);
        } catch (ImageReadException e) {
            e.printStackTrace();
            throw new IOException("Image parsing error", e);
        }
    }

    ///////////////////////////////////
    // ActionListener implementation //
    ///////////////////////////////////

    @Override
    public String getTitle() {
        return filesInDirectory.get(indexInDirectory).toString();
        //return file.getAbsolutePath()+" - "+image.getWidth(null)+"x"+image.getHeight(null)+" - "+((int)(zoomFactor*100))+"%";
    }

    public void actionPerformed(ActionEvent e) {
        Object source = e.getSource();

        if (source == zoomInItem && zoomInItem.isEnabled()) {
            zoomFactor *= 2;
            zoom(zoomFactor);
            updateFrame();
        } else if (source == zoomOutItem && zoomOutItem.isEnabled()) {
            zoomFactor /= 2;
            zoom(zoomFactor);
            updateFrame();
        } else if (source == nextImageItem && nextImageItem.isEnabled()) {
            gotoNextFile();
        } else if (source == prevImageItem && prevImageItem.isEnabled()) {
            gotoPrevFile();
        } else {
            super.actionPerformed(e);
        }
    }

    private int getNextFileIndex() {
        return indexInDirectory < filesInDirectory.size() - 1 ? indexInDirectory + 1 : -1;
    }

    private void gotoNextFile() {
        int index = getNextFileIndex();
        if (index >= 0) {
            indexInDirectory = index;
            gotoFile();
        }
    }

    private int getPrevFileIndex() {
        return indexInDirectory > 0 ? indexInDirectory - 1 : -1;
    }

    private void gotoPrevFile() {
        int index = getPrevFileIndex();
        if (index >= 0) {
            indexInDirectory = index;
            gotoFile();
        }
    }

    private void gotoFile() {
        try {
            show(filesInDirectory.get(indexInDirectory));
            statusBar.setFileNumber(indexInDirectory + 1, filesInDirectory.size());
            updateFrame();
        } catch (IOException e) {
            InformationDialog.showErrorDialog(this, Translator.get("file_viewer.view_error_title"),
                    Translator.get("file_viewer.view_error"));
            e.printStackTrace();
        }
    }

    private static String colorToRgbStr(int color) {
        int a = (color >> 24) & 0xff;
        int r = (color >> 16) & 0xff;
        int g = (color >> 8) & 0xff;
        int b = (color) & 0xff;
        String result = r + ", " + g + ", " + b;
        if (a == 0xff) {
            result = "RGB: (" + result + ")";
        } else {
            result = a + ", " + result;
            result = "ARGB: (" + result + ")";
        }
        return result;
    }

    private static String colorToHexStr(int color) {
        int a = (color >> 24) & 0xff;
        if (a == 0xff) {
            color &= 0x00ffffff;
        }
        String result = Integer.toHexString(color);
        while (result.length() < 6) {
            result = '0' + result;
        }
        if (result.length() == 7) {
            result = '0' + result;
        }
        return '#' + result.toUpperCase();
    }

    private static BufferedImage transcodeSVGDocument(AbstractFile file, float width, float height)
            throws IOException {
        // create a PNG transcoder.
        Transcoder t = new PNGTranscoder();
        // Set the transcoding hints.
        if (width > 0) {
            t.addTranscodingHint(PNGTranscoder.KEY_WIDTH, width);
        }
        if (height > 0) {
            t.addTranscodingHint(PNGTranscoder.KEY_HEIGHT, height);
        }
        t.addTranscodingHint(PNGTranscoder.KEY_XML_PARSER_VALIDATING, false);

        // create the transcoder input.
        TranscoderInput input = new TranscoderInput(file.getInputStream());
        ByteArrayOutputStream ostream = null;
        try {
            // create the transcoder output.
            ostream = new ByteArrayOutputStream();
            TranscoderOutput output = new TranscoderOutput(ostream);

            // Save the image.
            t.transcode(input, output);

            // Flush and close the stream.
            ostream.flush();
            ostream.close();
        } catch (Exception ex) {
            ex.printStackTrace();
        }

        // Convert the byte stream into an image.
        byte[] imgData = ostream.toByteArray();

        // Return the newly rendered image.
        return ImageIO.read(new ByteArrayInputStream(imgData));
    }

    private static final Color TRANSPARENT_COLOR_1 = new Color(0x666666);
    private static final Color TRANSPARENT_COLOR_2 = new Color(0x999999);
    private static final int TRANSPARENT_GRID_STEP = 8;

    /**
     * Image viewer panel
     */
    private class ImageViewerImpl extends JPanel implements MouseMotionListener, MouseListener, ThemeListener {

        private Color backgroundColor;

        ImageViewerImpl() {
            backgroundColor = ThemeManager.getCurrentColor(Theme.EDITOR_BACKGROUND_COLOR);
            ThemeManager.addCurrentThemeListener(this);
            addMouseListener(this);
            addMouseMotionListener(this);
        }

        ////////////////////////
        // Overridden methods //
        ////////////////////////

        @Override
        public void paint(Graphics g) {
            int width = getWidth();
            int height = getHeight();

            g.setColor(backgroundColor);
            g.fillRect(0, 0, width, height);

            if (scaledImage != null) {
                final int imageWidth = scaledImage.getWidth();
                final int imageHeight = scaledImage.getHeight();
                final int x0 = Math.max(0, (width - imageWidth) / 2);
                final int y0 = Math.max(0, (height - imageHeight) / 2);
                if (hasTransparentPixels) {
                    int cellW = imageWidth / TRANSPARENT_GRID_STEP;
                    if (imageWidth % TRANSPARENT_GRID_STEP > 0) {
                        cellW++;
                    }
                    int cellH = imageHeight / TRANSPARENT_GRID_STEP;
                    if (imageHeight % TRANSPARENT_GRID_STEP > 0) {
                        cellH++;
                    }
                    int x = x0;
                    int w = TRANSPARENT_GRID_STEP;
                    for (int cellX = 0; cellX < cellW; cellX++) {
                        if (cellX == cellW - 1) {
                            w = (imageWidth % TRANSPARENT_GRID_STEP == 0) ? TRANSPARENT_GRID_STEP
                                    : (imageWidth % TRANSPARENT_GRID_STEP);
                        }
                        int y = y0;
                        int h = TRANSPARENT_GRID_STEP;
                        for (int cellY = 0; cellY < cellH; cellY++) {
                            g.setColor((cellX + cellY) % 2 == 0 ? TRANSPARENT_COLOR_1 : TRANSPARENT_COLOR_2);
                            if (cellY == cellH - 1) {
                                h = (imageHeight % TRANSPARENT_GRID_STEP == 0) ? TRANSPARENT_GRID_STEP
                                        : (imageHeight % TRANSPARENT_GRID_STEP);
                            }
                            g.fillRect(x, y, w, h);
                            y += TRANSPARENT_GRID_STEP;
                        }
                        x += TRANSPARENT_GRID_STEP;
                    }
                }

                g.drawImage(scaledImage, x0, y0, null);
            }
        }

        @Override
        public synchronized Dimension getPreferredSize() {
            if (scaledImage == null) {
                return new Dimension(0, 0);
            }
            return new Dimension(scaledImage.getWidth(), scaledImage.getHeight());
        }

        //////////////////////////////////
        // ThemeListener implementation //
        //////////////////////////////////

        /**
         * Receives theme color changes notifications.
         */
        public void colorChanged(ColorChangedEvent event) {
            if (event.getColorId() == Theme.EDITOR_BACKGROUND_COLOR) {
                backgroundColor = event.getColor();
                repaint();
            }
        }

        @Override
        public void mouseMoved(MouseEvent e) {
            if (scaledImage == null) {
                return;
            }
            int x = e.getX();
            int y = e.getY();

            final int imageOffsetX = Math.max(0, (imageViewerImpl.getWidth() - scaledImage.getWidth()) / 2);
            final int imageOffsetY = Math.max(0, (imageViewerImpl.getHeight() - scaledImage.getHeight()) / 2);

            boolean inImageArea = x >= imageOffsetX && y >= imageOffsetY
                    && x < imageOffsetX + scaledImage.getWidth() && y < imageOffsetY + scaledImage.getHeight();
            if (inImageArea) {
                setFrameCursor(CURSOR_CROSS);
            } else {
                setFrameCursor(CURSOR_DEFAULT);
            }
        }

        @Override
        public void mouseClicked(MouseEvent e) {
            if (scaledImage == null) {
                return;
            }
            int x = e.getX();
            int y = e.getY();

            final int w = scaledImage.getWidth();
            final int h = scaledImage.getHeight();
            final int imageOffsetX = Math.max(0, (imageViewerImpl.getWidth() - w) / 2);
            final int imageOffsetY = Math.max(0, (imageViewerImpl.getHeight() - h) / 2);

            int pixelX = x - imageOffsetX;
            if (pixelX < 0 || pixelX >= w) {
                return;
            }
            int pixelY = y - imageOffsetY;
            if (pixelY < 0 || pixelY >= h) {
                return;
            }
            pixelX = (int) (pixelX / zoomFactor);
            pixelY = (int) (pixelY / zoomFactor);
            int color = image.getRGB(pixelX, pixelY);
            //            int r = (color >> 16) & 0xff;
            //            int g = (color >> 8) & 0xff;
            //            int b = (color) & 0xff;
            statusBar.setStatusMessage("XY: (" + pixelX + ", " + pixelY + ")  " + colorToRgbStr(color) + "  HTML: ("
                    + colorToHexStr(color) + ")");
        }

        @Override
        public void mouseExited(MouseEvent e) {
            setFrameCursor(CURSOR_DEFAULT);
        }

        /**
         * Not used, implemented as a no-op.
         */
        @Override
        public void fontChanged(FontChangedEvent event) {
        }

        @Override
        public void mouseDragged(MouseEvent e) {
            //            mouseMoved(e);
        }

        @Override
        public void mousePressed(MouseEvent e) {
        }

        @Override
        public void mouseReleased(MouseEvent e) {
        }

        @Override
        public void mouseEntered(MouseEvent e) {
        }
    }
}