Mercurial > cgi-bin > hgweb.cgi > ImagePrep
view src/name/blackcap/imageprep/RotateDialog.kt @ 31:99a0eb385c9a default tip
Work around annoying Swing glitch.
author | David Barts <n5jrn@me.com> |
---|---|
date | Sat, 20 Aug 2022 09:19:49 -0700 |
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) } } } } }