Mercurial > cgi-bin > hgweb.cgi > ImagePrep
annotate 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 |
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 |
12
26a507e095ab
Use ImageObserver and wait if needed.
David Barts <n5jrn@me.com>
parents:
11
diff
changeset
|
9 import java.awt.Image |
6
9129ae110146
Window reshapes to avoid gratuitous scrollbars (as it should).
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
10 import java.awt.Toolkit |
1 | 11 import java.awt.event.ActionListener |
0 | 12 import java.awt.image.BufferedImage |
12
26a507e095ab
Use ImageObserver and wait if needed.
David Barts <n5jrn@me.com>
parents:
11
diff
changeset
|
13 import java.awt.image.ImageObserver |
0 | 14 import java.io.File |
15 import java.io.IOException | |
12
26a507e095ab
Use ImageObserver and wait if needed.
David Barts <n5jrn@me.com>
parents:
11
diff
changeset
|
16 import java.util.concurrent.Semaphore |
3 | 17 import java.util.logging.Level |
18 import java.util.logging.Logger | |
0 | 19 import javax.imageio.ImageIO |
20 import javax.swing.* | |
21 import kotlin.math.* | |
22 | |
23 class RotateDialog(val file: File, initialImage: BufferedImage) : JDialog(Application.mainFrame) { | |
24 private val BW = 9 | |
25 private val BW2 = BW * 2 | |
26 | |
1 | 27 private class DrawingPane(initialImage: BufferedImage) : JPanel() { |
0 | 28 var image: BufferedImage = initialImage |
29 set(value) { | |
30 field = value | |
31 revalidate() | |
32 repaint() | |
33 } | |
34 | |
6
9129ae110146
Window reshapes to avoid gratuitous scrollbars (as it should).
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
35 override fun getPreferredSize(): Dimension { |
9129ae110146
Window reshapes to avoid gratuitous scrollbars (as it should).
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
36 val screen = Toolkit.getDefaultToolkit().screenSize |
9129ae110146
Window reshapes to avoid gratuitous scrollbars (as it should).
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
37 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
|
38 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
|
39 } |
0 | 40 |
1 | 41 override protected fun paintComponent(g: Graphics): Unit { |
0 | 42 g.drawImage(image, 0, 0, null) |
43 } | |
44 } | |
1 | 45 private val drawingPane = DrawingPane(initialImage) |
0 | 46 |
12
26a507e095ab
Use ImageObserver and wait if needed.
David Barts <n5jrn@me.com>
parents:
11
diff
changeset
|
47 private class ImageWaiter: ImageObserver { |
13 | 48 private val MASK = ImageObserver.ALLBITS or ImageObserver.ERROR or ImageObserver.ABORT |
49 private val sem = Semaphore(0) | |
12
26a507e095ab
Use ImageObserver and wait if needed.
David Barts <n5jrn@me.com>
parents:
11
diff
changeset
|
50 @Volatile private var flags: Int? = null |
26a507e095ab
Use ImageObserver and wait if needed.
David Barts <n5jrn@me.com>
parents:
11
diff
changeset
|
51 |
26a507e095ab
Use ImageObserver and wait if needed.
David Barts <n5jrn@me.com>
parents:
11
diff
changeset
|
52 override fun imageUpdate(img: Image, infoflags: Int, x: Int, y: Int, width: Int, height: Int): Boolean { |
13 | 53 if ((infoflags and MASK) != 0) { |
12
26a507e095ab
Use ImageObserver and wait if needed.
David Barts <n5jrn@me.com>
parents:
11
diff
changeset
|
54 flags = infoflags |
26a507e095ab
Use ImageObserver and wait if needed.
David Barts <n5jrn@me.com>
parents:
11
diff
changeset
|
55 sem.release() |
26a507e095ab
Use ImageObserver and wait if needed.
David Barts <n5jrn@me.com>
parents:
11
diff
changeset
|
56 return false |
26a507e095ab
Use ImageObserver and wait if needed.
David Barts <n5jrn@me.com>
parents:
11
diff
changeset
|
57 } |
26a507e095ab
Use ImageObserver and wait if needed.
David Barts <n5jrn@me.com>
parents:
11
diff
changeset
|
58 return true |
26a507e095ab
Use ImageObserver and wait if needed.
David Barts <n5jrn@me.com>
parents:
11
diff
changeset
|
59 } |
26a507e095ab
Use ImageObserver and wait if needed.
David Barts <n5jrn@me.com>
parents:
11
diff
changeset
|
60 |
26a507e095ab
Use ImageObserver and wait if needed.
David Barts <n5jrn@me.com>
parents:
11
diff
changeset
|
61 fun wait(): Int { |
26a507e095ab
Use ImageObserver and wait if needed.
David Barts <n5jrn@me.com>
parents:
11
diff
changeset
|
62 sem.acquire() |
26a507e095ab
Use ImageObserver and wait if needed.
David Barts <n5jrn@me.com>
parents:
11
diff
changeset
|
63 return flags!! |
26a507e095ab
Use ImageObserver and wait if needed.
David Barts <n5jrn@me.com>
parents:
11
diff
changeset
|
64 } |
26a507e095ab
Use ImageObserver and wait if needed.
David Barts <n5jrn@me.com>
parents:
11
diff
changeset
|
65 } |
26a507e095ab
Use ImageObserver and wait if needed.
David Barts <n5jrn@me.com>
parents:
11
diff
changeset
|
66 |
0 | 67 val image: BufferedImage |
68 get() { return drawingPane.image } | |
69 | |
1 | 70 private val r90cw = JButton("90° CW").also { |
0 | 71 it.addActionListener(ActionListener { doRotate(90) }) |
72 } | |
73 | |
1 | 74 private val r180 = JButton("180°").also { |
0 | 75 it.addActionListener(ActionListener { doRotate(180) }) |
76 } | |
77 | |
1 | 78 private val r90ccw = JButton("90° CCW").also { |
0 | 79 it.addActionListener(ActionListener { doRotate(270) }) |
80 } | |
81 | |
82 /* Theoretically, this should do the rotation in a background thread. | |
83 Practically, that is fraught with difficulties, as it involves | |
84 manipulating data used by Swing itself. Since the size of the images | |
85 being rotated are small, we do it in the foreground. */ | |
86 private fun doRotate(deg: Int) { | |
3 | 87 rootPane.defaultButton = null |
0 | 88 // https://stackoverflow.com/questions/15927014/rotating-an-image-90-degrees-in-java |
3 | 89 if (deg % 90 != 0) { |
90 val barf = "${deg} not a multiple of 90!" | |
91 LOGGER.log(Level.SEVERE, barf) | |
92 throw AssertionError(barf) | |
93 } | |
0 | 94 val rad = java.lang.Math.toRadians(deg.toDouble()) |
3 | 95 val (w, h) = if (deg % 180 == 0) Pair(image.width, image.height) else Pair(image.height, image.width) |
0 | 96 val rotatedImage = BufferedImage(w, h, image.type) |
97 rotatedImage.createGraphics().run { | |
98 translate((w - image.width) / 2, (h - image.height) / 2) | |
1 | 99 rotate(rad, image.width.toDouble()/2.0, image.height.toDouble()/2.0) |
0 | 100 drawRenderedImage(image, null) |
101 dispose() | |
102 } | |
103 drawingPane.image = rotatedImage | |
6
9129ae110146
Window reshapes to avoid gratuitous scrollbars (as it should).
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
104 revalidate() |
9129ae110146
Window reshapes to avoid gratuitous scrollbars (as it should).
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
105 pack() |
9129ae110146
Window reshapes to avoid gratuitous scrollbars (as it should).
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
106 repaint() |
0 | 107 } |
108 | |
109 init { | |
110 defaultCloseOperation = JDialog.DISPOSE_ON_CLOSE | |
111 title = "Untitled" | |
112 contentPane.apply { | |
113 layout = BoxLayout(this, BoxLayout.Y_AXIS) | |
1 | 114 add(JScrollPane(drawingPane).apply { |
0 | 115 alignmentX = JScrollPane.CENTER_ALIGNMENT |
116 addBorder(BorderFactory.createEmptyBorder(BW2, BW2, BW, BW2)) | |
117 verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS | |
118 horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS | |
119 background = Application.mainFrame.background | |
120 }) | |
121 add(Box(BoxLayout.X_AXIS).apply { | |
122 alignmentX = Box.CENTER_ALIGNMENT | |
123 addBorder(BorderFactory.createEmptyBorder(BW, BW2, BW2, BW2)) | |
124 add(JLabel("Rotate:")) | |
125 add(Box.createHorizontalGlue()) | |
126 add(r90cw) | |
127 add(Box.createHorizontalGlue()) | |
128 add(r180) | |
129 add(Box.createHorizontalGlue()) | |
130 add(r90ccw) | |
131 }) | |
132 } | |
3 | 133 rootPane.defaultButton = null |
0 | 134 pack() |
135 } | |
136 | |
137 companion object Factory { | |
138 /** | |
139 * Make a dialog asynchronously. | |
140 * | |
141 * @param input java.io.File to read for image. | |
142 */ | |
19
5fa5d15b4a7b
Attempt to make it a std. app. Builds, sorta runs, needs more work.
David Barts <n5jrn@me.com>
parents:
16
diff
changeset
|
143 fun makeDialog(input: File, maxDim: Int): Unit { |
0 | 144 Application.mainFrame.useWaitCursor() |
1 | 145 swingWorker<Pair<BufferedImage?, IOException?>> { |
0 | 146 inBackground { |
147 try { | |
148 val imageIn = ImageIO.read(input) /* IOException */ | |
16
2bb46da74667
Detect unsupported file types on input.
David Barts <n5jrn@me.com>
parents:
13
diff
changeset
|
149 if (imageIn == null) |
2bb46da74667
Detect unsupported file types on input.
David Barts <n5jrn@me.com>
parents:
13
diff
changeset
|
150 throw IOException("Unsupported file type.") |
19
5fa5d15b4a7b
Attempt to make it a std. app. Builds, sorta runs, needs more work.
David Barts <n5jrn@me.com>
parents:
16
diff
changeset
|
151 val ratio = maxDim.toDouble() / max(imageIn.width, imageIn.height).toDouble() |
0 | 152 if (ratio >= 1.0) { |
1 | 153 Pair(null, null) |
0 | 154 } else { |
155 val nWidth = (imageIn.width * ratio).toInt() | |
156 val nHeight = (imageIn.height * ratio).toInt() | |
11 | 157 val imageOut = BufferedImage(nWidth, nHeight, imageIn.type) |
158 imageOut.createGraphics().run { | |
12
26a507e095ab
Use ImageObserver and wait if needed.
David Barts <n5jrn@me.com>
parents:
11
diff
changeset
|
159 val w = ImageWaiter() |
26a507e095ab
Use ImageObserver and wait if needed.
David Barts <n5jrn@me.com>
parents:
11
diff
changeset
|
160 if (!drawImage(imageIn.getScaledInstance(nWidth, nHeight, BufferedImage.SCALE_SMOOTH), 0, 0, w)) |
26a507e095ab
Use ImageObserver and wait if needed.
David Barts <n5jrn@me.com>
parents:
11
diff
changeset
|
161 w.wait() |
11 | 162 dispose() |
0 | 163 } |
164 Pair(imageOut, null) | |
165 } | |
166 } catch (e: IOException) { | |
167 Pair(null, e) | |
168 } | |
169 } | |
170 whenDone { | |
171 Application.mainFrame.useNormalCursor() | |
172 val (image, error) = get() | |
173 if (error != null) | |
174 ioExceptionDialog(Application.mainFrame, input, "read", error) | |
1 | 175 else if (image != null) |
3 | 176 RotateDialog(input, image).run { |
177 title = input.name | |
178 setVisible(true) | |
179 } | |
1 | 180 else |
181 JOptionPane.showMessageDialog(Application.mainFrame, | |
182 "Image is too small to be scaled.", | |
19
5fa5d15b4a7b
Attempt to make it a std. app. Builds, sorta runs, needs more work.
David Barts <n5jrn@me.com>
parents:
16
diff
changeset
|
183 "Error", JOptionPane.ERROR_MESSAGE) |
0 | 184 } |
185 } | |
186 } | |
187 } | |
188 } |