view src/name/blackcap/imageprep/RotateDialog.kt @ 23:92afaa27f40a

Mac app support.
author David Barts <n5jrn@me.com>
date Tue, 24 Nov 2020 16:45:37 -0800
parents 5fa5d15b4a7b
children
line wrap: on
line source

/*
 * Represents a file being edited (rotated)
 */
package name.blackcap.imageprep

import java.awt.Dimension
import java.awt.Graphics
import java.awt.Graphics2D
import java.awt.Image
import java.awt.Toolkit
import java.awt.event.ActionListener
import java.awt.image.BufferedImage
import java.awt.image.ImageObserver
import java.io.File
import java.io.IOException
import java.util.concurrent.Semaphore
import java.util.logging.Level
import java.util.logging.Logger
import javax.imageio.ImageIO
import javax.swing.*
import kotlin.math.*

class RotateDialog(val file: File, initialImage: BufferedImage) : JDialog(Application.mainFrame) {
    private val BW = 9
    private val BW2 = BW * 2

    private class DrawingPane(initialImage: BufferedImage) : JPanel() {
        var image: BufferedImage = initialImage
        set(value) {
            field = value
            revalidate()
            repaint()
        }

        override fun getPreferredSize(): Dimension {
            val screen = Toolkit.getDefaultToolkit().screenSize
            return Dimension(min(image.width, screen.width/4*3),
                min(image.height, screen.height/4*3))
        }

        override protected fun paintComponent(g: Graphics): Unit {
            g.drawImage(image, 0, 0, null)
        }
    }
    private val drawingPane = DrawingPane(initialImage)

    private class ImageWaiter: ImageObserver {
        private val MASK = ImageObserver.ALLBITS or ImageObserver.ERROR or ImageObserver.ABORT
        private val sem = Semaphore(0)
        @Volatile private var flags: Int? = null

        override fun imageUpdate(img: Image, infoflags: Int, x: Int, y: Int, width: Int, height: Int): Boolean {
            if ((infoflags and MASK) != 0) {
                flags = infoflags
                sem.release()
                return false
            }
            return true
        }

        fun wait(): Int {
            sem.acquire()
            return flags!!
        }
    }

    val image: BufferedImage
    get() { return drawingPane.image }

    private val r90cw = JButton("90° CW").also {
        it.addActionListener(ActionListener { doRotate(90) })
    }

    private val r180 = JButton("180°").also {
        it.addActionListener(ActionListener { doRotate(180) })
    }

    private val r90ccw = JButton("90° CCW").also {
        it.addActionListener(ActionListener { doRotate(270) })
    }

    /* Theoretically, this should do the rotation in a background thread.
       Practically, that is fraught with difficulties, as it involves
       manipulating data used by Swing itself. Since the size of the images
       being rotated are small, we do it in the foreground. */
    private fun doRotate(deg: Int) {
        rootPane.defaultButton = null
        // https://stackoverflow.com/questions/15927014/rotating-an-image-90-degrees-in-java
        if (deg % 90 != 0) {
            val barf = "${deg} not a multiple of 90!"
            LOGGER.log(Level.SEVERE, barf)
            throw AssertionError(barf)
        }
        val rad = java.lang.Math.toRadians(deg.toDouble())
        val (w, h) = if (deg % 180 == 0) Pair(image.width, image.height) else Pair(image.height, image.width)
        val rotatedImage = BufferedImage(w, h, image.type)
        rotatedImage.createGraphics().run {
            translate((w - image.width) / 2, (h - image.height) / 2)
            rotate(rad, image.width.toDouble()/2.0, image.height.toDouble()/2.0)
            drawRenderedImage(image, null)
            dispose()
        }
        drawingPane.image = rotatedImage
        revalidate()
        pack()
        repaint()
    }

    init {
        defaultCloseOperation = JDialog.DISPOSE_ON_CLOSE
        title = "Untitled"
        contentPane.apply {
            layout = BoxLayout(this, BoxLayout.Y_AXIS)
            add(JScrollPane(drawingPane).apply {
                alignmentX = JScrollPane.CENTER_ALIGNMENT
                addBorder(BorderFactory.createEmptyBorder(BW2, BW2, BW, BW2))
                verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS
                horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS
                background = Application.mainFrame.background
            })
            add(Box(BoxLayout.X_AXIS).apply {
                alignmentX = Box.CENTER_ALIGNMENT
                addBorder(BorderFactory.createEmptyBorder(BW, BW2, BW2, BW2))
                add(JLabel("Rotate:"))
                add(Box.createHorizontalGlue())
                add(r90cw)
                add(Box.createHorizontalGlue())
                add(r180)
                add(Box.createHorizontalGlue())
                add(r90ccw)
            })
        }
        rootPane.defaultButton = null
        pack()
    }

    companion object Factory {
        /**
         * Make a dialog asynchronously.
         *
         * @param input java.io.File to read for image.
         */
        fun makeDialog(input: File, maxDim: Int): Unit {
            Application.mainFrame.useWaitCursor()
            swingWorker<Pair<BufferedImage?, IOException?>> {
                inBackground {
                    try {
                        val imageIn = ImageIO.read(input) /* IOException */
                        if (imageIn == null)
                            throw IOException("Unsupported file type.")
                        val ratio = maxDim.toDouble() / max(imageIn.width, imageIn.height).toDouble()
                        if (ratio >= 1.0) {
                            Pair(null, null)
                        } else {
                            val nWidth = (imageIn.width * ratio).toInt()
                            val nHeight = (imageIn.height * ratio).toInt()
                            val imageOut = BufferedImage(nWidth, nHeight, imageIn.type)
                            imageOut.createGraphics().run {
                                val w = ImageWaiter()
                                if (!drawImage(imageIn.getScaledInstance(nWidth, nHeight, BufferedImage.SCALE_SMOOTH), 0, 0, w))
                                    w.wait()
                                dispose()
                            }
                            Pair(imageOut, null)
                        }
                    } catch (e: IOException) {
                        Pair(null, e)
                    }
                }
                whenDone {
                    Application.mainFrame.useNormalCursor()
                    val (image, error) = get()
                    if (error != null)
                        ioExceptionDialog(Application.mainFrame, input, "read", error)
                    else if (image != null)
                        RotateDialog(input, image).run {
                            title = input.name
                            setVisible(true)
                        }
                    else
                        JOptionPane.showMessageDialog(Application.mainFrame,
                            "Image is too small to be scaled.",
                            "Error", JOptionPane.ERROR_MESSAGE)
                }
            }
        }
    }
}