/******************************************************************************* * Copyright (c) 2016 Weasis Team and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Nicolas Roduit - initial API and implementation *******************************************************************************/ package org.weasis.image.jni; import java.awt.Point; import java.awt.Transparency; import java.awt.color.ColorSpace; import java.awt.image.BufferedImage; import java.awt.image.ColorModel; import java.awt.image.ComponentColorModel; import java.awt.image.DataBuffer; import java.awt.image.DataBufferByte; import java.awt.image.DataBufferShort; import java.awt.image.DataBufferUShort; import java.awt.image.IndexColorModel; import java.awt.image.MultiPixelPackedSampleModel; import java.awt.image.PixelInterleavedSampleModel; import java.awt.image.Raster; import java.awt.image.RenderedImage; import java.awt.image.SampleModel; import java.awt.image.WritableRaster; import java.io.IOException; import java.io.InputStream; import java.nio.Buffer; import java.nio.ByteBuffer; import java.nio.ShortBuffer; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import javax.imageio.IIOException; 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.slf4j.Logger; import org.slf4j.LoggerFactory; public abstract class NativeImageReader extends ImageReader { private static final Logger LOGGER = LoggerFactory.getLogger(NativeImageReader.class); // The position of the byte after the last byte read so far. protected long highMark = Long.MIN_VALUE; // Indicating the stream positions of the start of each image. Entries are added as needed. protected final ArrayList<Long> imageStartPosition = new ArrayList<>(); // The number of images in the stream, if known, otherwise -1. private int numImages = -1; protected HashMap<Integer, ArrayList<ImageTypeSpecifier>> imageTypes = new HashMap<>(); protected HashMap<Integer, NativeImage> nativeImages = new HashMap<>(); protected NativeImageReader(ImageReaderSpi originatingProvider) { super(originatingProvider); } protected abstract NativeCodec getCodec(); /** * Creates a <code>ImageTypeSpecifier</code> from the <code>ImageParameters</code>. The default sample model is * pixel interleaved and the default color model is CS_GRAY or CS_sRGB and IndexColorModel with palettes. */ protected static final ImageTypeSpecifier createImageType(ImageParameters params, ColorSpace colorSpace, byte[] redPalette, byte[] greenPalette, byte[] bluePalette, byte[] alphaPalette) throws IOException { return createImageType(params, createColorModel(params, colorSpace, redPalette, greenPalette, bluePalette, alphaPalette)); } protected static final ImageTypeSpecifier createImageType(ImageParameters params, ColorModel colorModel) throws IOException { int nType = params.getDataType(); int nWidth = params.getWidth(); int nHeight = params.getHeight(); int nBands = params.getSamplesPerPixel(); int nBitDepth = params.getBitsPerSample(); int nScanlineStride = params.getBytesPerLine() / ((nBitDepth + 7) / 8); // TODO should handle all types. if (nType < 0 || (nType > 2 && nType != ImageParameters.TYPE_BIT)) { throw new UnsupportedOperationException("Unsupported data type" + " " + nType); } SampleModel sampleModel; if (nType == ImageParameters.TYPE_BIT) { sampleModel = new MultiPixelPackedSampleModel(nType, nWidth, nHeight, 1, nScanlineStride, params.getBitOffset()); } else { int[] bandOffsets = new int[nBands]; for (int i = 0; i < nBands; i++) { bandOffsets[i] = i; } sampleModel = new PixelInterleavedSampleModel(nType, nWidth, nHeight, nBands, nScanlineStride, bandOffsets); } return new ImageTypeSpecifier(colorModel, sampleModel); } private static ColorModel createColorModel(ImageParameters params, ColorSpace colorSpace, byte[] redPalette, byte[] greenPalette, byte[] bluePalette, byte[] alphaPalette) { int nType = params.getDataType(); int nBands = params.getSamplesPerPixel(); int nBitDepth = params.getBitsPerSample(); ColorModel colorModel; if (nBands == 1 && redPalette != null && greenPalette != null && bluePalette != null && redPalette.length == greenPalette.length && redPalette.length == bluePalette.length) { // Build IndexColorModel int paletteLength = redPalette.length; if (alphaPalette != null) { byte[] alphaTmp = alphaPalette; if (alphaPalette.length != paletteLength) { alphaTmp = new byte[paletteLength]; if (alphaPalette.length > paletteLength) { System.arraycopy(alphaPalette, 0, alphaTmp, 0, paletteLength); } else { System.arraycopy(alphaPalette, 0, alphaTmp, 0, alphaPalette.length); for (int i = alphaPalette.length; i < paletteLength; i++) { alphaTmp[i] = (byte) 255; // Opaque. } } } colorModel = new IndexColorModel(nBitDepth, paletteLength, redPalette, greenPalette, bluePalette, alphaTmp); } else { colorModel = new IndexColorModel(nBitDepth, paletteLength, redPalette, greenPalette, bluePalette); } } else if (nType == ImageParameters.TYPE_BIT) { // 0 -> 0x00 (black), 1 -> 0xff (white) byte[] comp = new byte[] { (byte) 0x00, (byte) 0xFF }; colorModel = new IndexColorModel(1, 2, comp, comp, comp); } else { ColorSpace cs; boolean hasAlpha; if (colorSpace != null && (colorSpace.getNumComponents() == nBands || colorSpace.getNumComponents() == nBands - 1)) { cs = colorSpace; hasAlpha = colorSpace.getNumComponents() + 1 == nBands; } else { cs = ColorSpace.getInstance(nBands < 3 ? ColorSpace.CS_GRAY : ColorSpace.CS_sRGB); hasAlpha = nBands % 2 == 0; } int[] bits = new int[nBands]; for (int i = 0; i < nBands; i++) { bits[i] = nBitDepth; } colorModel = new ComponentColorModel(cs, bits, hasAlpha, false, hasAlpha ? Transparency.TRANSLUCENT : Transparency.OPAQUE, nType); } return colorModel; } /** * Stores the location of the image at the specified index in the imageStartPosition List. * * @param imageIndex * @return the image index * @throws IIOException */ private int locateImage(int imageIndex) throws IIOException { if (imageIndex < 0) { throw new IndexOutOfBoundsException(); } try { // Find closest known index which can be -1 if none read before int index = Math.min(imageIndex, imageStartPosition.size() - 1); ImageInputStream stream = (ImageInputStream) input; // Seek to find the beginning of stream if (index >= 0) { if (index == imageIndex) { // Seek to previously identified position and return. stream.seek(imageStartPosition.get(index)); return imageIndex; } else if (highMark >= 0) { // Position not yet identify, so seek to first unread byte. stream.seek(highMark); } } ImageReaderSpi provider = getOriginatingProvider(); // Search images until at desired index or last image found. do { try { if (provider.canDecodeInput(stream)) { // Add the image position when the beginning stream is identify as an image. imageStartPosition.add(stream.getStreamPosition()); } else { return index; } } catch (IOException e) { // Ignore it. return index; } index++; if (index == imageIndex) { break; } if (!skipImage(index)) { return index - 1; } } while (true); } catch (IOException e) { throw new IIOException("Cannot locate image index", e); } return imageIndex; } /** * Verify that imageIndex is in bounds and find the image position. * * @param imageIndex * @throws IIOException */ protected void seekToImage(int imageIndex) throws IIOException { if (imageIndex < minIndex) { throw new IndexOutOfBoundsException("imageIndex less than minIndex!"); } // Update minIndex if cannot seek back. if (seekForwardOnly) { minIndex = imageIndex; } int index = locateImage(imageIndex); if (index != imageIndex) { throw new IndexOutOfBoundsException("imageIndex out of bounds!"); } } /** * Skip the current image. If possible subclasses should override this method with a more efficient implementation. * * @param index * * @return Whether the image was successfully skipped. */ protected boolean skipImage(int index) throws IOException { boolean retval; if (input == null) { throw new IllegalStateException("input cannot be null"); } InputStream stream; if (input instanceof ImageInputStream) { stream = new InputStreamAdapter((ImageInputStream) input); } else { throw new IllegalArgumentException("input is not an ImageInputStream!"); } // FIXME skip stream! retval = nativeDecode(stream, null, index) != null; if (retval) { long pos = ((ImageInputStream) input).getStreamPosition(); if (pos > highMark) { highMark = pos; } } return retval; } /** * Decodes an image from the supplied <code>InputStream</code>. * * @param stream * an input stream * @param param * @param imageIndex * @return NativeImage * @throws IOException */ protected abstract NativeImage nativeDecode(InputStream stream, ImageReadParam param, int imageIndex) throws IOException; protected synchronized NativeImage getImage(int imageIndex, ImageReadParam param) throws IOException { NativeImage nativeImage = nativeImages.get(imageIndex); if (nativeImage != null && nativeImage.getOutputBuffer() != null) { return nativeImage; } if (input == null) { throw new IllegalStateException("input cannot be null"); } seekToImage(imageIndex); InputStreamAdapter stream; if (input instanceof ImageInputStream) { stream = new InputStreamAdapter((ImageInputStream) input); } else { throw new IllegalArgumentException("input is not an ImageInputStream!"); } nativeImage = nativeDecode(stream, param, imageIndex); if (nativeImage != null) { checkParameters(nativeImage.getImageParameters(), param); nativeImages.put(imageIndex, nativeImage); long pos = ((ImageInputStream) input).getStreamPosition(); if (pos > highMark) { highMark = pos; } } return nativeImage; } @Override public int getNumImages(boolean allowSearch) throws IOException { if (input == null) { throw new IllegalStateException("input cannot be null"); } if (seekForwardOnly && allowSearch) { throw new IllegalStateException("can only seek forward!"); } if (numImages > 0) { return numImages; } if (allowSearch) { this.numImages = locateImage(Integer.MAX_VALUE) + 1; } return numImages; } protected synchronized ImageParameters getInfoImage(int imageIndex, ImageReadParam param) throws IOException { NativeImage nativeImage = nativeImages.get(imageIndex); if (nativeImage != null) { // Get parameters in cache ImageParameters p = nativeImage.getImageParameters(); if (!p.isInitSignedData() && param != null) { // Adapt parameters (for signed or unsigned data) when ImageReadParam was null in previous calls. checkParameters(p, param); } return p; } if (input == null) { throw new IllegalStateException("input cannot be null"); } if (!(input instanceof ImageInputStream)) { throw new IllegalArgumentException("input is not an ImageInputStream!"); } ImageInputStream iis = (ImageInputStream) input; seekToImage(imageIndex); // Mark the input. iis.mark(); ImageParameters infoImage; try { long start = System.currentTimeMillis(); NativeCodec decoder = getCodec(); NativeImage mlImage = decoder.buildImage(iis); infoImage = mlImage.getImageParameters(); if (infoImage == null) { throw new IIOException("Null ImageParameters!"); } if (infoImage.getBytesPerLine() <= 0) { // TODO handle ICC profile FileStreamSegment.adaptParametersFromStream(iis, mlImage); String error = decoder.readHeader(mlImage); if (error != null) { throw new IIOException("Native JPEG codec error: " + error); } } long stop = System.currentTimeMillis(); LOGGER.debug("Reading header time: {} ms", (stop - start)); //$NON-NLS-1$ LOGGER.debug("Parameters => {}", infoImage.toString()); checkParameters(infoImage, param); nativeImages.put(imageIndex, mlImage); // Free native resources. decoder.dispose(); } catch (Throwable t) { throw new IIOException("native JPEG lib error", t); } // Reset the marked position. iis.reset(); return infoImage; } private void checkParameters(ImageParameters p, ImageReadParam param) { if (param instanceof NativeImageReadParam) { p.setSignedData(((NativeImageReadParam) param).isSignedData()); p.setInitSignedData(true); } int bps = p.getBitsPerSample(); int spp = p.getSamplesPerPixel(); int dataType = bps <= 8 ? DataBuffer.TYPE_BYTE : p.isSignedData() ? DataBuffer.TYPE_SHORT : DataBuffer.TYPE_USHORT; if (bps > 16 && spp == 1) { dataType = DataBuffer.TYPE_INT; } if (bps == 1 && spp == 1) { dataType = ImageParameters.TYPE_BIT; } p.setDataType(dataType); } @Override public int getWidth(int imageIndex) throws IOException { return getInfoImage(imageIndex, null).getWidth(); } @Override public int getHeight(int imageIndex) throws IOException { return getInfoImage(imageIndex, null).getHeight(); } @Override public IIOMetadata getStreamMetadata() throws IOException { return null; } @Override public IIOMetadata getImageMetadata(int imageIndex) throws IOException { seekToImage(imageIndex); return null; } protected static DataBuffer createDataBuffer(NativeImage img) { DataBuffer db = null; if (img != null) { ImageParameters p = img.getImageParameters(); if (p != null && img.getOutputBuffer() != null) { int dataOffset = p.getDataOffset(); Buffer buf = img.getOutputBuffer(); buf.rewind(); int limit = buf.limit(); if (buf instanceof ByteBuffer) { // int spp = p.getSamplesPerPixel(); // if (spp == 1) { byte[] byteData; if (buf.hasArray()) { byteData = (byte[]) buf.array(); } else { ByteBuffer byteBuffer = (ByteBuffer) buf; byteData = new byte[limit]; for (int i = 0; i < byteData.length; i++) { byteData[i] = byteBuffer.get(); } } db = new DataBufferByte(byteData, byteData.length - dataOffset, dataOffset); // } else { // if ((limit % spp) == 0) { // ByteBuffer byteBuffer = (ByteBuffer) buf; // byte[][] byteData = new byte[spp][limit / 3]; // int pix = 0; // while (byteBuffer.hasRemaining()) { // for (int i = 0; i < spp; i++) { // byteData[i][pix] = byteBuffer.get(); // } // pix++; // } // db = new DataBufferByte(byteData, limit / 3); // } // } } else if (buf instanceof ShortBuffer) { short[] shortData; if (buf.hasArray()) { shortData = (short[]) buf.array(); } else { ShortBuffer byteBuffer = (ShortBuffer) buf; shortData = new short[limit]; for (int i = 0; i < shortData.length; i++) { shortData[i] = byteBuffer.get(); } } // By default short buffer is unsigned, must be explicitly set before to be signed short. // If not, RectifyUShortToShortDataDescriptor will fix this issue if (p.isSignedData()) { db = new DataBufferShort(shortData, shortData.length - dataOffset, dataOffset); } else { db = new DataBufferUShort(shortData, shortData.length - dataOffset, dataOffset); } } img.outputBuffer = null; } } return db; } @Override public synchronized RenderedImage readAsRenderedImage(int imageIndex, ImageReadParam param) throws IOException { return read(imageIndex, param); // TODO must be validated as the image reading concurrency is outside the pool thread // return new NativeRenderedImage(this, param, imageIndex); } @Override public synchronized BufferedImage read(int imageIndex, ImageReadParam param) throws IOException { long start = System.currentTimeMillis(); NativeImage img = getImage(imageIndex, param); if (img == null) { return null; } DataBuffer db = createDataBuffer(img); if (db == null) { return null; } ColorModel cm = createColorModel(img.getImageParameters(), null, null, null, null, null); ImageTypeSpecifier type = createImageType(img.getImageParameters(), cm); SampleModel sm = type.getSampleModel(); Point offset = null; if (param != null) { offset = param.getDestinationOffset(); if (param.getDestination() != null && param.getDestination().getColorModel() != null) { cm = param.getDestination().getColorModel(); sm = cm.createCompatibleSampleModel(img.getImageParameters().getWidth(), img.getImageParameters().getHeight()); } } // Create a new raster and copy the data. WritableRaster raster = Raster.createWritableRaster(sm, db, offset); long stop = System.currentTimeMillis(); LOGGER.debug("Building BufferedImage time: {} ms", stop - start); //$NON-NLS-1$ return new BufferedImage(cm, raster, false, null); } @Override public Iterator<ImageTypeSpecifier> getImageTypes(int imageIndex) throws IOException { seekToImage(imageIndex); ArrayList<ImageTypeSpecifier> types; synchronized (imageTypes) { if (imageTypes.containsKey(imageIndex)) { types = imageTypes.get(imageIndex); } else { types = new ArrayList<>(); ImageParameters info = getInfoImage(imageIndex, null); types.add(createImageType(info, null, null, null, null, null)); imageTypes.put(imageIndex, types); } } return types.iterator(); } @Override public void reset() { resetLocal(); super.reset(); } protected void resetLocal() { highMark = Long.MIN_VALUE; imageStartPosition.clear(); nativeImages.clear(); imageTypes.clear(); numImages = -1; } @Override public void setInput(Object input, boolean seekForwardOnly, boolean ignoreMetadata) { super.setInput(input, seekForwardOnly, ignoreMetadata); if (input != null && !(input instanceof ImageInputStream)) { throw new IllegalArgumentException("input is not an ImageInputStream!"); } resetLocal(); } @Override public ImageReadParam getDefaultReadParam() { return new NativeImageReadParam(); } }