/* ***** 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 * Agfa Healthcare. * Portions created by the Initial Developer are Copyright (C) 2013 * 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.plugins.rle; import java.awt.image.BufferedImage; import java.awt.image.DataBuffer; import java.awt.image.DataBufferByte; import java.awt.image.DataBufferShort; import java.awt.image.DataBufferUShort; import java.awt.image.Raster; import java.awt.image.SampleModel; import java.awt.image.WritableRaster; import java.io.EOFException; import java.io.IOException; import java.util.Iterator; import javax.imageio.ImageReadParam; import javax.imageio.ImageReader; import javax.imageio.ImageTypeSpecifier; import javax.imageio.metadata.IIOMetadata; import javax.imageio.spi.ImageReaderSpi; import javax.imageio.stream.ImageInputStream; import org.dcm4che3.util.ByteUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * @author Gunter Zeilinger <gunterze@gmail.com> * */ public class RLEImageReader extends ImageReader { private static Logger LOG = LoggerFactory.getLogger(RLEImageReader.class); private static final String UNKNOWN_IMAGE_TYPE = "RLE Image Reader needs ImageReadParam.destination or " + "ImageReadParam.destinationType specified"; private static final String UNSUPPORTED_DATA_TYPE = "Unsupported Data Type of ImageReadParam.destination or " + "ImageReadParam.destinationType: "; private static final String MISMATCH_NUM_RLE_SEGMENTS = "Number of RLE Segments does not match image type: "; private final int[] header = new int[16]; private final byte[] buf = new byte[8192]; private long headerPos; private long bufOff; private int bufPos; private int bufLen; private ImageInputStream iis; private int width; private int height; protected RLEImageReader(ImageReaderSpi originatingProvider) { super(originatingProvider); } @Override public void setInput(Object input, boolean seekForwardOnly, boolean ignoreMetadata) { super.setInput(input, seekForwardOnly, ignoreMetadata); resetInternalState(); iis = (ImageInputStream) input; } private void resetInternalState() { width = 0; height = 0; } @Override public int getNumImages(boolean allowSearch) throws IOException { return 1; } @Override public int getWidth(int imageIndex) throws IOException { return width; } @Override public int getHeight(int imageIndex) throws IOException { return height; } @Override public Iterator<ImageTypeSpecifier> getImageTypes(int imageIndex) throws IOException { return null; } @Override public IIOMetadata getStreamMetadata() throws IOException { return null; } @Override public IIOMetadata getImageMetadata(int imageIndex) throws IOException { return null; } @Override public boolean canReadRaster() { return true; } @Override public Raster readRaster(int imageIndex, ImageReadParam param) throws IOException { checkIndex(imageIndex); WritableRaster raster = getDestinationRaster(param); read(raster.getDataBuffer()); return raster; } @Override public BufferedImage read(int imageIndex, ImageReadParam param) throws IOException { checkIndex(imageIndex); BufferedImage bi = getDestination(param); read(bi.getRaster().getDataBuffer()); return bi; } private void checkIndex(int imageIndex) { if (imageIndex != 0) throw new IndexOutOfBoundsException("imageIndex: " + imageIndex); } private BufferedImage getDestination(ImageReadParam param) { if (param == null) throw new IllegalArgumentException(UNKNOWN_IMAGE_TYPE); BufferedImage bi = param.getDestination(); if (bi != null) { width = bi.getWidth(); height = bi.getHeight(); return bi; } ImageTypeSpecifier imageType = param.getDestinationType(); if (imageType != null) { SampleModel sm = imageType.getSampleModel(); width = sm.getWidth(); height = sm.getHeight(); return imageType.createBufferedImage(width, height); } throw new IllegalArgumentException(UNKNOWN_IMAGE_TYPE); } private WritableRaster getDestinationRaster(ImageReadParam param) { if (param == null) throw new IllegalArgumentException(UNKNOWN_IMAGE_TYPE); BufferedImage bi = param.getDestination(); if (bi != null) { width = bi.getWidth(); height = bi.getHeight(); return bi.getRaster(); } ImageTypeSpecifier imageType = param.getDestinationType(); if (imageType != null) { SampleModel sm = imageType.getSampleModel(); width = sm.getWidth(); height = sm.getHeight(); return Raster.createWritableRaster(sm, null); } throw new IllegalArgumentException(UNKNOWN_IMAGE_TYPE); } private void read(DataBuffer db) throws IOException { switch (db.getDataType()) { case DataBuffer.TYPE_BYTE: read(((DataBufferByte) db).getBankData()); break; case DataBuffer.TYPE_USHORT: read(((DataBufferUShort) db).getData()); break; case DataBuffer.TYPE_SHORT: read(((DataBufferShort) db).getData()); break; default: throw new IllegalArgumentException( UNSUPPORTED_DATA_TYPE + db.getDataType()); } } private void read(byte[][] bands) throws IOException { readRLEHeader(bands.length); for (int i = 0; i < bands.length; i++) unrle(i+1, bands[i]); } private void read(short[] data) throws IOException { readRLEHeader(2); unrle(1, data); unrle(2, data); } private void seekSegment(int seg) throws IOException { long streamPos = headerPos + (header[seg] & 0xffffffffL); int bufPos = (int) (streamPos - bufOff); if (bufPos >= 0 && bufPos <= bufLen) this.bufPos = bufPos; else { iis.seek(streamPos); this.bufPos = bufLen; // force fillBuffer on nextByte() } } private void readRLEHeader(int numSegments) throws IOException { fillBuffer(); if (bufLen < 64) throw new EOFException(); for (int i = 0, off = 0; i < header.length; i++, off += 4) header[i] = ByteUtils.bytesToIntLE(buf, off); bufPos = 64; if (header[0] != numSegments) throw new IOException(MISMATCH_NUM_RLE_SEGMENTS + header[0]); } private void unrle(int seg, byte[] data) throws IOException { seekSegment(seg); int pos = 0; try { int n; int end; byte val; while (pos < data.length) { n = nextByte(); if (n >= 0) { read(data, pos, ++n); pos += n; } else if (n != -128) { end = pos + 1 - n; val = nextByte(); while (pos < end) data[pos++] = val; } } } catch (EOFException e) { LOG.info("RLE Segment #{} too short, set missing {} bytes to 0", seg, data.length - pos); } catch (IndexOutOfBoundsException e) { LOG.info("RLE Segment #{} too long, truncate surplus bytes", seg); } } private void read(byte[] data, int pos, int len) throws IOException { int remaining = len; int n; while (remaining > 0) { n = bufLen - bufPos; if (n <= 0) { fillBuffer(); n = bufLen - bufPos; } if ((remaining -= n) < 0) n += remaining; System.arraycopy(buf, bufPos, data, pos, n); bufPos += n; pos += n; } } private void unrle(int seg, short[] data) throws IOException { seekSegment(seg); int pos = 0; try { int shift = seg == 1 ? 8 : 0; int n; int end; int val; while (pos < data.length) { n = nextByte(); if (n >= 0) { read(data, pos, ++n, shift); pos += n; } else if (n != -128) { end = pos + 1 - n; val = (nextByte() & 0xff) << shift; while (pos < end) data[pos++] |= val; } } } catch (EOFException e) { LOG.info("RLE Segment #{} too short, set missing {} bytes to 0", seg, data.length - pos); } catch (IndexOutOfBoundsException e) { LOG.info("RLE Segment #{} to long, truncate surplus bytes", seg); } } private void read(short[] data, int pos, int len, int shift) throws IOException { int remaining = len; int n; while (remaining > 0) { n = bufLen - bufPos; if (n <= 0) { fillBuffer(); n = bufLen - bufPos; } if ((remaining -= n) < 0) n += remaining; while (n-- > 0) data[pos++] |= (buf[bufPos++] & 0xff) << shift; } } private void fillBuffer() throws IOException { bufOff = iis.getStreamPosition(); bufPos = 0; bufLen = iis.read(buf); if (bufLen <= 0) throw new EOFException(); } private byte nextByte() throws IOException { if (bufPos >= bufLen) fillBuffer(); return buf[bufPos++]; } }