Mercurial > cgi-bin > hgweb.cgi > ImagePrep
annotate src/name/blackcap/imageprep/RotateDialog.kt @ 11:1f824742e1fa
Improve scaling quality.
author | David Barts <n5jrn@me.com> |
---|---|
date | Sat, 18 Jul 2020 08:12:00 -0700 |
parents | b5fcabce391f |
children | 26a507e095ab |
rev | line source |
---|---|
0 | 1 /* |
2 * Represents a file being edited (rotated) | |
3 */ | |
4 package name.blackcap.imageprep | |
5 | |
1 | 6 import java.awt.Dimension |
7 import java.awt.Graphics | |
0 | 8 import java.awt.Graphics2D |
6
9129ae110146
Window reshapes to avoid gratuitous scrollbars (as it should).
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
9 import java.awt.Toolkit |
1 | 10 import java.awt.event.ActionListener |
0 | 11 import java.awt.image.BufferedImage |
12 import java.io.File | |
13 import java.io.IOException | |
3 | 14 import java.util.logging.Level |
15 import java.util.logging.Logger | |
0 | 16 import javax.imageio.ImageIO |
17 import javax.swing.* | |
18 import kotlin.math.* | |
19 | |
20 class RotateDialog(val file: File, initialImage: BufferedImage) : JDialog(Application.mainFrame) { | |
21 private val BW = 9 | |
22 private val BW2 = BW * 2 | |
23 | |
1 | 24 private class DrawingPane(initialImage: BufferedImage) : JPanel() { |
0 | 25 var image: BufferedImage = initialImage |
26 set(value) { | |
27 field = value | |
28 revalidate() | |
29 repaint() | |
30 } | |
31 | |
6
9129ae110146
Window reshapes to avoid gratuitous scrollbars (as it should).
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
32 override fun getPreferredSize(): Dimension { |
9129ae110146
Window reshapes to avoid gratuitous scrollbars (as it should).
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
33 val screen = Toolkit.getDefaultToolkit().screenSize |
9129ae110146
Window reshapes to avoid gratuitous scrollbars (as it should).
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
34 return Dimension(min(image.width, screen.width/4*3), |
9129ae110146
Window reshapes to avoid gratuitous scrollbars (as it should).
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
35 min(image.height, screen.height/4*3)) |
9129ae110146
Window reshapes to avoid gratuitous scrollbars (as it should).
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
36 } |
0 | 37 |
1 | 38 override protected fun paintComponent(g: Graphics): Unit { |
0 | 39 g.drawImage(image, 0, 0, null) |
40 } | |
41 } | |
1 | 42 private val drawingPane = DrawingPane(initialImage) |
0 | 43 |
44 val image: BufferedImage | |
45 get() { return drawingPane.image } | |
46 | |
1 | 47 private val r90cw = JButton("90° CW").also { |
0 | 48 it.addActionListener(ActionListener { doRotate(90) }) |
49 } | |
50 | |
1 | 51 private val r180 = JButton("180°").also { |
0 | 52 it.addActionListener(ActionListener { doRotate(180) }) |
53 } | |
54 | |
1 | 55 private val r90ccw = JButton("90° CCW").also { |
0 | 56 it.addActionListener(ActionListener { doRotate(270) }) |
57 } | |
58 | |
59 /* Theoretically, this should do the rotation in a background thread. | |
60 Practically, that is fraught with difficulties, as it involves | |
61 manipulating data used by Swing itself. Since the size of the images | |
62 being rotated are small, we do it in the foreground. */ | |
63 private fun doRotate(deg: Int) { | |
3 | 64 rootPane.defaultButton = null |
0 | 65 // https://stackoverflow.com/questions/15927014/rotating-an-image-90-degrees-in-java |
3 | 66 if (deg % 90 != 0) { |
67 val barf = "${deg} not a multiple of 90!" | |
68 LOGGER.log(Level.SEVERE, barf) | |
69 throw AssertionError(barf) | |
70 } | |
0 | 71 val rad = java.lang.Math.toRadians(deg.toDouble()) |
3 | 72 val (w, h) = if (deg % 180 == 0) Pair(image.width, image.height) else Pair(image.height, image.width) |
0 | 73 val rotatedImage = BufferedImage(w, h, image.type) |
74 rotatedImage.createGraphics().run { | |
75 translate((w - image.width) / 2, (h - image.height) / 2) | |
1 | 76 rotate(rad, image.width.toDouble()/2.0, image.height.toDouble()/2.0) |
0 | 77 drawRenderedImage(image, null) |
78 dispose() | |
79 } | |
80 drawingPane.image = rotatedImage | |
6
9129ae110146
Window reshapes to avoid gratuitous scrollbars (as it should).
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
81 revalidate() |
9129ae110146
Window reshapes to avoid gratuitous scrollbars (as it should).
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
82 pack() |
9129ae110146
Window reshapes to avoid gratuitous scrollbars (as it should).
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
83 repaint() |
0 | 84 } |
85 | |
86 init { | |
87 defaultCloseOperation = JDialog.DISPOSE_ON_CLOSE | |
88 title = "Untitled" | |
89 contentPane.apply { | |
90 layout = BoxLayout(this, BoxLayout.Y_AXIS) | |
1 | 91 add(JScrollPane(drawingPane).apply { |
0 | 92 alignmentX = JScrollPane.CENTER_ALIGNMENT |
93 addBorder(BorderFactory.createEmptyBorder(BW2, BW2, BW, BW2)) | |
94 verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS | |
95 horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS | |
96 background = Application.mainFrame.background | |
97 }) | |
98 add(Box(BoxLayout.X_AXIS).apply { | |
99 alignmentX = Box.CENTER_ALIGNMENT | |
100 addBorder(BorderFactory.createEmptyBorder(BW, BW2, BW2, BW2)) | |
101 add(JLabel("Rotate:")) | |
102 add(Box.createHorizontalGlue()) | |
103 add(r90cw) | |
104 add(Box.createHorizontalGlue()) | |
105 add(r180) | |
106 add(Box.createHorizontalGlue()) | |
107 add(r90ccw) | |
108 }) | |
109 } | |
3 | 110 rootPane.defaultButton = null |
0 | 111 pack() |
112 } | |
113 | |
114 companion object Factory { | |
115 /** | |
116 * Make a dialog asynchronously. | |
117 * | |
118 * @param input java.io.File to read for image. | |
119 */ | |
120 fun makeDialog(input: File): Unit { | |
121 Application.mainFrame.useWaitCursor() | |
1 | 122 swingWorker<Pair<BufferedImage?, IOException?>> { |
0 | 123 inBackground { |
124 try { | |
125 val imageIn = ImageIO.read(input) /* IOException */ | |
126 val ratio = Settings.maxDimension.toDouble() / max(imageIn.width, imageIn.height).toDouble() | |
127 if (ratio >= 1.0) { | |
1 | 128 Pair(null, null) |
0 | 129 } else { |
130 val nWidth = (imageIn.width * ratio).toInt() | |
131 val nHeight = (imageIn.height * ratio).toInt() | |
11 | 132 val imageOut = BufferedImage(nWidth, nHeight, imageIn.type) |
133 imageOut.createGraphics().run { | |
134 drawImage(imageIn.getScaledInstance(nWidth, nHeight, BufferedImage.SCALE_SMOOTH), 0, 0, null) | |
135 dispose() | |
0 | 136 } |
137 Pair(imageOut, null) | |
138 } | |
139 } catch (e: IOException) { | |
140 Pair(null, e) | |
141 } | |
142 } | |
143 whenDone { | |
144 Application.mainFrame.useNormalCursor() | |
145 val (image, error) = get() | |
146 if (error != null) | |
147 ioExceptionDialog(Application.mainFrame, input, "read", error) | |
1 | 148 else if (image != null) |
3 | 149 RotateDialog(input, image).run { |
150 title = input.name | |
151 setVisible(true) | |
152 } | |
1 | 153 else |
154 JOptionPane.showMessageDialog(Application.mainFrame, | |
155 "Image is too small to be scaled.", | |
156 "Warning", JOptionPane.WARNING_MESSAGE) | |
0 | 157 } |
158 } | |
159 } | |
160 } | |
161 } |