/* * *** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is part of dcm4che, an implementation of DICOM(TM) in * Java(TM), hosted at https://github.com/gunterze/dcm4che. * * The Initial Developer of the Original Code is * J4Care. * Portions created by the Initial Developer are Copyright (C) 2015 * the Initial Developer. All Rights Reserved. * * Contributor(s): * See @authors listed below * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * *** END LICENSE BLOCK ***** */ package org.dcm4chee.archive.wado; import org.dcm4che3.data.Attributes; import org.dcm4che3.data.Tag; import org.dcm4che3.image.BufferedImageUtils; import org.dcm4che3.image.PixelAspectRatio; import org.dcm4che3.imageio.codec.ImageParams; import org.dcm4che3.imageio.codec.TransferSyntaxType; import org.dcm4che3.imageio.plugins.dcm.DicomImageReadParam; import org.dcm4che3.imageio.plugins.dcm.DicomMetaData; import javax.imageio.IIOImage; import javax.imageio.ImageReader; import javax.imageio.ImageTypeSpecifier; import javax.imageio.ImageWriteParam; import javax.imageio.ImageWriter; import javax.imageio.metadata.IIOMetadata; import javax.imageio.metadata.IIOMetadataNode; import javax.imageio.stream.ImageOutputStream; import javax.imageio.stream.MemoryCacheImageOutputStream; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.StreamingOutput; import java.awt.geom.AffineTransform; import java.awt.geom.Point2D; import java.awt.image.AffineTransformOp; import java.awt.image.BufferedImage; import java.io.IOException; import java.io.OutputStream; /** * Streams a rendered image (e.g. JPEG/PNG/GIF) or a sequence of rendered images (animated GIF). * * @author Gunter Zeilinger <gunterze@gmail.com> * @author Hermann Czedik-Eysenberg <hermann-agfa@czedik.net> * @since Aug 2015 */ public class RenderedImageOutput implements StreamingOutput { private static final float DEF_FRAME_TIME = 1000.f; private static final byte[] LOOP_FOREVER = {1, 0, 0}; private final ImageReader reader; private final DicomImageReadParam readParam; private final int rows; private final int columns; private final int imageIndex; private final ImageWriter writer; private final ImageWriteParam writeParam; public RenderedImageOutput(ImageReader reader, DicomImageReadParam readParam, int rows, int columns, int imageIndex, ImageWriter writer, ImageWriteParam writeParam) { this.reader = reader; this.readParam = readParam; this.rows = rows; this.columns = columns; this.imageIndex = imageIndex; this.writer = writer; this.writeParam = writeParam; } @Override public void write(OutputStream out) throws IOException, WebApplicationException { // Note: when changing the logic here, please also consider the getEstimatedNeededMemory() method ImageOutputStream imageOut = null; try { imageOut = new MemoryCacheImageOutputStream(out); writer.setOutput(imageOut); if (imageIndex < 0) { IIOMetadata metadata = null; int numImages = reader.getNumImages(false); writer.prepareWriteSequence(null); BufferedImage bi = null; for (int i = 0; i < numImages; i++) { readParam.setDestination(bi); bi = reader.read(i, readParam); BufferedImage adjustedBi = adjust(bi); if (metadata == null) metadata = createAnimatedGIFMetadata(adjustedBi, writeParam, frameTime()); writer.writeToSequence( new IIOImage(adjustedBi, null, metadata), writeParam); imageOut.flushBefore(imageOut.length()); } writer.endWriteSequence(); } else { BufferedImage bi = reader.read(imageIndex, readParam); bi = adjust(bi); writer.write(null, new IIOImage(bi, null, null), writeParam); } } finally { writer.dispose(); reader.dispose(); if (imageOut != null) imageOut.close(); } } private float frameTime() throws IOException { DicomMetaData metaData = getStreamMetadata(); Attributes attrs = metaData.getAttributes(); return attrs.getFloat(Tag.FrameTime, DEF_FRAME_TIME); } private DicomMetaData getStreamMetadata() throws IOException { return (DicomMetaData) reader.getStreamMetadata(); } private IIOMetadata createAnimatedGIFMetadata(BufferedImage bi, ImageWriteParam param, float frameTime) throws IOException { ImageTypeSpecifier imageType = ImageTypeSpecifier.createFromRenderedImage(bi); IIOMetadata metadata = writer.getDefaultImageMetadata(imageType, param); String formatName = metadata.getNativeMetadataFormatName(); IIOMetadataNode root = (IIOMetadataNode) metadata.getAsTree(formatName); IIOMetadataNode graphicControlExt = (IIOMetadataNode) root.getElementsByTagName("GraphicControlExtension").item(0); graphicControlExt.setAttribute("delayTime", Integer.toString(Math.round(frameTime() / 10))); IIOMetadataNode appExts = new IIOMetadataNode("ApplicationExtensions"); IIOMetadataNode appExt = new IIOMetadataNode("ApplicationExtension"); appExt.setAttribute("applicationID", "NETSCAPE"); appExt.setAttribute("authenticationCode", "2.0"); appExt.setUserObject(LOOP_FOREVER); appExts.appendChild(appExt); root.appendChild(appExts); metadata.setFromTree(formatName, root); return metadata; } private BufferedImage adjust(BufferedImage bi) throws IOException { if (bi.getColorModel().getNumComponents() == 3) bi = BufferedImageUtils.convertToIntRGB(bi); return rescale(bi); } private BufferedImage rescale(BufferedImage bi) throws IOException { if (!needsRescaling()) return bi; Point2D.Double scalingFactors = getScalingFactors(bi.getHeight(), bi.getWidth()); AffineTransformOp op = new AffineTransformOp( AffineTransform.getScaleInstance(scalingFactors.getX(), scalingFactors.getY()), AffineTransformOp.TYPE_NEAREST_NEIGHBOR); return op.filter(bi, null); } private Point2D.Double getScalingFactors(int origRows, int origColumns) throws IOException { int r = rows; int c = columns; float sy = getPixelAspectRatio(); float sx = 1f; if (r != 0 || c != 0) { if (r != 0 && c != 0) if (r * origColumns > c * origRows * sy) r = 0; else c = 0; sx = r != 0 ? r / (origRows * sy) : c / (float) origColumns; sy *= sx; } return new Point2D.Double(sx, sy); } private boolean needsRescaling() throws IOException { return rows != 0 || columns != 0 || getPixelAspectRatio() != 1f; } private float getPixelAspectRatio() throws IOException { Attributes prAttrs = readParam.getPresentationState(); return prAttrs != null ? PixelAspectRatio.forPresentationState(prAttrs) : PixelAspectRatio.forImage(getAttributes()); } private Attributes getAttributes() throws IOException { return getStreamMetadata().getAttributes(); } /** * @return (Pessimistic) estimation of the heap memory (in bytes) that will be needed at any moment in time during * decompression, rendering and compression. */ public long getEstimatedNeededMemory() throws IOException { DicomMetaData dicomMetaData = getStreamMetadata(); Attributes attributes = dicomMetaData.getAttributes(); ImageParams imageParams = new ImageParams(attributes); long uncompressedFrameLength = imageParams.getFrameLength(); long memoryNeededForDecompression = 0; Attributes fmi = dicomMetaData.getFileMetaInformation(); if (fmi != null && TransferSyntaxType.forUID(fmi.getString(Tag.TransferSyntaxUID)) != TransferSyntaxType.NATIVE) { // Memory needed for reading one compressed frame // (For now: pessimistic assumption that same memory as for the uncompressed frame is needed. This very much // depends on the compression algorithm and properties.) // Actually it might be much less, if the decompressor supports streaming in the compressed data. long sourceCompressedFrameLength = uncompressedFrameLength; memoryNeededForDecompression += sourceCompressedFrameLength; } // size for intermediate un/decompressed buffered image memoryNeededForDecompression += uncompressedFrameLength; long memoryNeededForApplyingLUT = 0; memoryNeededForApplyingLUT += uncompressedFrameLength; long readImageLength; // in most cases (if it is a monochrome image) the DicomImageReader will apply a LUT if (imageParams.getPhotometricInterpretation().isMonochrome()) { // the resulting buffered image has one byte per sample (see DicomImageReader.read) long uncompressedFrameAppliedLUTLength = (long)imageParams.getRows() * imageParams.getColumns(); // Note: in some cases (if it is already 8-bit) the raster of the original uncompressed image will also be // re-used for the version with the applied LUT. This is not (yet) considered here. memoryNeededForApplyingLUT += uncompressedFrameAppliedLUTLength; // the final read image is the one is the applied lut readImageLength = uncompressedFrameAppliedLUTLength; } else { readImageLength = uncompressedFrameLength; // no LUT to apply } // Note: additional memory needed for applying overlays is currently not considered long memoryNeededForReading = Math.max(memoryNeededForDecompression, memoryNeededForApplyingLUT); long memoryNeededForAdjustingAndCompression = 0; memoryNeededForAdjustingAndCompression += readImageLength; long sizeOfColorAdjustedImage; // for color images in some cases a new INT-RGB buffered image is allocated (BufferedImageUtils.convertToIntRGB()) boolean needsColorConversion = attributes.getInt(Tag.SamplesPerPixel, 0) == 3; if (needsColorConversion) { sizeOfColorAdjustedImage = (long)imageParams.getRows() * imageParams.getColumns() * 4; // int has 4 bytes memoryNeededForAdjustingAndCompression += sizeOfColorAdjustedImage; } else { sizeOfColorAdjustedImage = readImageLength; // no color conversion } long rescaledImageSize; // rescaling is sometimes required if (needsRescaling()) { Point2D.Double scalingFactors = getScalingFactors(imageParams.getRows(), imageParams.getColumns()); rescaledImageSize = Math.round(Math.ceil(sizeOfColorAdjustedImage * scalingFactors.getX() * scalingFactors.getY())); memoryNeededForAdjustingAndCompression += rescaledImageSize; } else { rescaledImageSize = sizeOfColorAdjustedImage; // no rescaling } // memory for a resulting compressed frame // (For now: pessimistic assumption that same memory as for the uncompressed frame is needed. This very much // depends on the compression algorithm and properties.) // Actually it might be much less, if the decompressor supports streaming out the compressed data. long compressedFrameLength = rescaledImageSize; memoryNeededForAdjustingAndCompression += compressedFrameLength; // reading and adjusting/compression happen sequentially (in between GC can run), // therefore we have to consider the maximum of the two return Math.max(memoryNeededForReading, memoryNeededForAdjustingAndCompression); } }