001/** 002 * Copyright (C) 2014 Universidade de Aveiro, DETI/IEETA, Bioinformatics Group - http://bioinformatics.ua.pt/ 003 * 004 * This file is part of Dicoogle/dicoogle. 005 * 006 * Dicoogle/dicoogle is free software: you can redistribute it and/or modify 007 * it under the terms of the GNU General Public License as published by 008 * the Free Software Foundation, either version 3 of the License, or 009 * (at your option) any later version. 010 * 011 * Dicoogle/dicoogle is distributed in the hope that it will be useful, 012 * but WITHOUT ANY WARRANTY; without even the implied warranty of 013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 014 * GNU General Public License for more details. 015 * 016 * You should have received a copy of the GNU General Public License 017 * along with Dicoogle. If not, see <http://www.gnu.org/licenses/>. 018 */ 019package pt.ua.dicoogle.server.web.dicom; 020 021import javax.imageio.ImageIO; 022import javax.imageio.ImageReader; 023import javax.imageio.ImageWriter; 024import javax.imageio.ImageWriteParam; 025import javax.imageio.stream.ImageInputStream; 026import javax.imageio.stream.ImageOutputStream; 027 028import org.dcm4che2.imageio.plugins.dcm.DicomImageReadParam; 029 030import pt.ua.dicoogle.sdk.StorageInputStream; 031 032import java.awt.Image; 033import java.awt.image.BufferedImage; 034import java.io.IOException; 035import java.io.ByteArrayOutputStream; 036import java.io.InputStream; 037import java.util.Iterator; 038import org.slf4j.Logger; 039import org.slf4j.LoggerFactory; 040import pt.ua.dicoogle.server.web.utils.ImageLoader; 041 042/** 043 * Handles conversion between the images (formats) found inside a DICOM file 044 * and the PNG image format, for proper lossless web view. 045 * 046 * @author António Novo <antonio.novo@ua.pt> 047 * @author Eduardo Pinho <eduardopinho@ua.pt> 048 */ 049public class Convert2PNG 050{ 051 private static final Logger logger = LoggerFactory.getLogger(Convert2PNG.class); 052 053 private synchronized static ImageReader createDICOMImageReader() { 054 Iterator<ImageReader> it = ImageIO.getImageReadersByFormatName("DICOM"); // gets the first registered ImageReader that can read DICOM data 055 056 ImageReader sReader = it.next(); 057 return sReader; 058 } 059 060 private synchronized static ImageWriter createPNGImageWriter() { 061 Iterator<ImageWriter> it = ImageIO.getImageWritersByFormatName("PNG"); // gets the first registered ImageWriter that can write PNG data 062 063 ImageWriter sWriter = it.next(); 064 return sWriter; 065 } 066 067 // Transformations that can be applied on each conversion. 068 /** 069 * No transform applied. 070 */ 071 @Deprecated 072 public static final int TRANSFORM_NONE = 0; 073 /** 074 * Scale image based on a width target. 075 */ 076 @Deprecated 077 public static final int TRANSFORM_SCALE_BY_WIDTH = 1; 078 /** 079 * Scale image based on a height target. 080 */ 081 @Deprecated 082 public static final int TRANSFORM_SCALE_BY_HEIGHT = 2; 083 /** 084 * Scale image based on a percentage value. 085 */ 086 @Deprecated 087 public static final int TRANSFORM_SCALE_BY_PERCENT = 3; 088 089 /** 090 * Reads an input DICOM file, applies a transformation to it if any, and returns 091 * the desired frame as a PNG-encoded memory stream. 092 * 093 * @param dcmStream The Dicoogle storage input Stream for the DICOM File. 094 * @param frameIndex the index of the frame wanted (zero based). 095 * @param transformType the transform to execute. 096 * @param transformParam1 the param used on width and height based scales. 097 * @param transformParam2 the param used on percentage based scales. 098 * @return the frame encoded in a PNG memory stream. 099 */ 100 @Deprecated 101 public static ByteArrayOutputStream DICOM2PNGStream(StorageInputStream dcmStream, int frameIndex, int transformType, int transformParam1, float transformParam2) 102 { 103 // setup the DICOM reader 104 ImageReader reader = createDICOMImageReader(); 105 if (reader == null) // if no valid reader was found abort 106 return null; 107 108 DicomImageReadParam readParams = (DicomImageReadParam) reader.getDefaultReadParam(); // and set the default params for it (height, weight, alpha) // FIXME is this even needed? 109 110 // setup the PNG writer 111 ImageWriter writer = createPNGImageWriter(); // gets the first registered ImageWriter that can save PNG data 112 if (writer == null) // if no valid writer was found abort 113 return null; 114 115 ImageWriteParam writeParams = writer.getDefaultWriteParam(); // and set the default params for it (height, weight, alpha) 116// writeParams.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); // activate PNG compression to save on bandwidth 117// writeParams.setCompressionType("Deflater"); // overall best compressor for black and white pictures 118// writeParams.setCompressionQuality(0.0F); // best compression ratio (but longer [de]compression time) 119// NOTE the above compression params are not currently needed because ImageIO already automatically compresses PNG by default, it's depicted on http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4829970 as a bug but it's more of a "can't set the compression level" problem as it uses the Deflater.BEST_COMPRESSION as default (see line 144 of com.sun.imageio.plugins.png.PNGImageWriter), the code remains comment so if in the future ImageIO allows for compression level set we can set it to the best compression level again 120 writeParams.setProgressiveMode(ImageWriteParam.MODE_DEFAULT); // activate progressive mode (adam7), best for low bandwidth connections 121 122 try 123 { 124 ImageInputStream inStream = ImageIO.createImageInputStream(dcmStream.getInputStream()); 125 126 reader.setInput(inStream); 127 128 // make sure that we will read a frame within bounds 129 int frameCount = reader.getNumImages(true); // get the number of avalable frame in the DICOM file 130 if ((frameCount < 1) || (frameIndex < 0) || (frameIndex >= frameCount)) // if teither the file has no frames or the frame wanted is outside of bounds abort 131 return null; 132 133 // read the specified frame from the file 134 BufferedImage image = reader.read(frameIndex, readParams); 135 136 inStream.close(); 137 138 139 // if no frame was read abort 140 if (image == null) 141 return null; 142 143 // if there is a transform to be applied to the image, this is the right place to do it 144 switch (transformType) 145 { 146 case TRANSFORM_SCALE_BY_WIDTH: 147 image = scaleImageByWidth(image, transformParam1); 148 break; 149 case TRANSFORM_SCALE_BY_HEIGHT: 150 image = scaleImageByHeight(image, transformParam1); 151 break; 152 case TRANSFORM_SCALE_BY_PERCENT: 153 image = scaleImageByPercent(image, transformParam2); 154 break; 155 } 156 // mount the resulting memory stream 157 ByteArrayOutputStream result = new ByteArrayOutputStream(); 158 159 // write the specified frame to the resulting stream 160 ImageOutputStream outStream = ImageIO.createImageOutputStream(result); 161 writer.setOutput(outStream); 162 writer.write(image); 163 outStream.close(); 164 165 return result; 166 } 167 catch (IOException e) 168 { 169 System.out.println("\nError: couldn't read dicom image!" + e.getMessage()); 170 return null; 171 } 172 } 173 174 /** 175 * Reads an input DICOM file and returns the desired frame as a PNG-encoded memory stream. 176 * 177 * @param dcmStream The Dicoogle storage input Stream for the DICOM File. 178 * @param frameIndex the index of the frame wanted (starting with #0). 179 * @return the frame encoded in a PNG memory stream. 180 * @throws IOException if the I/O operations on the images fail 181 */ 182 public static ByteArrayOutputStream DICOM2PNGStream(StorageInputStream dcmStream, int frameIndex) throws IOException { 183 return DICOM2PNGStream(dcmStream.getInputStream(), frameIndex); 184 } 185 186 /** 187 * Reads an input DICOM file and returns the desired frame as a PNG-encoded memory stream. 188 * 189 * @param iStream an input stream for the DICOM File. 190 * @param frameIndex the index of the frame wanted (starting with #0). 191 * @return the frame encoded in a PNG memory stream. 192 * @throws IOException if the I/O operations on the images fail 193 */ 194 public static ByteArrayOutputStream DICOM2PNGStream(InputStream iStream, int frameIndex) throws IOException { 195 // setup the PNG writer 196 ImageWriter writer = createPNGImageWriter(); 197 198 ImageWriteParam writeParams = writer.getDefaultWriteParam(); // and set the default params for it (height, weight, alpha) 199 writeParams.setProgressiveMode(ImageWriteParam.MODE_DEFAULT); // activate progressive mode (adam7), best for low bandwidth connections 200 201 BufferedImage image = ImageLoader.loadImage(iStream); 202 203 // mount the resulting memory stream 204 ByteArrayOutputStream result = new ByteArrayOutputStream(); 205 206 // write the specified frame to the resulting stream 207 try (ImageOutputStream outStream = ImageIO.createImageOutputStream(result)) { 208 writer.setOutput(outStream); 209 writer.write(image); 210 } 211 212 return result; 213 } 214 215 /** 216 * Reads an input DICOM file, scales it to fit in the given dimensions and returns the 217 * desired frame as a PNG-encoded memory stream. 218 * 219 * @param inStream The Dicoogle storage input Stream for the DICOM File. 220 * @param frameIndex the index of the frame wanted (starting with #0). 221 * @param width the maximum width of the resulting image 222 * @param height the maximum height of the resulting image 223 * @return the frame encoded in a PNG memory stream. 224 * @throws IOException if the I/O operations on the images fail 225 */ 226 public static ByteArrayOutputStream DICOM2ScaledPNGStream(InputStream inStream, int frameIndex, int width, int height) throws IOException { 227 if (inStream == null) { 228 throw new NullPointerException("dcmStream"); 229 } 230 if (frameIndex < 0) { 231 throw new IllegalArgumentException("bad frameIndex"); 232 } 233 if (width <= 0) { 234 throw new IllegalArgumentException("bad width"); 235 } 236 if (height <= 0) { 237 throw new IllegalArgumentException("bad height"); 238 } 239 240 // setup the PNG writer 241 BufferedImage image = ImageLoader.loadImage(inStream); 242 243 image = scaleImage(image, width, height); 244 245 // mount the resulting memory stream 246 ByteArrayOutputStream result = new ByteArrayOutputStream(); 247 248 // write the specified frame to the resulting stream 249 try (ImageOutputStream outStream = ImageIO.createImageOutputStream(result)) { 250 ImageWriter writer = createPNGImageWriter(); 251 ImageWriteParam writeParams = writer.getDefaultWriteParam(); // and set the default params for it 252 writeParams.setProgressiveMode(ImageWriteParam.MODE_DEFAULT); // activate progressive mode (adam7), best for low bandwidth connections 253 writer.setOutput(outStream); 254 writer.write(image); 255 } 256 257 return result; 258 } 259 260 /** 261 * Reads an input DICOM file, scales it to fit in the given dimensions and returns the 262 * desired frame as a PNG-encoded memory stream. 263 * 264 * @param dcmStream The Dicoogle storage input Stream for the DICOM File. 265 * @param frameIndex the index of the frame wanted (starting with #0). 266 * @param width the maximum width of the resulting image 267 * @param height the maximum height of the resulting image 268 * @return the frame encoded in a PNG memory stream. 269 * @throws IOException if the I/O operations on the images fail 270 */ 271 public static ByteArrayOutputStream DICOM2ScaledPNGStream(StorageInputStream dcmStream, int frameIndex, int width, int height) throws IOException { 272 return DICOM2ScaledPNGStream(dcmStream.getInputStream(), frameIndex, width, height); 273 } 274 275 /** 276 * Retrieve the number of frames from an input DICOM file. 277 * 278 * @param dcmFile The Dicoogle storage input Stream for the DICOM File. 279 * @return an integer representing the number of frames in the image, 280 * or <tt>-1</tt> if the operation fails 281 */ 282 public static int getNumberOfFrames(StorageInputStream dcmFile) 283 { 284 // setup the DICOM reader 285 ImageReader reader = createDICOMImageReader(); 286 287 try (ImageInputStream inStream = ImageIO.createImageInputStream(dcmFile.getInputStream())) { 288 289 reader.setInput(inStream); 290 291 // make sure that we will read a frame within bounds 292 int frameCount = reader.getNumImages(true); 293 294 return frameCount; 295 296 } catch (IOException e) { 297 logger.error("Failed to read DICOM image", e); 298 } 299 return -1; 300 } 301 302 /** 303 * Resize a BufferedImage to the specified width, preserving the original image aspect ratio. 304 * 305 * @param image the original image to scale. 306 * @param width the target width. 307 * @return a BufferedImage scaled to the target width. 308 */ 309 public static BufferedImage scaleImageByWidth(BufferedImage image, int width) 310 { 311 if (width <= 0) 312 return image; 313 314 Image scaledImage = image.getScaledInstance(width, -1, Image.SCALE_SMOOTH); // scale the input, smooth scaled 315 BufferedImage result = new BufferedImage(scaledImage.getWidth(null), scaledImage.getHeight(null), BufferedImage.TYPE_INT_RGB); // create the output image 316 317 result.getGraphics().drawImage(scaledImage, 0, 0, null); // draw the scaled image onto the output 318 319 return result; 320 } 321 322 /** 323 * Resize a BufferedImage to the specified height, preserving the original image aspect ratio. 324 * 325 * @param image the original image to scale. 326 * @param height the target height. 327 * @return a BufferedImage scaled to the target height. 328 */ 329 public static BufferedImage scaleImageByHeight(BufferedImage image, int height) 330 { 331 if (height <= 0) 332 return image; 333 334 Image scaledImage = image.getScaledInstance(-1, height, Image.SCALE_SMOOTH); // scale the input, smooth scaled 335 BufferedImage result = new BufferedImage(scaledImage.getWidth(null), scaledImage.getHeight(null), BufferedImage.TYPE_INT_RGB); // create the output image 336 result.getGraphics().drawImage(scaledImage, 0, 0, null); // draw the scaled image onto the output 337 338 return result; 339 } 340 341 /** 342 * Resize a BufferedImage based on the specified percentage, preserving the original image aspect ratio. 343 * 344 * @param image the original image to scale. 345 * @param percent the percentage to scale to, 1.0 is original, 0.5 is half, 2.0 is double, etc. 346 * @return a BufferedImage scaled to the target percentage. 347 */ 348 public static BufferedImage scaleImageByPercent(BufferedImage image, float percent) 349 { 350 if ((percent <= 0.0F) || (percent == 1.0F)) // if the scale is either null, negative or none at all return the original image 351 return image; 352 353 Image scaledImage = image.getScaledInstance((int) (percent * image.getWidth()), -1, Image.SCALE_SMOOTH); // scale the input, smooth scaled 354 BufferedImage result = new BufferedImage(scaledImage.getWidth(null), scaledImage.getHeight(null), BufferedImage.TYPE_INT_RGB); // create the output image 355 356 result.getGraphics().drawImage(scaledImage, 0, 0, null); // draw the scaled image onto the output 357 358 return result; 359 } 360 361 /** 362 * Resize a BufferedImage to fit the specified dimensions, while preserving the original image aspect ratio. 363 * The image scaling procedure will guarantee that at least one of the dimensions will be equal to 364 * the maximum target. 365 * 366 * @param image the original image to scale. 367 * @param width the maximum width of the target 368 * @param height the maximum height of the target 369 * @return a copy of the image, scaled to fit into the given dimensions 370 * @throws IllegalArgumentException on bad width and height dimensions 371 * @throws NullPointerException on null image 372 */ 373 public static BufferedImage scaleImage(BufferedImage image, int width, int height) 374 { 375 if (image == null) { 376 throw new NullPointerException("image is null"); 377 } 378 if (width <= 0) { 379 throw new IllegalArgumentException("illegal width dimension: " + width); 380 } 381 if (height <= 0) { 382 throw new IllegalArgumentException("illegal height dimension: " + height); 383 } 384 385 if (width < height) { 386 return scaleImageByHeight(image, height); 387 } else { 388 return scaleImageByWidth(image, width); 389 } 390 } 391}