/******************************************************************************* * 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.Rectangle; import java.awt.image.BufferedImage; import java.awt.image.ColorModel; import java.awt.image.ComponentSampleModel; import java.awt.image.DataBuffer; import java.awt.image.DataBufferByte; import java.awt.image.DataBufferShort; import java.awt.image.DataBufferUShort; import java.awt.image.DirectColorModel; import java.awt.image.IndexColorModel; import java.awt.image.MultiPixelPackedSampleModel; import java.awt.image.PackedColorModel; 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 javax.imageio.ImageTypeSpecifier; import javax.imageio.ImageWriteParam; import javax.imageio.ImageWriter; import javax.imageio.metadata.IIOMetadata; import javax.imageio.spi.ImageWriterSpi; public abstract class NativeImageWriter extends ImageWriter { protected static final Object getDataBufferData(DataBuffer db) { Object data; int dType = db.getDataType(); switch (dType) { case DataBuffer.TYPE_BYTE: data = ((DataBufferByte) db).getData(); break; case DataBuffer.TYPE_USHORT: data = ((DataBufferUShort) db).getData(); break; case DataBuffer.TYPE_SHORT: data = ((DataBufferShort) db).getData(); break; default: throw new IllegalArgumentException("Unsupported data type: " + dType); } return data; } /** * Returns a contiguous <code>Raster</code> of data over the specified <code>Rectangle</code>. If the region is a * sub-region of a single tile, then a child of that tile will be returned. If the region overlaps more than one * tile and has 8 bits per sample, then a pixel interleaved Raster having band offsets 0,1,... will be returned. * Otherwise the Raster returned by <code>im.copyData(null)</code> will be returned. */ protected static final Raster getContiguousData(RenderedImage im, Rectangle region) { if (im == null) { throw new IllegalArgumentException("im == null"); } else if (region == null) { throw new IllegalArgumentException("region == null"); } Raster raster; if (im.getNumXTiles() == 1 && im.getNumYTiles() == 1) { // Image is not tiled so just get a reference to the tile. raster = im.getTile(im.getMinTileX(), im.getMinTileY()); // Ensure result has requested coverage. Rectangle bounds = raster.getBounds(); if (!bounds.equals(region)) { raster = raster.createChild(region.x, region.y, region.width, region.height, region.x, region.y, null); } } else { // Image is tiled. // Create an interleaved raster for copying for 8-bit case. // This ensures that for RGB data the band offsets are {0,1,2}. SampleModel sampleModel = im.getSampleModel(); WritableRaster target = sampleModel.getSampleSize(0) == 8 ? Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, im.getWidth(), im.getHeight(), sampleModel.getNumBands(), new Point(im.getMinX(), im.getMinY())) : null; // Copy the data. raster = im.copyData(target); } return raster; } /** * Subsamples and sub-bands the input <code>Raster</code> over a sub-region and stores the result in a * <code>WritableRaster</code>. * * @param src * The source <code>Raster</code> * @param sourceBands * The source bands to use; may be <code>null</code> * @param subsampleX * The subsampling factor along the horizontal axis. * @param subsampleY * The subsampling factor along the vertical axis. in which case all bands will be used. * @param dst * The destination <code>WritableRaster</code>. * @throws IllegalArgumentException * if <code>source</code> is <code>null</code> or empty, <code>dst</code> is <code>null</code>, * <code>sourceBands.length</code> exceeds the number of bands in <code>source</code>, or * <code>sourcBands</code> contains an element which is negative or greater than or equal to the number * of bands in <code>source</code>. */ private static void reformat(Raster source, int[] sourceBands, int subsampleX, int subsampleY, WritableRaster dst) { // Check for nulls. if (source == null) { throw new IllegalArgumentException("source == null!"); } else if (dst == null) { throw new IllegalArgumentException("dst == null!"); } // Validate the source bounds. XXX is this needed? Rectangle sourceBounds = source.getBounds(); if (sourceBounds.isEmpty()) { throw new IllegalArgumentException("source.getBounds().isEmpty()!"); } // Check sub-banding. boolean isSubBanding = false; int numSourceBands = source.getSampleModel().getNumBands(); if (sourceBands != null) { if (sourceBands.length > numSourceBands) { throw new IllegalArgumentException("sourceBands.length > numSourceBands!"); } boolean isRamp = sourceBands.length == numSourceBands; for (int i = 0; i < sourceBands.length; i++) { if (sourceBands[i] < 0 || sourceBands[i] >= numSourceBands) { throw new IllegalArgumentException("sourceBands[i] < 0 || sourceBands[i] >= numSourceBands!"); } else if (sourceBands[i] != i) { isRamp = false; } } isSubBanding = !isRamp; } // Allocate buffer for a single source row. int sourceWidth = sourceBounds.width; int[] pixels = new int[sourceWidth * numSourceBands]; // Initialize variables used in loop. int sourceX = sourceBounds.x; int sourceY = sourceBounds.y; int numBands = sourceBands != null ? sourceBands.length : numSourceBands; int dstWidth = dst.getWidth(); int dstYMax = dst.getHeight() - 1; int copyFromIncrement = numSourceBands * subsampleX; // Loop over source rows, subsample each, and store in destination. for (int dstY = 0; dstY <= dstYMax; dstY++) { // Read one row. source.getPixels(sourceX, sourceY, sourceWidth, 1, pixels); // Copy within the same buffer by left shifting. if (isSubBanding) { int copyFrom = 0; int copyTo = 0; for (int i = 0; i < dstWidth; i++) { for (int j = 0; j < numBands; j++) { pixels[copyTo++] = pixels[copyFrom + sourceBands[j]]; } copyFrom += copyFromIncrement; } } else { int copyFrom = copyFromIncrement; int copyTo = numSourceBands; // Start from index 1 as no need to copy the first pixel. for (int i = 1; i < dstWidth; i++) { int k = copyFrom; for (int j = 0; j < numSourceBands; j++) { pixels[copyTo++] = pixels[k++]; } copyFrom += copyFromIncrement; } } // Set the destionation row. dst.setPixels(0, dstY, dstWidth, 1, pixels); // Increment the source row. sourceY += subsampleY; } } protected NativeImageWriter(ImageWriterSpi originatingProvider) { super(originatingProvider); } @Override public IIOMetadata convertImageMetadata(IIOMetadata inData, ImageTypeSpecifier imageType, ImageWriteParam param) { return null; } @Override public IIOMetadata convertStreamMetadata(IIOMetadata inData, ImageWriteParam param) { return null; } @Override public IIOMetadata getDefaultImageMetadata(ImageTypeSpecifier imageType, ImageWriteParam param) { return null; } @Override public IIOMetadata getDefaultStreamMetadata(ImageWriteParam param) { return null; } protected static final Rectangle getSourceRegion(ImageWriteParam param, int sourceMinX, int sourceMinY, int srcWidth, int srcHeight) { Rectangle sourceRegion = new Rectangle(sourceMinX, sourceMinY, srcWidth, srcHeight); if (param != null) { Rectangle region = param.getSourceRegion(); if (region != null) { sourceRegion = sourceRegion.intersection(region); } int subsampleXOffset = param.getSubsamplingXOffset(); int subsampleYOffset = param.getSubsamplingYOffset(); sourceRegion.x += subsampleXOffset; sourceRegion.y += subsampleYOffset; sourceRegion.width -= subsampleXOffset; sourceRegion.height -= subsampleYOffset; } return sourceRegion; } protected void formatInputDataBuffer(NativeImage nImage, RenderedImage image, ImageWriteParam param, boolean allowBilevel, int[] supportedFormats) { if (supportedFormats == null) { throw new IllegalArgumentException("supportedFormats == null!"); } // Determine the source region. Rectangle sourceRegion = getSourceRegion(param, image.getMinX(), image.getMinY(), image.getWidth(), image.getHeight()); if (sourceRegion.isEmpty()) { throw new IllegalArgumentException("sourceRegion.isEmpty()"); } // Check whether reformatting is necessary to conform to mediaLib // image format (packed bilevel if allowed or ((G|I)|(RGB))[A]). boolean reformatData = false; boolean isBilevel = false; SampleModel sampleModel = image.getSampleModel(); int numSourceBands = sampleModel.getNumBands(); int[] sourceBands = param != null ? param.getSourceBands() : null; // Check for non-nominal sub-banding. int numBands; if (sourceBands != null) { numBands = sourceBands.length; if (numBands != numSourceBands) { // The number of bands must be the same. reformatData = true; } else { // The band order must not change. for (int i = 0; i < numSourceBands; i++) { if (sourceBands[i] != i) { reformatData = true; break; } } } } else { numBands = numSourceBands; } int cs_format = numBands == 1 ? ImageParameters.CM_GRAY : numBands == 3 ? ImageParameters.CM_S_RGB : ImageParameters.CM_S_RGBA; // If sub-banding does not dictate reformatting, check subsampling.. if (!reformatData && param != null && (param.getSourceXSubsampling() != 1 || param.getSourceXSubsampling() != 1)) { reformatData = true; } // If sub-banding does not dictate reformatting check SampleModel. if (!reformatData) { if (allowBilevel && sampleModel.getNumBands() == 1 && sampleModel.getSampleSize(0) == 1 && sampleModel instanceof MultiPixelPackedSampleModel && sampleModel.getDataType() == DataBuffer.TYPE_BYTE) { // Need continguous packed bits. MultiPixelPackedSampleModel mppsm = (MultiPixelPackedSampleModel) sampleModel; if (mppsm.getPixelBitStride() == 1) { isBilevel = true; } else { reformatData = true; } } else { // TODO get format // cs_format = getFormat(sampleModel, image.getColorModel()); // Set the data reformatting flag. reformatData = true; int len = supportedFormats.length; for (int i = 0; i < len; i++) { if (cs_format == supportedFormats[i]) { reformatData = false; break; } } } } // Variable for the eventual destination data. Raster raster = null; if (reformatData) { // Determine the maximum bit depth. int[] sampleSize = sampleModel.getSampleSize(); int bitDepthMax = sampleSize[0]; for (int i = 1; i < numSourceBands; i++) { bitDepthMax = Math.max(bitDepthMax, sampleSize[i]); } // Set the data type as a function of bit depth. int dataType; if (bitDepthMax <= 8) { dataType = DataBuffer.TYPE_BYTE; } else if (bitDepthMax <= 16) { dataType = DataBuffer.TYPE_USHORT; } else { throw new IllegalArgumentException("Unsupported data type, pixel depth: " + bitDepthMax); } // Determine the width and height. int width; int height; if (param != null) { int subsampleX = param.getSourceXSubsampling(); int subsampleY = param.getSourceYSubsampling(); width = (sourceRegion.width + subsampleX - 1) / subsampleX; height = (sourceRegion.height + subsampleY - 1) / subsampleY; } else { width = sourceRegion.width; height = sourceRegion.height; } // Load a ramp for band offsets. int[] newBandOffsets = new int[numBands]; for (int i = 0; i < numBands; i++) { newBandOffsets[i] = i; } // Create a new SampleModel. SampleModel newSampleModel; if (allowBilevel && sampleModel.getNumBands() == 1 && bitDepthMax == 1) { newSampleModel = new MultiPixelPackedSampleModel(dataType, width, height, 1); isBilevel = true; } else { newSampleModel = new PixelInterleavedSampleModel(dataType, width, height, newBandOffsets.length, width * numSourceBands, newBandOffsets); } // Create a new Raster at (0,0). WritableRaster newRaster = Raster.createWritableRaster(newSampleModel, null); // Populate the new Raster. if (param != null && (param.getSourceXSubsampling() != 1 || param.getSourceXSubsampling() != 1)) { // Subsampling, possibly with sub-banding. reformat(getContiguousData(image, sourceRegion), sourceBands, param.getSourceXSubsampling(), param.getSourceYSubsampling(), newRaster); } else if (sourceBands == null && image.getSampleModel().getClass().isInstance(newSampleModel) && newSampleModel.getTransferType() == image.getSampleModel().getTransferType()) { // Neither subsampling nor sub-banding. WritableRaster translatedChild = newRaster.createWritableTranslatedChild(sourceRegion.x, sourceRegion.y); // Use copyData() to avoid potentially cobbling the entire // source region into an extra Raster via getData(). image.copyData(translatedChild); } else { // Cannot use copyData() so use getData() to retrieve and // possibly sub-band the source data and use setRect(). WritableRaster translatedChild = newRaster.createWritableTranslatedChild(sourceRegion.x, sourceRegion.y); Raster sourceRaster = getContiguousData(image, sourceRegion); if (sourceBands != null) { // Copy only the requested bands. sourceRaster = sourceRaster.createChild(sourceRegion.x, sourceRegion.y, sourceRegion.width, sourceRegion.height, sourceRegion.x, sourceRegion.y, sourceBands); } // Get the region from the image and set it into the Raster. translatedChild.setRect(sourceRaster); } raster = newRaster; sampleModel = newRaster.getSampleModel(); } else { raster = getContiguousData(image, sourceRegion).createTranslatedChild(0, 0); sampleModel = raster.getSampleModel(); // TODO get format // cs_format = getFormat(sampleModel, image.getColorModel()); } // Create the input data buffer if (isBilevel) { // Bilevel image MultiPixelPackedSampleModel mppsm = ((MultiPixelPackedSampleModel) sampleModel); int stride = mppsm.getScanlineStride(); // Determine the offset to the start of the data. int offset = raster.getDataBuffer().getOffset() - raster.getSampleModelTranslateY() * stride - raster.getSampleModelTranslateX() / 8 + mppsm.getOffset(0, 0); // Get a reference to the internal data array. Object bitData = getDataBufferData(raster.getDataBuffer()); int dataLength = raster.getDataBuffer().getSize(); ImageParameters params = nImage.getImageParameters(); params.setDataType(ImageParameters.TYPE_BIT); params.setSamplesPerPixel(sampleModel.getNumBands()); params.setBitsPerSample(sampleModel.getSampleSize(0)); params.setWidth(raster.getWidth()); params.setHeight(raster.getHeight()); params.setBytesPerLine((stride * params.getBitsPerSample() + 7) / 8); nImage.fillInputBuffer(bitData, offset, dataLength); } else { ComponentSampleModel csm = (ComponentSampleModel) sampleModel; // Get the internal data array. Object data = getDataBufferData(raster.getDataBuffer()); int dataLength = raster.getDataBuffer().getSize(); int stride = csm.getScanlineStride(); int[] bandOffsets = csm.getBandOffsets(); int minBandOffset = bandOffsets[0]; for (int i = 1; i < bandOffsets.length; i++) { if (bandOffsets[i] < minBandOffset) { minBandOffset = bandOffsets[i]; } } // Determine the offset to the start of the data int offset = (raster.getMinY() - raster.getSampleModelTranslateY()) * stride + (raster.getMinX() - raster.getSampleModelTranslateX()) * numSourceBands + minBandOffset; ImageParameters params = nImage.getImageParameters(); params.setDataType(sampleModel.getDataType()); params.setSamplesPerPixel(sampleModel.getNumBands()); params.setBitsPerSample(sampleModel.getSampleSize(0)); params.setWidth(raster.getWidth()); params.setHeight(raster.getHeight()); params.setBytesPerLine((stride * params.getBitsPerSample() + 7) / 8); nImage.fillInputBuffer(data, offset, dataLength); } } protected abstract NativeCodec getCodec(); /** * Convert an IndexColorModel-based image to 3-band component RGB. * * @param image * The source image. */ protected static BufferedImage convertTo3BandRGB(RenderedImage image) { if (image == null) { throw new IllegalArgumentException("image cannot be null"); } ColorModel cm = image.getColorModel(); if (!(cm instanceof IndexColorModel)) { throw new IllegalArgumentException("color model in not IndexColorModel!"); } Raster src; if (image.getNumXTiles() == 1 && image.getNumYTiles() == 1) { // Image is not tiled so just get a reference to the tile. src = image.getTile(image.getMinTileX(), image.getMinTileY()); if (src.getWidth() != image.getWidth() || src.getHeight() != image.getHeight()) { src = src.createChild(src.getMinX(), src.getMinY(), image.getWidth(), image.getHeight(), src.getMinX(), src.getMinY(), null); } } else { // Image is tiled so need to get a contiguous raster. src = image.getData(); } // This is probably not the most efficient approach given that // the mediaLibImage will eventually need to be in component form. BufferedImage dst = ((IndexColorModel) cm).convertToIntDiscrete(src, false); if (dst.getSampleModel().getNumBands() == 4) { // // Without copying data create a BufferedImage which has // only the RGB bands, not the alpha band. // WritableRaster rgbaRas = dst.getRaster(); WritableRaster rgbRas = rgbaRas.createWritableChild(0, 0, dst.getWidth(), dst.getHeight(), 0, 0, new int[] { 0, 1, 2 }); PackedColorModel pcm = (PackedColorModel) dst.getColorModel(); int bits = pcm.getComponentSize(0) + pcm.getComponentSize(1) + pcm.getComponentSize(2); DirectColorModel dcm = new DirectColorModel(bits, pcm.getMask(0), pcm.getMask(1), pcm.getMask(2)); dst = new BufferedImage(dcm, rgbRas, false, null); } return dst; } }