#714 PNM: Add support for writing TYPE_INT_* images + implementation of WriterSpi.canEncode

(cherry picked from commit 26981513d8)
This commit is contained in:
Harald Kuhr
2022-11-21 16:13:54 +01:00
parent 0538db7103
commit debf7d0207
12 changed files with 449 additions and 49 deletions
@@ -30,8 +30,6 @@
package com.twelvemonkeys.imageio.plugins.pnm;
import com.twelvemonkeys.imageio.ImageWriterBase;
import org.w3c.dom.NodeList;
import javax.imageio.IIOImage;
@@ -40,7 +38,7 @@ import javax.imageio.metadata.IIOMetadataFormatImpl;
import javax.imageio.metadata.IIOMetadataNode;
import javax.imageio.spi.ImageWriterSpi;
import javax.imageio.stream.ImageOutputStream;
import java.awt.image.DataBuffer;
import java.awt.image.*;
import java.io.IOException;
import java.util.Locale;
@@ -53,11 +51,7 @@ abstract class HeaderWriter {
this.imageOutput = imageOutput;
}
public static void write(final IIOImage image, final ImageWriterBase writer, final ImageOutputStream imageOutput) throws IOException {
createHeaderWriter(writer.getFormatName(), imageOutput).writeHeader(image, writer.getOriginatingProvider());
}
private static HeaderWriter createHeaderWriter(final String formatName, final ImageOutputStream imageOutput) {
static HeaderWriter createHeaderWriter(final String formatName, final ImageOutputStream imageOutput) {
if (formatName.equals("pam")) {
return new PAMHeaderWriter(imageOutput);
}
@@ -83,25 +77,36 @@ abstract class HeaderWriter {
return image.hasRaster() ? image.getRaster().getNumBands() : image.getRenderedImage().getSampleModel().getNumBands();
}
protected final SampleModel getSampleModel(final IIOImage image) {
return image.hasRaster() ? image.getRaster().getSampleModel() : image.getRenderedImage().getSampleModel();
}
protected int getMaxVal(final IIOImage image) {
int transferType = getTransferType(image);
if (transferType == DataBuffer.TYPE_BYTE) {
return PNM.MAX_VAL_8BIT;
}
else if (transferType == DataBuffer.TYPE_USHORT) {
return PNM.MAX_VAL_16BIT;
}
// else if (transferType == DataBuffer.TYPE_INT) {
// TODO: Support TYPE_INT through conversion, if number of channels is 3 or 4 (TYPE_INT_RGB, TYPE_INT_ARGB)
// }
else {
throw new IllegalArgumentException("Unsupported data type: " + transferType);
switch (transferType) {
case DataBuffer.TYPE_BYTE:
return PNM.MAX_VAL_8BIT;
case DataBuffer.TYPE_USHORT:
return PNM.MAX_VAL_16BIT;
case DataBuffer.TYPE_INT:
// We support TYPE_INT through conversion, if number of channels is 3 or 4 (TYPE_INT_RGB, TYPE_INT_ARGB)
SampleModel sampleModel = getSampleModel(image);
int numBands = sampleModel.getNumBands();
if (sampleModel instanceof SinglePixelPackedSampleModel && (numBands == 3 || numBands == 4)) {
return PNM.MAX_VAL_8BIT;
}
// ...else fall through
default:
throw new IllegalArgumentException("Unsupported data type: " + transferType);
}
}
protected final int getTransferType(final IIOImage image) {
return image.hasRaster() ? image.getRaster().getTransferType() : image.getRenderedImage().getSampleModel().getTransferType();
return image.hasRaster() ? image.getRaster().getTransferType() : image.getRenderedImage().getSampleModel().getTransferType();
}
protected final void writeComments(final IIOMetadata metadata, final ImageWriterSpi provider) throws IOException {
@@ -120,5 +125,4 @@ abstract class HeaderWriter {
}
}
}
}
@@ -30,6 +30,8 @@
package com.twelvemonkeys.imageio.plugins.pnm;
import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers;
import javax.imageio.IIOImage;
import javax.imageio.spi.ImageWriterSpi;
import javax.imageio.stream.ImageOutputStream;
@@ -44,6 +46,8 @@ final class PAMHeaderWriter extends HeaderWriter {
@Override
public void writeHeader(final IIOImage image, final ImageWriterSpi provider) throws IOException {
TupleType tupleType = tupleType(image);
// Write PAM magic
imageOutput.writeShort(PNM.PAM);
imageOutput.write('\n');
@@ -52,12 +56,19 @@ final class PAMHeaderWriter extends HeaderWriter {
// Write width/height and number of channels
imageOutput.write(String.format("WIDTH %s\nHEIGHT %s\n", getWidth(image), getHeight(image)).getBytes(UTF_8));
imageOutput.write(String.format("DEPTH %s\n", getNumBands(image)).getBytes(UTF_8));
// TODO: maxSample (8 or16 bit)
imageOutput.write(String.format("MAXVAL %s\n", getMaxVal(image)).getBytes(UTF_8));
// TODO: Determine tuple type based on input color model and image data
TupleType tupleType = getNumBands(image) > 3 ? TupleType.RGB_ALPHA : TupleType.RGB;
imageOutput.write(String.format("TUPLTYPE %s\nENDHDR\n", tupleType).getBytes(UTF_8));
}
private static TupleType tupleType(IIOImage image) {
TupleType tupleType = image.hasRaster()
? TupleType.forPAM(image.getRaster())
: TupleType.forPAM(ImageTypeSpecifiers.createFromRenderedImage(image.getRenderedImage()));
if (tupleType == null) {
throw new IllegalArgumentException("Unknown TupleType for " + (image.hasRaster() ? image.getRaster() : image.getRenderedImage()));
}
return tupleType;
}
}
@@ -45,12 +45,11 @@ public final class PAMImageWriterSpi extends ImageWriterSpiBase {
super(new PAMProviderInfo());
}
public boolean canEncodeImage(final ImageTypeSpecifier pType) {
// TODO: FixMe
return true;
public boolean canEncodeImage(final ImageTypeSpecifier type) {
return TupleType.forPAM(type) != null;
}
public ImageWriter createWriterInstance(final Object pExtension) {
public ImageWriter createWriterInstance(final Object extension) {
return new PNMImageWriter(this);
}
@@ -69,7 +69,7 @@ final class PFMHeaderParser extends HeaderParser {
public PNMHeader parse() throws IOException {
int width = 0;
int height = 0;
float maxSample = tupleType == TupleType.BLACKANDWHITE_WHITE_IS_ZERO ? 1 : 0; // PBM has no maxSample line
float maxSample = 0;
List<String> comments = new ArrayList<>();
@@ -77,7 +77,7 @@ final class PFMHeaderParser extends HeaderParser {
String line = input.readLine();
if (line == null) {
throw new IIOException("Unexpeced end of stream");
throw new IIOException("Unexpected end of stream");
}
int commentStart = line.indexOf('#');
@@ -30,6 +30,8 @@
package com.twelvemonkeys.imageio.plugins.pnm;
import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers;
import javax.imageio.IIOImage;
import javax.imageio.spi.ImageWriterSpi;
import javax.imageio.stream.ImageOutputStream;
@@ -45,8 +47,7 @@ final class PNMHeaderWriter extends HeaderWriter {
@Override
public void writeHeader(final IIOImage image, final ImageWriterSpi provider) throws IOException {
// Write P4/P5/P6 magic (Support only RAW formats for now; if we are to support PLAIN formats, pass parameter)
// TODO: Determine PBM, PBM or PPM based on input color model and image data?
short type = PNM.PPM;
short type = type(image);
imageOutput.writeShort(type);
imageOutput.write('\n');
@@ -61,4 +62,35 @@ final class PNMHeaderWriter extends HeaderWriter {
imageOutput.write(String.format("%s\n", getMaxVal(image)).getBytes(UTF_8));
}
}
private short type(IIOImage image) {
TupleType type = tupleType(image);
if (type != null) {
switch (type) {
case BLACKANDWHITE_WHITE_IS_ZERO:
return PNM.PBM;
case GRAYSCALE:
return PNM.PGM;
case RGB:
return PNM.PPM;
default:
// fall through...
}
}
throw new IllegalArgumentException("Unsupported tupleType: " + type);
}
private static TupleType tupleType(IIOImage image) {
TupleType tupleType = image.hasRaster()
? TupleType.forPNM(image.getRaster())
: TupleType.forPNM(ImageTypeSpecifiers.createFromRenderedImage(image.getRenderedImage()));
if (tupleType == null) {
throw new IllegalArgumentException("Unknown TupleType for " + (image.hasRaster() ? image.getRaster() : image.getRenderedImage()));
}
return tupleType;
}
}
@@ -31,14 +31,17 @@
package com.twelvemonkeys.imageio.plugins.pnm;
import com.twelvemonkeys.imageio.ImageWriterBase;
import com.twelvemonkeys.imageio.util.RasterUtils;
import javax.imageio.*;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageWriterSpi;
import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.awt.image.Raster;
import java.awt.image.SampleModel;
import javax.imageio.stream.ImageOutputStream;
import java.awt.image.*;
import java.io.File;
import java.io.IOException;
@@ -69,7 +72,7 @@ public final class PNMImageWriter extends ImageWriterBase {
// TODO: Issue warning if streamMetadata is non-null?
// TODO: Issue warning if IIOImage contains thumbnails or other data we can't store?
HeaderWriter.write(image, this, imageOutput);
writeHeader(image, this, imageOutput);
// TODO: Sub region
// TODO: Subsampling
@@ -80,16 +83,20 @@ public final class PNMImageWriter extends ImageWriterBase {
processImageComplete();
}
private void writeHeader(final IIOImage image, final ImageWriterBase writer, final ImageOutputStream imageOutput) throws IOException {
HeaderWriter.createHeaderWriter(writer.getFormatName(), imageOutput).writeHeader(image, writer.getOriginatingProvider());
}
private void writeImageData(final IIOImage image) throws IOException {
// - dump data as is (or convert, if TYPE_INT_xxx)
// - dump data as is (or convert, if TYPE_INT_xxx
// Enforce RGB/CMYK order for such data!
// TODO: Loop over x/y tiles, using 0,0 is only valid for BufferedImage
// TODO: PNM/PAM does not support tiling, we must iterate all tiles along the x-axis for each row we write
Raster tile = image.hasRaster() ? image.getRaster() : image.getRenderedImage().getTile(0, 0);
tile = tile.getTransferType() == DataBuffer.TYPE_INT ? RasterUtils.asByteRaster(tile) : tile;
SampleModel sampleModel = tile.getSampleModel();
DataBuffer dataBuffer = tile.getDataBuffer();
int tileWidth = tile.getWidth();
@@ -48,12 +48,11 @@ public final class PNMImageWriterSpi extends ImageWriterSpiBase {
super(new PNMProviderInfo());
}
public boolean canEncodeImage(final ImageTypeSpecifier pType) {
// TODO: FixMe: Support only 1 bit b/w, 8-16 bit gray and 8-16 bit/sample RGB
return true;
public boolean canEncodeImage(final ImageTypeSpecifier type) {
return TupleType.forPNM(type) != null;
}
public ImageWriter createWriterInstance(final Object pExtension) {
public ImageWriter createWriterInstance(final Object extension) {
return new PNMImageWriter(this);
}
@@ -30,7 +30,10 @@
package com.twelvemonkeys.imageio.plugins.pnm;
import javax.imageio.ImageTypeSpecifier;
import java.awt.*;
import java.awt.color.*;
import java.awt.image.*;
enum TupleType {
// Official:
@@ -80,4 +83,150 @@ enum TupleType {
public boolean isValidMaxSample(int maxSample) {
return maxSample >= minMaxSample && maxSample <= maxMaxSample;
}
static TupleType forPNM(Raster raster) {
return filterPNM(forPAM(raster));
}
static TupleType forPNM(ImageTypeSpecifier type) {
return filterPNM(forPAM(type));
}
private static TupleType filterPNM(TupleType tupleType) {
if (tupleType == null) {
return null;
}
switch (tupleType) {
case BLACKANDWHITE:
return BLACKANDWHITE_WHITE_IS_ZERO;
case GRAYSCALE:
case RGB:
return tupleType;
default:
return null;
}
}
static TupleType forPAM(Raster raster) {
SampleModel sampleModel = raster.getSampleModel();
switch (sampleModel.getTransferType()) {
case DataBuffer.TYPE_BYTE:
case DataBuffer.TYPE_USHORT:
case DataBuffer.TYPE_INT:
// B/W, Gray or RGB
int bands = sampleModel.getNumBands();
if (bands == 1 && sampleModel.getSampleSize(0) == 1) {
return TupleType.BLACKANDWHITE;
}
else if (bands == 2 && sampleModel.getSampleSize(0) == 1 && sampleModel.getSampleSize(1) == 1) {
return TupleType.BLACKANDWHITE_ALPHA;
}
// We can only write 8 or 16 bits/band
if (!(sampleModel.getSampleSize(0) == 8 || sampleModel.getSampleSize(0) == 16)) {
return null;
}
for (int i = 1; i < bands; i++) {
if (sampleModel.getSampleSize(0) != sampleModel.getSampleSize(i)) {
return null;
}
}
if (bands == 1) {
return TupleType.GRAYSCALE;
}
else if (bands == 2) {
return TupleType.GRAYSCALE_ALPHA;
}
else if (bands == 3) {
return TupleType.RGB;
}
else if (bands == 4) {
return TupleType.RGB_ALPHA;
}
// ...else fall through...
}
return null;
}
static TupleType forPAM(ImageTypeSpecifier type) {
// Support only 1 bit b/w, 8-16 bit gray and 8-16 bit/sample RGB
switch (type.getBufferedImageType()) {
// 1 bit b/w or b/w + a
case BufferedImage.TYPE_BYTE_BINARY:
switch (type.getNumBands()) {
case 1:
return type.getBitsPerBand(0) == 1 ? TupleType.BLACKANDWHITE : null;
case 2:
return type.getBitsPerBand(0) == 2 || type.getBitsPerBand(0) == 1 && type.getBitsPerBand(1) == 1 ? TupleType.BLACKANDWHITE_ALPHA : null;
default:
return null;
}
// Gray
case BufferedImage.TYPE_BYTE_GRAY:
case BufferedImage.TYPE_USHORT_GRAY:
return TupleType.GRAYSCALE;
// RGB
case BufferedImage.TYPE_3BYTE_BGR:
case BufferedImage.TYPE_INT_RGB:
case BufferedImage.TYPE_INT_BGR:
return TupleType.RGB;
// RGBA
case BufferedImage.TYPE_4BYTE_ABGR:
case BufferedImage.TYPE_4BYTE_ABGR_PRE:
case BufferedImage.TYPE_INT_ARGB:
case BufferedImage.TYPE_INT_ARGB_PRE:
return TupleType.RGB_ALPHA;
default:
// BYTE, USHORT or INT (packed)
switch (type.getSampleModel().getTransferType()) {
case DataBuffer.TYPE_BYTE:
case DataBuffer.TYPE_USHORT:
case DataBuffer.TYPE_INT:
// Gray or RGB
ColorModel colorModel = type.getColorModel();
if (!(colorModel instanceof IndexColorModel)) {
ColorSpace cs = colorModel.getColorSpace();
// We can only write 8 or 16 bits/band
int bands = type.getNumBands();
if (!(type.getBitsPerBand(0) == 8 || type.getBitsPerBand(0) == 16)) {
return null;
}
for (int i = 1; i < bands; i++) {
if (type.getBitsPerBand(0) != type.getBitsPerBand(i)) {
return null;
}
}
if (cs.getType() == ColorSpace.TYPE_GRAY && bands == 1) {
return TupleType.GRAYSCALE;
}
else if (cs.getType() == ColorSpace.TYPE_GRAY && colorModel.hasAlpha() && bands == 2) {
return TupleType.GRAYSCALE_ALPHA;
}
else if (cs.getType() == ColorSpace.TYPE_RGB && bands == 3) {
return TupleType.RGB;
}
else if (cs.getType() == ColorSpace.TYPE_RGB && colorModel.hasAlpha() && bands == 4) {
return TupleType.RGB_ALPHA;
}
else if (cs.getType() == ColorSpace.TYPE_CMYK && bands == 4) {
return TupleType.CMYK;
}
else if (cs.getType() == ColorSpace.TYPE_CMYK && colorModel.hasAlpha() && bands == 5) {
return TupleType.CMYK_ALPHA;
}
// ...else fall through...
}
}
}
return null;
}
}