/* * **** 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/dcm4che. * * The Initial Developer of the Original Code is Agfa Healthcare. * Portions created by the Initial Developer are Copyright (C) 2011-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.dcm4che3.imageio.codec; import org.dcm4che3.data.Attributes; import org.dcm4che3.data.Tag; import org.dcm4che3.data.VR; import org.dcm4che3.image.Overlays; import org.dcm4che3.imageio.codec.jpeg.PatchJPEGLS; import org.dcm4che3.imageio.codec.jpeg.PatchJPEGLSImageOutputStream; import org.dcm4che3.io.DicomInputStream; import org.dcm4che3.io.DicomOutputStream; import org.dcm4che3.util.ByteUtils; import org.dcm4che3.util.Property; import org.dcm4che3.util.StreamUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.imageio.*; import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.MemoryCacheImageOutputStream; import java.awt.image.*; import java.io.IOException; /** * @author Gunter Zeilinger <gunterze@gmail.com> * @since Feb 2015. * * @deprecated This is prototype code. StreamCompressor will be replaced by a Transcoder that supports both stream * compression and decompression. For now you can continue using {@link Compressor} for non-stream compression. */ @Deprecated public class StreamCompressor extends StreamDecompressor { private static final Logger LOG = LoggerFactory.getLogger(StreamCompressor.class); private TransferSyntaxType compressTsType; private ImageWriter compressor; private PatchJPEGLS compressPatchJPEGLS; private ImageWriteParam compressParam; private int maxPixelValueError = -1; private int avgPixelValueBlockSize = 1; private ImageReader verifier; private ImageReadParam verifyParam; private ImageParams imageParams; private BufferedImage bi2; private int frameIndex; public StreamCompressor(DicomInputStream in, String inTransferSyntaxUID, DicomOutputStream out) { super(in, inTransferSyntaxUID, out); } public boolean compress(String compressTsuid, Property... params) throws IOException { if (compressTsuid == null) throw new NullPointerException("compressTsuid"); this.compressTsType = TransferSyntaxType.forUID(compressTsuid); if (compressTsType == null) throw new IllegalArgumentException("Unknown Transfer Syntax: " + compressTsuid); ImageWriterFactory.ImageWriterParam param = ImageWriterFactory.getImageWriterParam(compressTsuid); if (param == null) throw new UnsupportedOperationException( "Unsupported Transfer Syntax: " + compressTsuid); this.compressor = ImageWriterFactory.getImageWriter(param); LOG.debug("Compressor: {}", compressor.getClass().getName()); this.compressPatchJPEGLS = param.patchJPEGLS; this.compressParam = compressor.getDefaultWriteParam(); int count = 0; for (Property property : cat(param.getImageWriteParams(), params)) { String name = property.getName(); if (name.equals("maxPixelValueError")) this.maxPixelValueError = ((Number) property.getValue()).intValue(); else if (name.equals("avgPixelValueBlockSize")) this.avgPixelValueBlockSize = ((Number) property.getValue()).intValue(); else { if (count++ == 0) compressParam.setCompressionMode( ImageWriteParam.MODE_EXPLICIT); property.setAt(compressParam); } } if (maxPixelValueError >= 0) { ImageReaderFactory.ImageReaderParam readerParam = ImageReaderFactory.getImageReaderParam(compressTsuid); if (readerParam == null) throw new UnsupportedOperationException( "Unsupported Transfer Syntax: " + compressTsuid); this.verifier = ImageReaderFactory.getImageReader(readerParam); this.verifyParam = verifier.getDefaultReadParam(); LOG.debug("Verifier: {}", verifier.getClass().getName()); } decompress(); return pixeldataProcessed; } @Override public void dispose() { super.dispose(); if (compressor != null) compressor.dispose(); if (verifier != null) verifier.dispose(); } private Property[] cat(Property[] a, Property[] b) { if (a.length == 0) return b; if (b.length == 0) return a; Property[] c = new Property[a.length + b.length]; System.arraycopy(a, 0, c, 0, a.length); System.arraycopy(b, 0, c, a.length, b.length); return c; } @Override protected void onPixelData(DicomInputStream dis, Attributes attrs) throws IOException { int tag = dis.tag(); VR vr = dis.vr(); int len = dis.length(); BufferedImage bi = null; this.imageParams = new ImageParams(dataset); if (decompressor != null) imageParams.decompress(attrs, tsType); if (decompressor == null || tsType == TransferSyntaxType.RLE) bi = BufferedImageUtils.createBufferedImage(imageParams, compressTsType); imageParams.compress(attrs, compressTsType); coerceAttributes.coerce(attrs).writeTo(out); attrs.clear(); out.writeHeader(Tag.PixelData, VR.OB, -1); out.writeHeader(Tag.Item, null, 0); if (len == -1) { if (!tsType.isPixeldataEncapsulated()) { throw new IOException("Unexpected encapsulated Pixel Data"); } decompressFrames(dis, imageParams, bi); } else { if (tsType.isPixeldataEncapsulated()) throw new IOException("Pixel Data not encapsulated"); int padding = len - imageParams.getLength(); if (padding < 0) throw new IllegalArgumentException( "Pixel data too short: " + len + " instead " + imageParams.getLength() + " bytes"); byte[] buf = null; DataBuffer dataBuffer = bi.getRaster().getDataBuffer(); if (dataBuffer.getDataType() != DataBuffer.TYPE_BYTE) buf = new byte[imageParams.getFrameLength()]; for (int i = 0; i < imageParams.getFrames(); i++) { readFrame(dis, dataBuffer, buf); writeFrame(bi); } dis.skipFully(padding); } out.writeHeader(Tag.SequenceDelimitationItem, null, 0); pixeldataProcessed = true; } private void readFrame(DicomInputStream dis, DataBuffer db, byte[] buf) throws IOException { switch (db.getDataType()) { case DataBuffer.TYPE_BYTE: byte[][] data = ((DataBufferByte) db).getBankData(); for (byte[] bs : data) dis.readFully(bs); if (dis.bigEndian() && dis.vr() == VR.OW) ByteUtils.swapShorts(data); break; case DataBuffer.TYPE_USHORT: dis.readFully(buf); ByteUtils.bytesToShorts(buf, 0, ((DataBufferUShort) db).getData(), 0, buf.length >> 1, dis.bigEndian()); break; case DataBuffer.TYPE_SHORT: dis.readFully(buf); ByteUtils.bytesToShorts(buf, 0, ((DataBufferShort) db).getData(), 0, buf.length >> 1, dis.bigEndian()); break; default: throw new UnsupportedOperationException("Unsupported Datatype: " + db.getDataType()); } } @Override protected void writeFrame(BufferedImage bi) throws IOException { if (imageParams.getBitsStored() < imageParams.getBitsAllocated()) BufferedImageUtils.nullifyUnusedBits(imageParams.getBitsStored(), bi.getRaster().getDataBuffer()); MemoryCacheImageOutputStream compressedFrame = new MemoryCacheImageOutputStream(out) { @Override public void flush() throws IOException { // defer flush to close() LOG.debug("Ignore invoke of MemoryCacheImageOutputStream.flush()"); } }; compressor.setOutput(compressPatchJPEGLS != null ? new PatchJPEGLSImageOutputStream(compressedFrame, compressPatchJPEGLS) : compressedFrame); long start = System.currentTimeMillis(); compressor.write(null, new IIOImage(bi, null, null), compressParam); long end = System.currentTimeMillis(); int streamLength = (int) compressedFrame.getStreamPosition(); if (LOG.isDebugEnabled()) LOG.debug("Compressed frame #{} {}:1 in {} ms", frameIndex+1, (float) BufferedImageUtils.sizeOf(bi) / streamLength, end - start); verify(compressedFrame, bi); out.writeHeader(Tag.Item, null, (streamLength + 1) & ~1); start = System.currentTimeMillis(); compressedFrame.close(); if ((streamLength & 1) != 0) out.write(0); end = System.currentTimeMillis(); LOG.debug("Flushed frame #{} from memory in {} ms", frameIndex+1, start - end); frameIndex++; } private void verify(ImageInputStream iis, BufferedImage bi) throws IOException { if (verifier == null) return; iis.seek(0); verifier.setInput(iis); verifyParam.setDestination(bi2); long start = System.currentTimeMillis(); bi2 = verifier.read(0, verifyParam); int maxDiff = BufferedImageUtils.maxDiff(bi.getRaster(), bi2.getRaster(), avgPixelValueBlockSize); long end = System.currentTimeMillis(); LOG.debug("Verified compressed frame #{} in {} ms - max pixel value error: {}", frameIndex+1, end - start, maxDiff); if (maxDiff > maxPixelValueError) throw new CompressionVerificationException(maxDiff); } }