|
| 1 | +/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ |
| 2 | + |
| 3 | +/* |
| 4 | + Part of the Processing project - http://processing.org |
| 5 | +
|
| 6 | + Copyright (c) 2015 The Processing Foundation |
| 7 | +
|
| 8 | + This library is free software; you can redistribute it and/or |
| 9 | + modify it under the terms of the GNU Lesser General Public |
| 10 | + License version 2.1 as published by the Free Software Foundation. |
| 11 | +
|
| 12 | + This library is distributed in the hope that it will be useful, |
| 13 | + but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 14 | + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| 15 | + Lesser General Public License for more details. |
| 16 | +
|
| 17 | + You should have received a copy of the GNU Lesser General |
| 18 | + Public License along with this library; if not, write to the |
| 19 | + Free Software Foundation, Inc., 59 Temple Place, Suite 330, |
| 20 | + Boston, MA 02111-1307 USA |
| 21 | +*/ |
| 22 | + |
| 23 | +package processing.awt; |
| 24 | + |
| 25 | +import java.awt.Graphics2D; |
| 26 | +import java.awt.Image; |
| 27 | +import java.awt.RenderingHints; |
| 28 | +import java.awt.Transparency; |
| 29 | +import java.awt.image.BufferedImage; |
| 30 | +import java.awt.image.DataBuffer; |
| 31 | +import java.awt.image.DataBufferInt; |
| 32 | +import java.awt.image.PixelGrabber; |
| 33 | +import java.awt.image.WritableRaster; |
| 34 | +import java.io.BufferedOutputStream; |
| 35 | +import java.io.File; |
| 36 | +import java.io.IOException; |
| 37 | +import java.util.Iterator; |
| 38 | + |
| 39 | +import javax.imageio.IIOImage; |
| 40 | +import javax.imageio.ImageIO; |
| 41 | +import javax.imageio.ImageTypeSpecifier; |
| 42 | +import javax.imageio.ImageWriteParam; |
| 43 | +import javax.imageio.ImageWriter; |
| 44 | +import javax.imageio.metadata.IIOInvalidTreeException; |
| 45 | +import javax.imageio.metadata.IIOMetadata; |
| 46 | +import javax.imageio.metadata.IIOMetadataNode; |
| 47 | + |
| 48 | +import processing.core.PApplet; |
| 49 | +import processing.core.PImage; |
| 50 | + |
| 51 | + |
| 52 | +public class PImageAWT extends PImage { |
| 53 | + |
| 54 | + /** |
| 55 | + * Construct a new PImage from a java.awt.Image. This constructor assumes |
| 56 | + * that you've done the work of making sure a MediaTracker has been used |
| 57 | + * to fully download the data and that the img is valid. |
| 58 | + * |
| 59 | + * @nowebref |
| 60 | + * @param img assumes a MediaTracker has been used to fully download |
| 61 | + * the data and the img is valid |
| 62 | + */ |
| 63 | + public PImageAWT(Image img) { |
| 64 | + format = RGB; |
| 65 | + if (img instanceof BufferedImage) { |
| 66 | + BufferedImage bi = (BufferedImage) img; |
| 67 | + width = bi.getWidth(); |
| 68 | + height = bi.getHeight(); |
| 69 | + int type = bi.getType(); |
| 70 | + if (type == BufferedImage.TYPE_3BYTE_BGR || |
| 71 | + type == BufferedImage.TYPE_4BYTE_ABGR) { |
| 72 | + pixels = new int[width * height]; |
| 73 | + bi.getRGB(0, 0, width, height, pixels, 0, width); |
| 74 | + if (type == BufferedImage.TYPE_4BYTE_ABGR) { |
| 75 | + format = ARGB; |
| 76 | + } else { |
| 77 | + opaque(); |
| 78 | + } |
| 79 | + } else { |
| 80 | + DataBuffer db = bi.getRaster().getDataBuffer(); |
| 81 | + if (db instanceof DataBufferInt) { |
| 82 | + pixels = ((DataBufferInt) db).getData(); |
| 83 | + if (type == BufferedImage.TYPE_INT_ARGB) { |
| 84 | + format = ARGB; |
| 85 | + } else if (type == BufferedImage.TYPE_INT_RGB) { |
| 86 | + opaque(); |
| 87 | + } |
| 88 | + } |
| 89 | + } |
| 90 | + } |
| 91 | + // Implements fall-through if not DataBufferInt above, or not a |
| 92 | + // known type, or not DataBufferInt for the data itself. |
| 93 | + if (pixels == null) { // go the old school Java 1.0 route |
| 94 | + width = img.getWidth(null); |
| 95 | + height = img.getHeight(null); |
| 96 | + pixels = new int[width * height]; |
| 97 | + PixelGrabber pg = |
| 98 | + new PixelGrabber(img, 0, 0, width, height, pixels, 0, width); |
| 99 | + try { |
| 100 | + pg.grabPixels(); |
| 101 | + } catch (InterruptedException e) { } |
| 102 | + } |
| 103 | + pixelDensity = 1; |
| 104 | + pixelWidth = width; |
| 105 | + pixelHeight = height; |
| 106 | + } |
| 107 | + |
| 108 | + |
| 109 | + /** |
| 110 | + * Use the getNative() method instead, which allows library interfaces to be |
| 111 | + * written in a cross-platform fashion for desktop, Android, and others. |
| 112 | + * This is still included for PGraphics objects, which may need the image. |
| 113 | + */ |
| 114 | + public Image getImage() { // ignore |
| 115 | + return (Image) getNative(); |
| 116 | + } |
| 117 | + |
| 118 | + |
| 119 | + /** |
| 120 | + * Returns a native BufferedImage from this PImage. |
| 121 | + */ |
| 122 | + @Override |
| 123 | + public Object getNative() { // ignore |
| 124 | + loadPixels(); |
| 125 | + int type = (format == RGB) ? |
| 126 | + BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB; |
| 127 | + BufferedImage image = new BufferedImage(pixelWidth, pixelHeight, type); |
| 128 | + WritableRaster wr = image.getRaster(); |
| 129 | + wr.setDataElements(0, 0, pixelWidth, pixelHeight, pixels); |
| 130 | + return image; |
| 131 | + } |
| 132 | + |
| 133 | + |
| 134 | + @Override |
| 135 | + public void resize(int w, int h) { // ignore |
| 136 | + if (w <= 0 && h <= 0) { |
| 137 | + throw new IllegalArgumentException("width or height must be > 0 for resize"); |
| 138 | + } |
| 139 | + |
| 140 | + if (w == 0) { // Use height to determine relative size |
| 141 | + float diff = (float) h / (float) height; |
| 142 | + w = (int) (width * diff); |
| 143 | + } else if (h == 0) { // Use the width to determine relative size |
| 144 | + float diff = (float) w / (float) width; |
| 145 | + h = (int) (height * diff); |
| 146 | + } |
| 147 | + |
| 148 | + BufferedImage img = |
| 149 | + shrinkImage((BufferedImage) getNative(), w*pixelDensity, h*pixelDensity); |
| 150 | + |
| 151 | + PImage temp = new PImageAWT(img); |
| 152 | + this.pixelWidth = temp.width; |
| 153 | + this.pixelHeight = temp.height; |
| 154 | + |
| 155 | + // Get the resized pixel array |
| 156 | + this.pixels = temp.pixels; |
| 157 | + |
| 158 | + this.width = pixelWidth / pixelDensity; |
| 159 | + this.height = pixelHeight / pixelDensity; |
| 160 | + |
| 161 | + // Mark the pixels array as altered |
| 162 | + updatePixels(); |
| 163 | + } |
| 164 | + |
| 165 | + |
| 166 | + // Adapted from getFasterScaledInstance() method from page 111 of |
| 167 | + // "Filthy Rich Clients" by Chet Haase and Romain Guy |
| 168 | + // Additional modifications and simplifications have been added, |
| 169 | + // plus a fix to deal with an infinite loop if images are expanded. |
| 170 | + // http://code.google.com/p/processing/issues/detail?id=1463 |
| 171 | + static private BufferedImage shrinkImage(BufferedImage img, |
| 172 | + int targetWidth, int targetHeight) { |
| 173 | + int type = (img.getTransparency() == Transparency.OPAQUE) ? |
| 174 | + BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB; |
| 175 | + BufferedImage outgoing = img; |
| 176 | + BufferedImage scratchImage = null; |
| 177 | + Graphics2D g2 = null; |
| 178 | + int prevW = outgoing.getWidth(); |
| 179 | + int prevH = outgoing.getHeight(); |
| 180 | + boolean isTranslucent = img.getTransparency() != Transparency.OPAQUE; |
| 181 | + |
| 182 | + // Use multi-step technique: start with original size, then scale down in |
| 183 | + // multiple passes with drawImage() until the target size is reached |
| 184 | + int w = img.getWidth(); |
| 185 | + int h = img.getHeight(); |
| 186 | + |
| 187 | + do { |
| 188 | + if (w > targetWidth) { |
| 189 | + w /= 2; |
| 190 | + // if this is the last step, do the exact size |
| 191 | + if (w < targetWidth) { |
| 192 | + w = targetWidth; |
| 193 | + } |
| 194 | + } else if (targetWidth >= w) { |
| 195 | + w = targetWidth; |
| 196 | + } |
| 197 | + if (h > targetHeight) { |
| 198 | + h /= 2; |
| 199 | + if (h < targetHeight) { |
| 200 | + h = targetHeight; |
| 201 | + } |
| 202 | + } else if (targetHeight >= h) { |
| 203 | + h = targetHeight; |
| 204 | + } |
| 205 | + if (scratchImage == null || isTranslucent) { |
| 206 | + // Use a single scratch buffer for all iterations and then copy |
| 207 | + // to the final, correctly-sized image before returning |
| 208 | + scratchImage = new BufferedImage(w, h, type); |
| 209 | + g2 = scratchImage.createGraphics(); |
| 210 | + } |
| 211 | + g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, |
| 212 | + RenderingHints.VALUE_INTERPOLATION_BILINEAR); |
| 213 | + g2.drawImage(outgoing, 0, 0, w, h, 0, 0, prevW, prevH, null); |
| 214 | + prevW = w; |
| 215 | + prevH = h; |
| 216 | + outgoing = scratchImage; |
| 217 | + } while (w != targetWidth || h != targetHeight); |
| 218 | + g2.dispose(); |
| 219 | + |
| 220 | + |
| 221 | + // If we used a scratch buffer that is larger than our target size, |
| 222 | + // create an image of the right size and copy the results into it |
| 223 | + if (targetWidth != outgoing.getWidth() || |
| 224 | + targetHeight != outgoing.getHeight()) { |
| 225 | + scratchImage = new BufferedImage(targetWidth, targetHeight, type); |
| 226 | + g2 = scratchImage.createGraphics(); |
| 227 | + g2.drawImage(outgoing, 0, 0, null); |
| 228 | + g2.dispose(); |
| 229 | + outgoing = scratchImage; |
| 230 | + } |
| 231 | + return outgoing; |
| 232 | + } |
| 233 | + |
| 234 | + |
| 235 | + @Override |
| 236 | + protected boolean saveImpl(String filename) { |
| 237 | + if (saveImageFormats == null) { |
| 238 | + saveImageFormats = javax.imageio.ImageIO.getWriterFormatNames(); |
| 239 | + } |
| 240 | + try { |
| 241 | + if (saveImageFormats != null) { |
| 242 | + for (String saveImageFormat : saveImageFormats) { |
| 243 | + if (filename.endsWith("." + saveImageFormat)) { |
| 244 | + if (!saveImageIO(filename)) { |
| 245 | + System.err.println("Error while saving image."); |
| 246 | + return false; |
| 247 | + } |
| 248 | + return true; |
| 249 | + } |
| 250 | + } |
| 251 | + } |
| 252 | + } catch (IOException e) { |
| 253 | + } |
| 254 | + return false; |
| 255 | + } |
| 256 | + |
| 257 | + |
| 258 | + protected String[] saveImageFormats; |
| 259 | + |
| 260 | + |
| 261 | + /** |
| 262 | + * Use ImageIO functions from Java 1.4 and later to handle image save. |
| 263 | + * Various formats are supported, typically jpeg, png, bmp, and wbmp. |
| 264 | + * To get a list of the supported formats for writing, use: <BR> |
| 265 | + * <TT>println(javax.imageio.ImageIO.getReaderFormatNames())</TT> |
| 266 | + * |
| 267 | + * @path The path to which the file should be written. |
| 268 | + */ |
| 269 | + protected boolean saveImageIO(String path) throws IOException { |
| 270 | + try { |
| 271 | + int outputFormat = (format == ARGB) ? |
| 272 | + BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB; |
| 273 | + |
| 274 | + String extension = |
| 275 | + path.substring(path.lastIndexOf('.') + 1).toLowerCase(); |
| 276 | + |
| 277 | + // JPEG and BMP images that have an alpha channel set get pretty unhappy. |
| 278 | + // BMP just doesn't write, and JPEG writes it as a CMYK image. |
| 279 | + // http://code.google.com/p/processing/issues/detail?id=415 |
| 280 | + if (extension.equals("bmp") || extension.equals("jpg") || extension.equals("jpeg")) { |
| 281 | + outputFormat = BufferedImage.TYPE_INT_RGB; |
| 282 | + } |
| 283 | + |
| 284 | + BufferedImage bimage = new BufferedImage(pixelWidth, pixelHeight, outputFormat); |
| 285 | + bimage.setRGB(0, 0, pixelWidth, pixelHeight, pixels, 0, pixelWidth); |
| 286 | + |
| 287 | + File file = new File(path); |
| 288 | + |
| 289 | + ImageWriter writer = null; |
| 290 | + ImageWriteParam param = null; |
| 291 | + IIOMetadata metadata = null; |
| 292 | + |
| 293 | + if (extension.equals("jpg") || extension.equals("jpeg")) { |
| 294 | + if ((writer = imageioWriter("jpeg")) != null) { |
| 295 | + // Set JPEG quality to 90% with baseline optimization. Setting this |
| 296 | + // to 1 was a huge jump (about triple the size), so this seems good. |
| 297 | + // Oddly, a smaller file size than Photoshop at 90%, but I suppose |
| 298 | + // it's a completely different algorithm. |
| 299 | + param = writer.getDefaultWriteParam(); |
| 300 | + param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); |
| 301 | + param.setCompressionQuality(0.9f); |
| 302 | + } |
| 303 | + } |
| 304 | + |
| 305 | + if (extension.equals("png")) { |
| 306 | + if ((writer = imageioWriter("png")) != null) { |
| 307 | + param = writer.getDefaultWriteParam(); |
| 308 | + if (false) { |
| 309 | + metadata = imageioDPI(writer, param, 100); |
| 310 | + } |
| 311 | + } |
| 312 | + } |
| 313 | + |
| 314 | + if (writer != null) { |
| 315 | + try (BufferedOutputStream output = new BufferedOutputStream(PApplet.createOutput(file))) { |
| 316 | + writer.setOutput(ImageIO.createImageOutputStream(output)); |
| 317 | +// writer.write(null, new IIOImage(bimage, null, null), param); |
| 318 | + writer.write(metadata, new IIOImage(bimage, null, metadata), param); |
| 319 | + writer.dispose(); |
| 320 | + |
| 321 | + output.flush(); |
| 322 | + } |
| 323 | + return true; |
| 324 | + } |
| 325 | + // If iter.hasNext() somehow fails up top, it falls through to here |
| 326 | + return javax.imageio.ImageIO.write(bimage, extension, file); |
| 327 | + |
| 328 | + } catch (IOException e) { |
| 329 | + throw new IOException("image save failed."); |
| 330 | + } |
| 331 | + } |
| 332 | + |
| 333 | + |
| 334 | + private ImageWriter imageioWriter(String extension) { |
| 335 | + Iterator<ImageWriter> iter = ImageIO.getImageWritersByFormatName(extension); |
| 336 | + if (iter.hasNext()) { |
| 337 | + return iter.next(); |
| 338 | + } |
| 339 | + return null; |
| 340 | + } |
| 341 | + |
| 342 | + |
| 343 | + private IIOMetadata imageioDPI(ImageWriter writer, ImageWriteParam param, double dpi) { |
| 344 | + // http://stackoverflow.com/questions/321736/how-to-set-dpi-information-in-an-image |
| 345 | + ImageTypeSpecifier typeSpecifier = |
| 346 | + ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_RGB); |
| 347 | + IIOMetadata metadata = |
| 348 | + writer.getDefaultImageMetadata(typeSpecifier, param); |
| 349 | + |
| 350 | + if (!metadata.isReadOnly() && metadata.isStandardMetadataFormatSupported()) { |
| 351 | + // for PNG, it's dots per millimeter |
| 352 | + double dotsPerMilli = dpi / 25.4; |
| 353 | + |
| 354 | + IIOMetadataNode horiz = new IIOMetadataNode("HorizontalPixelSize"); |
| 355 | + horiz.setAttribute("value", Double.toString(dotsPerMilli)); |
| 356 | + |
| 357 | + IIOMetadataNode vert = new IIOMetadataNode("VerticalPixelSize"); |
| 358 | + vert.setAttribute("value", Double.toString(dotsPerMilli)); |
| 359 | + |
| 360 | + IIOMetadataNode dim = new IIOMetadataNode("Dimension"); |
| 361 | + dim.appendChild(horiz); |
| 362 | + dim.appendChild(vert); |
| 363 | + |
| 364 | + IIOMetadataNode root = new IIOMetadataNode("javax_imageio_1.0"); |
| 365 | + root.appendChild(dim); |
| 366 | + |
| 367 | + try { |
| 368 | + metadata.mergeTree("javax_imageio_1.0", root); |
| 369 | + return metadata; |
| 370 | + |
| 371 | + } catch (IIOInvalidTreeException e) { |
| 372 | + System.err.println("Could not set the DPI of the output image"); |
| 373 | + } |
| 374 | + } |
| 375 | + return null; |
| 376 | + } |
| 377 | +} |
0 commit comments