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}