001/******************************************************************************* 002 * This software is provided as a supplement to the authors' textbooks on digital 003 * image processing published by Springer-Verlag in various languages and editions. 004 * Permission to use and distribute this software is granted under the BSD 2-Clause 005 * "Simplified" License (see http://opensource.org/licenses/BSD-2-Clause). 006 * Copyright (c) 2006-2023 Wilhelm Burger, Mark J. Burge. All rights reserved. 007 * Visit https://imagingbook.com for additional details. 008 ******************************************************************************/ 009package imagingbook.common.image; 010 011import ij.process.Blitter; 012import ij.process.ByteProcessor; 013import ij.process.ColorProcessor; 014import ij.process.FloatProcessor; 015import ij.process.ImageProcessor; 016import ij.process.ShortProcessor; 017import imagingbook.common.ij.overlay.ShapeOverlayAdapter; 018 019import java.awt.BasicStroke; 020import java.awt.Color; 021import java.awt.Graphics; 022import java.awt.Graphics2D; 023import java.awt.RenderingHints; 024import java.awt.Shape; 025import java.awt.geom.Ellipse2D; 026import java.awt.geom.Line2D; 027import java.awt.geom.Path2D; 028import java.awt.geom.Point2D; 029import java.awt.geom.Rectangle2D; 030import java.awt.image.BufferedImage; 031 032/** 033 * <p> 034 * This class defines functionality for drawing anti-aliased "pixel" graphics in images of type {@link ByteProcessor}, 035 * {@link ShortProcessor} or {@link ColorProcessor} (there is no support for {@link FloatProcessor}). It uses the 036 * capabilities of AWT's {@link Graphics2D} class by drawing to a {@link BufferedImage}, which is a copy of the original 037 * image. After performing the drawing operations the {@link BufferedImage} is copied back to the original. Thus all 038 * operations possible on a {@link Graphics2D} instance are available, including the drawing of {@link Shape} objects 039 * with floating-point coordinates, arbitrary strokes and anti-aliasing, which is not available with ImageJ's built-in 040 * graphics operations (for class {@link ImageProcessor}). 041 * </p> 042 * <p> 043 * Since drawing involves copying the image multiple times, graphic operations should be grouped for efficiency reasons. 044 * Here is an example for the intended form of use: 045 * </p> 046 * <pre> 047 * ImageProcessor ip = ... ; // some ByteProcessor, ShortProcessor or ColorProcessor 048 * try (ImageGraphics ig = new ImageGraphics(ip)) { 049 * ig.setColor(255); 050 * ig.setLineWidth(1.0); 051 * ig.drawLine(40, 100.5, 250, 101.5); 052 * ig.drawOval(230.6, 165.2, 150, 150); 053 * ... 054 * }</pre> 055 * <p> 056 * Note the use of <code>double</code> coordinates throughout. The original image ({@code ip} in the above example) is 057 * automatically updated at the end of the {@code try() ...} clause (by {@link ImageGraphics} implementing the 058 * {@link AutoCloseable} interface). The {@link #getGraphics2D()} method exposes the underlying {@link Graphics2D} 059 * instance of the {@link ImageGraphics} object, which can then be used to perform arbitrary AWT graphic operations. 060 * Thus, the above example could <strong>alternatively</strong> be implemented as follows: 061 * <pre> 062 * ImageProcessor ip = ... ; // some ByteProcessor, ShortProcessor or ColorProcessor 063 * try (ImageGraphics ig = new ImageGraphics(ip)) { 064 * Graphics2D g2 = ig.getGraphics2D(); 065 * g2.setColor(Color.white); 066 * g2.setStroke(new BasicStroke(1.0f)); 067 * g2.draw(new Line2D.Double(40, 100.5, 250, 101.5)); 068 * g2.draw(new Ellipse2D.Double(230.6, 165.2, 150, 150)); 069 * ... 070 * }</pre> 071 * <p> 072 * This class also defines several convenience methods for drawing shapes with floating-point ({@code double}) 073 * coordinates, as well as for setting colors and stroke parameters. If intermediate updates are needed (e.g., for 074 * animations), the {@code update()} method can be invoked any time. 075 * </p> 076 * 077 * @author WB 078 * @version 2020-01-07 079 * @see ShapeOverlayAdapter 080 */ 081public class ImageGraphics implements AutoCloseable { 082 083 // TODO: Add text drawing, integrate with ShapeOverlayAdapter and ColoredStroke. 084 085 private static BasicStroke DEFAULT_STROKE = new BasicStroke(); 086 private static Color DEFAULT_COLOR = Color.white; 087 private static boolean DEFAULT_ANTIALIASING = true; 088 089 private ImageProcessor ip; 090 private BufferedImage bi; 091 private final Graphics2D g2; 092 093 private BasicStroke stroke = DEFAULT_STROKE; 094 private Color color = DEFAULT_COLOR; 095 096 // ------------------------------------------------------------- 097 098 /** 099 * Constructor. The supplied image must be of type {@link ByteProcessor}, {@link ShortProcessor} or 100 * {@link ColorProcessor}. An {@link IllegalArgumentException} is thrown for images of type {@link FloatProcessor}. 101 * 102 * @param ip image to draw on 103 */ 104 public ImageGraphics(ImageProcessor ip) { 105 this(ip, null, null); 106 } 107 108 /** 109 * Constructor. The supplied image must be of type {@link ByteProcessor}, {@link ShortProcessor} or 110 * {@link ColorProcessor}. An {@link IllegalArgumentException} is thrown for images of type {@link FloatProcessor}. 111 * 112 * @param ip image to draw on 113 * @param color the initial drawing color 114 * @param stroke the initial stroke 115 */ 116 public ImageGraphics(ImageProcessor ip, Color color, BasicStroke stroke) { 117 this.ip = ip; 118 this.bi = toBufferedImage(ip); // throws exception when ip is a ShortProcessor 119 120 if (color != null) this.color = color; 121 if (stroke != null) this.stroke = stroke; 122 123 this.g2 = (Graphics2D) bi.getGraphics(); 124 this.g2.setColor(color); 125 this.g2.setColor(this.color); 126 this.g2.setStroke(this.stroke); 127 this.setAntialiasing(DEFAULT_ANTIALIASING); 128 } 129 130 /** 131 * Turn anti-aliasing on/off for this {@link ImageGraphics} instance (turned on by default). The new setting will 132 * only affect subsequent graphics operations. 133 * 134 * @param onoff set true to turn on 135 */ 136 public void setAntialiasing(boolean onoff) { 137 g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, onoff ? 138 RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF); 139 g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, onoff ? 140 RenderingHints.VALUE_TEXT_ANTIALIAS_ON : RenderingHints.VALUE_TEXT_ANTIALIAS_OFF); 141 } 142 143 /** 144 * Returns the underlying {@link Graphics2D} object, which can be used to perform arbitrary graphics operations. 145 * 146 * @return the {@link Graphics2D} object 147 */ 148 public Graphics2D getGraphics2D() { 149 return this.g2; 150 } 151 152 /** 153 * Forces the image to be updated by copying the (modified) {@link BufferedImage} back to the input image. There is 154 * usually no need to call this (expensive) method explicitly. It is called automatically and only once at end of 155 * the {@code try() ...} clause, as described in the {@link ImageGraphics} class documentation above. 156 */ 157 public void update() { 158 copyImageToProcessor(bi, ip); 159 } 160 161 @Override 162 public void close() { 163 update(); 164 ip = null; 165 bi = null; 166 } 167 168 // ----------------------------------------------------------- 169 170 // Needed, since IJ's conversion methods are not named consistently. 171 private BufferedImage toBufferedImage(ImageProcessor ip) { 172 if (ip instanceof ByteProcessor) { 173 return ((ByteProcessor) ip).getBufferedImage(); 174 } 175 else if (ip instanceof ShortProcessor) { 176 return ((ShortProcessor) ip).get16BitBufferedImage(); 177 } 178 else if (ip instanceof ColorProcessor) { 179 return ((ColorProcessor) ip).getBufferedImage(); 180 } 181 else { 182 throw new IllegalArgumentException("Cannot create BufferedImage from " + 183 ip.getClass().getName()); 184 } 185 } 186 187 /** 188 * Copies the contents of the {@link BufferedImage} to the specified {@link ImageProcessor}. The size and type of 189 * the BufferedImage is assumed to match the ImageProcessor. 190 * 191 * @param bi the local (intermediate) {@link BufferedImage} instance 192 * @param ip the original {@link ImageProcessor} 193 */ 194 private void copyImageToProcessor(BufferedImage bi, ImageProcessor ip) { 195 ImageProcessor ip2 = null; 196 if (ip instanceof ByteProcessor) { 197 ip2 = new ByteProcessor(bi); 198 } 199 else if (ip instanceof ShortProcessor) { 200 ip2 = new ShortProcessor(bi); 201 } 202 else if (ip instanceof ColorProcessor) { 203 ip2 = new ColorProcessor(bi); 204 } 205 else { 206 throw new IllegalArgumentException("Cannot create BufferedImage from " + 207 ip.getClass().getName()); 208 } 209 ip.copyBits(ip2, 0, 0, Blitter.COPY); 210 } 211 212 // ----------------------------------------------------------- 213 // Convenience methods for drawing selected shapes with double coordinates 214 // ----------------------------------------------------------- 215 216 /** 217 * Draws a straight line segment specified with {@code double} coordinate values (convenience method). 218 * 219 * @param x1 x-coordinate of start point 220 * @param y1 y-coordinate of start point 221 * @param x2 x-coordinate of end point 222 * @param y2 y-coordinate of end point 223 * @see Line2D 224 */ 225 public void drawLine(double x1, double y1, double x2, double y2) { 226 g2.draw(new Line2D.Double(x1, y1, x2, y2)); 227 } 228 229 /** 230 * Draws an ellipse specified with {@code double} coordinate values (convenience method). 231 * 232 * @param x x-coordinate of the upper-left corner of the framing rectangle 233 * @param y y-coordinate of the upper-left corner of the framing rectangle 234 * @param w width 235 * @param h height 236 * @see Ellipse2D 237 */ 238 public void drawOval(double x, double y, double w, double h) { 239 g2.draw(new Ellipse2D.Double(x, y, w, h)); 240 } 241 242 /** 243 * Draws a rectangle specified with {@code double} coordinate values (convenience method). 244 * 245 * @param x x-coordinate of the upper-left corner 246 * @param y y-coordinate of the upper-left corner 247 * @param w width 248 * @param h height 249 * @see Rectangle2D 250 */ 251 public void drawRectangle(double x, double y, double w, double h) { 252 g2.draw(new Rectangle2D.Double(x, y, w, h)); 253 } 254 255 /** 256 * Draws a closed polygon specified by a sequence of {@link Point2D} objects with arbitrary coordinate values 257 * (convenience method). Note that the the polygon is automatically closed, i.e., N+1 segments are drawn if the 258 * number of given points is N. 259 * 260 * @param points a sequence of 2D points 261 * @see Path2D 262 */ 263 public void drawPolygon(Point2D ... points) { 264 Path2D.Double p = new Path2D.Double(); 265 p.moveTo(points[0].getX(), points[0].getY()); 266 for (int i = 1; i < points.length; i++) { 267 p.lineTo(points[i].getX(), points[i].getY()); 268 } 269 p.closePath(); 270 g2.draw(p); 271 } 272 273 // stroke-related methods ------------------------------------- 274 275 /** 276 * Sets this graphics context's current color to the specified color. All subsequent graphics operations using this 277 * graphics context use this specified color. 278 * 279 * @param color the new rendering color 280 * @see Graphics#setColor 281 */ 282 public void setColor(Color color) { 283 this.color = color; 284 g2.setColor(color); 285 } 286 287 /** 288 * Sets this graphics context's current color to the specified (gray) color, with RGB = (gray, gray, gray). 289 * 290 * @param gray the gray value 291 */ 292 public void setColor(int gray) { 293 if (gray < 0) gray = 0; 294 if (gray > 255) gray = 255; 295 this.setColor(new Color(gray, gray, gray)); 296 } 297 298 /** 299 * Sets the stroke to be used for all subsequent graphics operations. 300 * 301 * @param stroke a {@link BasicStroke} instance 302 * @see BasicStroke 303 */ 304 public void setStroke(BasicStroke stroke) { 305 this.stroke = stroke; 306 g2.setStroke(this.stroke); 307 } 308 309 /** 310 * Sets the line width of the current stroke. All other stroke properties remain unchanged. 311 * 312 * @param width the line width 313 * @see BasicStroke 314 */ 315 public void setLineWidth(double width) { 316 this.stroke = new BasicStroke((float)width, stroke.getEndCap(), stroke.getLineJoin()); 317 g2.setStroke(this.stroke); 318 } 319 320 /** 321 * Sets the end cap style of the current stroke to "BUTT". All other stroke properties remain unchanged. 322 * 323 * @see BasicStroke 324 */ 325 public void setEndCapButt() { 326 this.setEndCap(BasicStroke.CAP_BUTT); 327 } 328 329 /** 330 * Sets the end cap style of the current stroke to "ROUND". All other stroke properties remain unchanged. 331 * 332 * @see BasicStroke 333 */ 334 public void setEndCapRound() { 335 this.setEndCap(BasicStroke.CAP_ROUND); 336 } 337 338 /** 339 * Sets the end cap style of the current stroke to "SQUARE". All other stroke properties remain unchanged. 340 * 341 * @see BasicStroke 342 */ 343 public void setEndCapSquare() { 344 this.setEndCap(BasicStroke.CAP_SQUARE); 345 } 346 347 private void setEndCap(int cap) { 348 setStroke(new BasicStroke(stroke.getLineWidth(), cap, stroke.getLineJoin())); 349 } 350 351 // --------------------- 352 353 /** 354 * Sets the line segment join style of the current stroke to "BEVEL". All other stroke properties remain unchanged. 355 * 356 * @see BasicStroke 357 */ 358 public void setLineJoinBevel() { 359 setLineJoin(BasicStroke.JOIN_BEVEL); 360 } 361 362 /** 363 * Sets the line segment join style of the current stroke to "MITER". All other stroke properties remain unchanged. 364 * 365 * @see BasicStroke 366 */ 367 public void setLineJoinMiter() { 368 setLineJoin(BasicStroke.JOIN_MITER); 369 } 370 371 /** 372 * Sets the line segment join style of the current stroke to "ROUND". All other stroke properties remain unchanged. 373 * 374 * @see BasicStroke 375 */ 376 public void setLineJoinRound() { 377 setLineJoin(BasicStroke.JOIN_ROUND); 378 } 379 380 private void setLineJoin(int join) { 381 setStroke(new BasicStroke(stroke.getLineWidth(), stroke.getEndCap(), join)); 382 } 383}