TMI-PNM: Initial commit.

This commit is contained in:
Harald Kuhr
2014-10-01 14:13:04 +02:00
parent c6558d7433
commit eca8f84f6e
39 changed files with 10282 additions and 0 deletions
@@ -0,0 +1,43 @@
package com.twelvemonkeys.imageio.plugins.pnm;
import static com.twelvemonkeys.lang.Validate.notNull;
import java.io.IOException;
import javax.imageio.IIOException;
import javax.imageio.stream.ImageInputStream;
abstract class HeaderParser {
protected final ImageInputStream input;
protected HeaderParser(final ImageInputStream input) {
this.input = notNull(input);
}
public abstract PNMHeader parse() throws IOException;
public static PNMHeader parse(ImageInputStream input) throws IOException {
short type = input.readShort();
return createParser(input, type).parse();
}
private static HeaderParser createParser(final ImageInputStream input, final short type) throws IOException {
switch (type) {
case PNM.PBM_PLAIN:
case PNM.PBM:
case PNM.PGM_PLAIN:
case PNM.PGM:
case PNM.PPM_PLAIN:
case PNM.PPM:
return new PNMHeaderParser(input, type);
case PNM.PAM:
return new PAMHeaderParser(input);
case PNM.PFM_GRAY:
case PNM.PFM_RGB:
return new PFMHeaderParser(input, type);
default:
throw new IIOException("Unexpected type for PBM, PGM or PPM format: " + type);
}
}
}
@@ -0,0 +1,90 @@
package com.twelvemonkeys.imageio.plugins.pnm;
import java.awt.image.DataBuffer;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Locale;
import javax.imageio.IIOImage;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataFormatImpl;
import javax.imageio.metadata.IIOMetadataNode;
import javax.imageio.spi.ImageWriterSpi;
import javax.imageio.stream.ImageOutputStream;
import org.w3c.dom.NodeList;
abstract class HeaderWriter {
protected static final Charset UTF8 = Charset.forName("UTF8");
protected final ImageOutputStream imageOutput;
protected HeaderWriter(final ImageOutputStream imageOutput) {
this.imageOutput = imageOutput;
}
public static void write(final IIOImage image, final ImageWriterSpi provider, final ImageOutputStream imageOutput) throws IOException {
// TODO: This is somewhat sketchy...
if (provider.getFormatNames()[0].equals("pam")) {
new PAMHeaderWriter(imageOutput).writeHeader(image, provider);
}
else if (provider.getFormatNames()[0].equals("pnm")) {
new PNMHeaderWriter(imageOutput).writeHeader(image, provider);
}
else {
throw new AssertionError("Unsupported provider: " + provider);
}
}
public abstract void writeHeader(IIOImage image, final ImageWriterSpi provider) throws IOException;
protected final int getWidth(final IIOImage image) {
return image.hasRaster() ? image.getRaster().getWidth() : image.getRenderedImage().getWidth();
}
protected final int getHeight(final IIOImage image) {
return image.hasRaster() ? image.getRaster().getHeight() : image.getRenderedImage().getHeight();
}
protected final int getNumBands(final IIOImage image) {
return image.hasRaster() ? image.getRaster().getNumBands() : image.getRenderedImage().getSampleModel().getNumBands();
}
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);
}
}
protected final int getTransferType(final IIOImage image) {
return image.hasRaster() ? image.getRaster().getTransferType() : image.getRenderedImage().getSampleModel().getTransferType();
}
protected final void writeComments(final IIOMetadata metadata, final ImageWriterSpi provider) throws IOException {
// TODO: Only write creator if not already present
imageOutput.write(String.format("# CREATOR: %s %s\n", provider.getVendorName(), provider.getDescription(Locale.getDefault())).getBytes(UTF8));
// Comments from metadata
if (metadata != null && metadata.isStandardMetadataFormatSupported()) {
IIOMetadataNode root = (IIOMetadataNode) metadata.getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName);
NodeList textEntries = root.getElementsByTagName("TextEntry");
for (int i = 0; i < textEntries.getLength(); i++) {
// TODO: Write on the format "# KEYWORD: value" (if keyword != comment)?
IIOMetadataNode textEntry = (IIOMetadataNode) textEntries.item(i);
imageOutput.write(String.format("# %s", textEntry.getAttribute("value")).getBytes(UTF8));
}
}
}
}
@@ -0,0 +1,76 @@
package com.twelvemonkeys.imageio.plugins.pnm;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.imageio.IIOException;
import javax.imageio.stream.ImageInputStream;
final class PAMHeaderParser extends HeaderParser {
static final String ENDHDR = "ENDHDR";
static final String WIDTH = "WIDTH";
static final String HEIGHT = "HEIGHT";
static final String MAXVAL = "MAXVAL";
static final String DEPTH = "DEPTH";
static final String TUPLTYPE = "TUPLTYPE";
public PAMHeaderParser(final ImageInputStream input) {
super(input);
}
@Override public PNMHeader parse() throws IOException {
/* Note: Comments are allowed
P7
WIDTH 227
HEIGHT 149
DEPTH 3
MAXVAL 255
TUPLTYPE RGB
ENDHDR
*/
int width = -1;
int height = -1;
int depth = -1;
int maxVal = -1;
TupleType tupleType = null;
List<String> comments = new ArrayList<String>();
String line;
while ((line = input.readLine()) != null && !line.startsWith(ENDHDR)) {
line = line.trim();
if (line.isEmpty()) {
continue;
}
if (line.startsWith(WIDTH)) {
width = Integer.parseInt(line.substring(WIDTH.length() + 1));
}
else if (line.startsWith(HEIGHT)) {
height = Integer.parseInt(line.substring(HEIGHT.length() + 1));
}
else if (line.startsWith(DEPTH)) {
depth = Integer.parseInt(line.substring(DEPTH.length() + 1));
}
else if (line.startsWith(MAXVAL)) {
maxVal = Integer.parseInt(line.substring(MAXVAL.length() + 1));
}
else if (line.startsWith(TUPLTYPE)) {
tupleType = TupleType.valueOf(line.substring(TUPLTYPE.length() + 1));
}
else if (line.startsWith("#")) {
comments.add(line.substring(1).trim());
}
else {
throw new IIOException("Unknown PAM header token: '" + line + "'");
}
}
if (tupleType == null) {
// TODO: Assume a type, based on depth + maxVal, or at least, allow reading as raster
}
return new PNMHeader(PNM.PAM, tupleType, width, height, depth, maxVal, comments);
}
}
@@ -0,0 +1,31 @@
package com.twelvemonkeys.imageio.plugins.pnm;
import java.io.IOException;
import javax.imageio.IIOImage;
import javax.imageio.spi.ImageWriterSpi;
import javax.imageio.stream.ImageOutputStream;
final class PAMHeaderWriter extends HeaderWriter {
public PAMHeaderWriter(final ImageOutputStream imageOutput) {
super(imageOutput);
}
@Override public void writeHeader(final IIOImage image, final ImageWriterSpi provider) throws IOException {
// Write PAM magic
imageOutput.writeShort(PNM.PAM);
imageOutput.write('\n');
// Comments
writeComments(image.getMetadata(), provider);
// Write width/height and number of channels
imageOutput.write(String.format("WIDTH %s\nHEIGHT %s\n", getWidth(image), getHeight(image)).getBytes(UTF8));
imageOutput.write(String.format("DEPTH %s\n", getNumBands(image)).getBytes(UTF8));
// TODO: maxSample (8 or16 bit)
imageOutput.write(String.format("MAXVAL %s\n", getMaxVal(image)).getBytes(UTF8));
// 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(UTF8));
}
}
@@ -0,0 +1,52 @@
package com.twelvemonkeys.imageio.plugins.pnm;
import java.util.Locale;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageWriter;
import javax.imageio.spi.ImageWriterSpi;
import javax.imageio.stream.ImageOutputStream;
import com.twelvemonkeys.imageio.spi.ProviderInfo;
import com.twelvemonkeys.imageio.util.IIOUtil;
public final class PAMImageWriterSpi extends ImageWriterSpi {
/**
* Creates a {@code PAMImageWriterSpi}.
*/
public PAMImageWriterSpi() {
this(IIOUtil.getProviderInfo(PAMImageWriterSpi.class));
}
private PAMImageWriterSpi(final ProviderInfo pProviderInfo) {
super(
pProviderInfo.getVendorName(),
pProviderInfo.getVersion(),
new String[]{"pam", "PAM"},
new String[]{"pam"},
new String[]{
// No official IANA record exists, these are conventional
"image/x-portable-arbitrarymap" // PAM
},
"com.twelvemonkeys.imageio.plugins.pnm.PNMImageWriter",
new Class[] {ImageOutputStream.class},
new String[] {"com.twelvemonkeys.imageio.plugins.pnm.PNMImageReaderSpi"},
true, null, null, null, null,
true, null, null, null, null
);
}
public boolean canEncodeImage(final ImageTypeSpecifier pType) {
// TODO: FixMe
return true;
}
public ImageWriter createWriterInstance(final Object pExtension) {
return new PNMImageWriter(this);
}
@Override public String getDescription(final Locale locale) {
return "NetPBM Portable Arbitrary Map (PAM) image writer";
}
}
@@ -0,0 +1,88 @@
package com.twelvemonkeys.imageio.plugins.pnm;
import java.io.IOException;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.List;
import javax.imageio.IIOException;
import javax.imageio.stream.ImageInputStream;
final class PFMHeaderParser extends HeaderParser {
private final short fileType;
private final TupleType tupleType;
public PFMHeaderParser(final ImageInputStream input, final short type) {
super(input);
this.fileType = type;
this.tupleType = asTupleType(type);
}
static TupleType asTupleType(int fileType) {
switch (fileType) {
case PNM.PFM_GRAY:
return TupleType.GRAYSCALE;
case PNM.PFM_RGB:
return TupleType.RGB;
default:
throw new AssertionError("Illegal PNM type :" + fileType);
}
}
// http://netpbm.sourceforge.net/doc/pfm.html
// http://www.pauldebevec.com/Research/HDR/PFM/ (note that this is just one of *several* *incompatible* specs)
// The text header of a .pfm file takes the following form:
// [type]
// [xres] [yres]
// [scale/byte_order] where positive means big-endian, negative means little-endian, maxVal is abs(scale)
// Samples are 1 or 3 samples/pixels, interleaved, IEEE 32 bit floating point values
@Override 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
List<String> comments = new ArrayList<String>();
while (width == 0 || height == 0 || maxSample == 0) {
String line = input.readLine();
if (line == null) {
throw new IIOException("Unexpeced end of stream");
}
int commentStart = line.indexOf('#');
if (commentStart >= 0) {
String comment = line.substring(commentStart + 1).trim();
if (!comment.isEmpty()) {
comments.add(comment);
}
line = line.substring(0, commentStart);
}
line = line.trim();
if (!line.isEmpty()) {
// We have tokens...
String[] tokens = line.split("\\s");
for (String token : tokens) {
if (width == 0) {
width = Integer.parseInt(token);
} else if (height == 0) {
height = Integer.parseInt(token);
} else if (maxSample == 0) {
maxSample = Float.parseFloat(token);
} else {
throw new IIOException("Unknown PNM token: " + token);
}
}
}
}
return new PNMHeader(fileType, tupleType, width, height, tupleType.getSamplesPerPixel(), byteOrder(maxSample), comments);
}
private ByteOrder byteOrder(final float maxSample) {
return maxSample > 0 ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN;
}
}
@@ -0,0 +1,42 @@
package com.twelvemonkeys.imageio.plugins.pnm;
/**
* @see <a href="http://netpbm.sourceforge.net/doc/index.html#formats">The Netpbm Formats</a>.
*/
interface PNM {
/** 1 bit per sample, ASCII format, white is zero. */
short PBM_PLAIN = 'P' << 8 | '1';
/** Grayscale up to 16 bits per sample, ASCII format. */
short PGM_PLAIN = 'P' << 8 | '2';
/** Color (RGB) up to 16 bits per sample, ASCII format. */
short PPM_PLAIN = 'P' << 8 | '3';
/** 1 bit per sample, RAW format, white is zero. */
short PBM = 'P' << 8 | '4';
/** Grayscale up to 16 bits per sample, RAW format. */
short PGM = 'P' << 8 | '5';
/** Color (RGB) up to 16 bits per sample, RAW format. */
short PPM = 'P' << 8 | '6';
/**
* PAM format, may contain data in same formats as the above, has extended header.
* Always 1-16 bits per sample, RAW format.
* @see <a href="http://netpbm.sourceforge.net/doc/pam.html">PAM format</a>
*/
short PAM = 'P' << 8 | '7';
// Consider these for a future PFM (floating point) format
short PFM_RGB = 'P' << 8 | 'F'; // PPM_FLOAT? PFM?
short PFM_GRAY = 'P' << 8 | 'f'; // PGM_FLOAT? PfM?
/** Max value for 1 bit rasters (1). */
int MAX_VAL_1BIT = 1;
/** Max value for 8 bit rasters (255). */
int MAX_VAL_8BIT = 255;
/** Max value for 16 bit rasters (65535). */
int MAX_VAL_16BIT = 65535;
/** Max value for 32 bit rasters (4294967295). Experimental, not supported by the "spec". */
long MAX_VAL_32BIT = 4294967295L;
/** In order to not confuse PAM ("P7") with xv thumbnails. */
int XV_THUMBNAIL_MAGIC = (' ' << 24 | '3' << 16 | '3' << 8 | '2');;
}
@@ -0,0 +1,136 @@
package com.twelvemonkeys.imageio.plugins.pnm;
import static com.twelvemonkeys.lang.Validate.*;
import java.awt.image.DataBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
final class PNMHeader {
private final short fileType;
private final TupleType tupleType;
private final int width;
private final int height;
private final int maxSample;
private final List<String> comments;
private final ByteOrder byteOrder;
public PNMHeader(final short fileType, final TupleType tupleType, final int width, final int height, final int depth, final int maxSample, final Collection<String> comments) {
this.fileType = isTrue(isValidFileType(fileType), fileType, String.format("Illegal type: %s", PNMImageReader.asASCII(fileType)));
this.tupleType = notNull(tupleType, "tuple type may not be null");
this.width = isTrue(width > 0, width, "width must be greater than 0: %d");
this.height = isTrue(height > 0, height, "height must be greater than: %d");
isTrue(depth == tupleType.getSamplesPerPixel(), depth, String.format("incorrect depth for %s, expected %d: %d", tupleType, tupleType.getSamplesPerPixel(), depth));
this.maxSample = isTrue(tupleType.isValidMaxSample(maxSample), maxSample, "maxSample out of range: %d");
this.comments = Collections.unmodifiableList(new ArrayList<String>(comments));
byteOrder = ByteOrder.BIG_ENDIAN;
}
public PNMHeader(final short fileType, final TupleType tupleType, final int width, final int height, final int depth, final ByteOrder byteOrder, final Collection<String> comments) {
this.fileType = isTrue(isValidFileType(fileType), fileType, String.format("Illegal type: %s", PNMImageReader.asASCII(fileType)));
this.tupleType = notNull(tupleType, "tuple type may not be null");
this.width = isTrue(width > 0, width, "width must be greater than 0: %d");
this.height = isTrue(height > 0, height, "height must be greater than: %d");
isTrue(depth == tupleType.getSamplesPerPixel(), depth, String.format("incorrect depth for %s, expected %d: %d", tupleType, tupleType.getSamplesPerPixel(), depth));
this.maxSample = -1;
this.byteOrder = byteOrder;
this.comments = Collections.unmodifiableList(new ArrayList<String>(comments));
}
private boolean isValidFileType(final short fileType) {
return (fileType >= PNM.PBM_PLAIN && fileType <= PNM.PAM || fileType == PNM.PFM_GRAY || fileType == PNM.PFM_RGB);
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
public TupleType getTupleType() {
return tupleType;
}
public int getMaxSample() {
return maxSample;
}
public int getTransparency() {
return tupleType.getTransparency();
}
public int getSamplesPerPixel() {
return tupleType.getSamplesPerPixel();
}
public int getBitsPerSample() {
if (fileType == PNM.PFM_GRAY || fileType == PNM.PFM_RGB) {
return 32;
}
if (tupleType == TupleType.BLACKANDWHITE_WHITE_IS_ZERO) {
// Special case for PBM, PAM B/W uses 8 bits per sample for some reason
return 1;
}
if (maxSample <= PNM.MAX_VAL_8BIT) {
return 8;
}
if (maxSample <= PNM.MAX_VAL_16BIT) {
return 16;
}
if ((maxSample & 0xffffffffL) <= PNM.MAX_VAL_32BIT) {
return 32;
}
throw new AssertionError("maxSample exceeds 32 bit");
}
public int getTransferType() {
if (fileType == PNM.PFM_GRAY || fileType == PNM.PFM_RGB) {
return DataBuffer.TYPE_FLOAT;
}
if (maxSample <= PNM.MAX_VAL_8BIT) {
return DataBuffer.TYPE_BYTE;
}
if (maxSample <= PNM.MAX_VAL_16BIT) {
return DataBuffer.TYPE_USHORT;
}
if ((maxSample & 0xffffffffL) <= PNM.MAX_VAL_32BIT) {
return DataBuffer.TYPE_INT;
}
throw new AssertionError("maxSample exceeds 32 bit");
}
public List<String> getComments() {
return comments;
}
public short getFileType() {
return fileType;
}
public ByteOrder getByteOrder() {
return byteOrder;
}
@Override public String toString() {
return "PNMHeader{" +
"fileType=" + PNMImageReader.asASCII(fileType) +
", tupleType=" + tupleType +
", width=" + width +
", height=" + height +
(getTransferType() == DataBuffer.TYPE_FLOAT ? ", byteOrder=" + byteOrder : ", maxSample=" + maxSample) +
", comments=" + comments +
'}';
}
}
@@ -0,0 +1,81 @@
package com.twelvemonkeys.imageio.plugins.pnm;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.imageio.IIOException;
import javax.imageio.stream.ImageInputStream;
final class PNMHeaderParser extends HeaderParser {
private final short fileType;
private final TupleType tupleType;
public PNMHeaderParser(final ImageInputStream input, final short type) {
super(input);
this.fileType = type;
this.tupleType = asTupleType(type);
}
static TupleType asTupleType(int fileType) {
switch (fileType) {
case PNM.PBM:
case PNM.PBM_PLAIN:
return TupleType.BLACKANDWHITE_WHITE_IS_ZERO;
case PNM.PGM:
case PNM.PGM_PLAIN:
return TupleType.GRAYSCALE;
case PNM.PPM:
case PNM.PPM_PLAIN:
return TupleType.RGB;
default:
throw new AssertionError("Illegal PNM type :" + fileType);
}
}
@Override public PNMHeader parse() throws IOException {
int width = 0;
int height = 0;
int maxSample = tupleType == TupleType.BLACKANDWHITE_WHITE_IS_ZERO ? 1 : 0; // PBM has no maxSample line
List<String> comments = new ArrayList<String>();
while (width == 0 || height == 0 || maxSample == 0) {
String line = input.readLine();
if (line == null) {
throw new IIOException("Unexpeced end of stream");
}
int commentStart = line.indexOf('#');
if (commentStart >= 0) {
String comment = line.substring(commentStart + 1).trim();
if (!comment.isEmpty()) {
comments.add(comment);
}
line = line.substring(0, commentStart);
}
line = line.trim();
if (!line.isEmpty()) {
// We have tokens...
String[] tokens = line.split("\\s");
for (String token : tokens) {
if (width == 0) {
width = Integer.parseInt(token);
} else if (height == 0) {
height = Integer.parseInt(token);
} else if (maxSample == 0) {
maxSample = Integer.parseInt(token);
} else {
throw new IIOException("Unknown PNM token: " + token);
}
}
}
}
return new PNMHeader(fileType, tupleType, width, height, tupleType.getSamplesPerPixel(), maxSample, comments);
}
}
@@ -0,0 +1,32 @@
package com.twelvemonkeys.imageio.plugins.pnm;
import java.io.IOException;
import javax.imageio.IIOImage;
import javax.imageio.spi.ImageWriterSpi;
import javax.imageio.stream.ImageOutputStream;
final class PNMHeaderWriter extends HeaderWriter {
public PNMHeaderWriter(final ImageOutputStream imageOutput) {
super(imageOutput);
}
@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;
imageOutput.writeShort(type);
imageOutput.write('\n');
// Comments
writeComments(image.getMetadata(), provider);
// Dimensions (width/height)
imageOutput.write(String.format("%s %s\n", getWidth(image), getHeight(image)).getBytes(HeaderWriter.UTF8));
// MaxSample
if (type != PNM.PBM) {
imageOutput.write(String.format("%s\n", getMaxVal(image)).getBytes(HeaderWriter.UTF8));
}
}
}
@@ -0,0 +1,502 @@
package com.twelvemonkeys.imageio.plugins.pnm;
import java.awt.Rectangle;
import java.awt.Transparency;
import java.awt.color.ColorSpace;
import java.awt.image.BufferedImage;
import java.awt.image.ColorConvertOp;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.DataBufferFloat;
import java.awt.image.DataBufferUShort;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;
import java.io.DataInput;
import java.io.DataInputStream;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javax.imageio.IIOException;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import com.twelvemonkeys.imageio.ImageReaderBase;
import com.twelvemonkeys.imageio.color.ColorSpaces;
import com.twelvemonkeys.imageio.util.IIOUtil;
public final class PNMImageReader extends ImageReaderBase {
// TODO: Allow reading unknown tuple types as Raster!
// TODO: readAsRenderedImage?
private PNMHeader header;
PNMImageReader(final ImageReaderSpi provider) {
super(provider);
}
@Override protected void resetMembers() {
header = null;
}
private void readHeader() throws IOException {
if (header == null) {
header = HeaderParser.parse(imageInput);
imageInput.flushBefore(imageInput.getStreamPosition());
imageInput.setByteOrder(header.getByteOrder()); // For PFM support
} else {
imageInput.seek(imageInput.getFlushedPosition());
}
}
static String asASCII(final short type) {
byte[] asciiBytes = {(byte) ((type >> 8) & 0xff), (byte) (type & 0xff)};
return new String(asciiBytes, Charset.forName("ASCII"));
}
@Override public int getWidth(final int imageIndex) throws IOException {
checkBounds(imageIndex);
readHeader();
return header.getWidth();
}
@Override public int getHeight(final int imageIndex) throws IOException {
checkBounds(imageIndex);
readHeader();
return header.getHeight();
}
@Override public ImageTypeSpecifier getRawImageType(final int imageIndex) throws IOException {
checkBounds(imageIndex);
readHeader();
int bitsPerSample = header.getBitsPerSample();
int transferType = header.getTransferType();
int samplesPerPixel = header.getSamplesPerPixel();
boolean hasAlpha = header.getTransparency() != Transparency.OPAQUE;
switch (header.getTupleType()) {
case BLACKANDWHITE_WHITE_IS_ZERO:
// PBM: As TIFF WhiteIsZero
// NOTE: We handle this by inverting the values when reading, as Java has no ColorModel that easily supports this.
case BLACKANDWHITE_ALPHA:
case GRAYSCALE_ALPHA:
case BLACKANDWHITE:
case GRAYSCALE:
// PGM: Linear or non-linear gray?
ColorSpace gray = ColorSpace.getInstance(ColorSpace.CS_GRAY);
if (header.getTransferType() == DataBuffer.TYPE_FLOAT) {
return ImageTypeSpecifier.createInterleaved(gray, createBandOffsets(samplesPerPixel), transferType, hasAlpha, false);
}
if (header.getMaxSample() <= PNM.MAX_VAL_16BIT) {
return hasAlpha ? ImageTypeSpecifier.createGrayscale(bitsPerSample, transferType, false, false)
: ImageTypeSpecifier.createGrayscale(bitsPerSample, transferType, false);
}
return ImageTypeSpecifier.createInterleaved(gray, createBandOffsets(samplesPerPixel), transferType, hasAlpha, false);
case RGB:
case RGB_ALPHA:
// Using sRGB seems sufficient for PPM, as it is very close to ITU-R Recommendation BT.709 (same gamut and white point CIE D65)
ColorSpace sRGB = ColorSpace.getInstance(ColorSpace.CS_sRGB);
if (header.getTransferType() == DataBuffer.TYPE_FLOAT) {
return ImageTypeSpecifier.createInterleaved(sRGB, createBandOffsets(samplesPerPixel), transferType, hasAlpha, false);
}
return ImageTypeSpecifier.createInterleaved(sRGB, createBandOffsets(samplesPerPixel), transferType, hasAlpha, false);
case CMYK:
case CMYK_ALPHA:
ColorSpace cmyk = ColorSpaces.getColorSpace(ColorSpaces.CS_GENERIC_CMYK);
return ImageTypeSpecifier.createInterleaved(cmyk, createBandOffsets(samplesPerPixel), transferType, hasAlpha, false);
default:
// TODO: Allow reading unknown tuple types as Raster!
throw new AssertionError("Unknown PNM tuple type: " + header.getTupleType());
}
}
private int[] createBandOffsets(int numBands) {
int[] offsets = new int[numBands];
for (int i = 0; i < numBands; i++) {
offsets[i] = i;
}
return offsets;
}
@Override public Iterator<ImageTypeSpecifier> getImageTypes(final int imageIndex) throws IOException {
ImageTypeSpecifier rawType = getRawImageType(imageIndex);
List<ImageTypeSpecifier> specifiers = new ArrayList<ImageTypeSpecifier>();
switch (header.getTupleType()) {
case RGB:
if (header.getTransferType() == DataBuffer.TYPE_BYTE) {
specifiers.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_3BYTE_BGR));
specifiers.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_BGR));
specifiers.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_RGB));
}
break;
case RGB_ALPHA:
if (header.getTransferType() == DataBuffer.TYPE_BYTE) {
specifiers.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR));
// TODO: Why does ColorConvertOp choke on these (Ok, because it misinterprets the alpha channel for a color component, but how do we make it work)?
// specifiers.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_ARGB));
specifiers.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_4BYTE_ABGR_PRE));
// specifiers.add(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_ARGB_PRE));
}
break;
}
if (rawType != null) {
specifiers.add(rawType);
}
return specifiers.iterator();
}
@Override public BufferedImage read(final int imageIndex, final ImageReadParam param) throws IOException {
Iterator<ImageTypeSpecifier> imageTypes = getImageTypes(imageIndex);
ImageTypeSpecifier rawType = getRawImageType(imageIndex);
int width = getWidth(imageIndex);
int height = getHeight(imageIndex);
BufferedImage destination = getDestination(param, imageTypes, width, height);
Rectangle srcRegion = new Rectangle();
Rectangle destRegion = new Rectangle();
computeRegions(param, width, height, destination, srcRegion, destRegion);
WritableRaster destRaster = clipToRect(destination.getRaster(), destRegion, param != null ? param.getDestinationBands() : null);
checkReadParamBandSettings(param, rawType.getNumBands(), destRaster.getNumBands());
WritableRaster rowRaster = rawType.createBufferedImage(width, 1).getRaster();
// Clip to source region
Raster clippedRow = clipRowToRect(rowRaster, srcRegion,
param != null ? param.getSourceBands() : null,
param != null ? param.getSourceXSubsampling() : 1);
int transferType = rowRaster.getTransferType();
int samplesPerPixel = header.getSamplesPerPixel();
byte[] rowDataByte = null;
short[] rowDataUShort = null;
float[] rowDataFloat = null;
switch (transferType) {
case DataBuffer.TYPE_BYTE:
rowDataByte = ((DataBufferByte) rowRaster.getDataBuffer()).getData();
break;
case DataBuffer.TYPE_USHORT:
rowDataUShort = ((DataBufferUShort) rowRaster.getDataBuffer()).getData();
break;
case DataBuffer.TYPE_FLOAT:
rowDataFloat = ((DataBufferFloat) rowRaster.getDataBuffer()).getData();
break;
default:
throw new AssertionError("Unsupported transfer type: " + transferType);
}
ColorConvertOp colorConvert = null;
if (!destination.getColorModel().isCompatibleRaster(rowRaster)) {
colorConvert = new ColorConvertOp(rawType.getColorModel().getColorSpace(), destination.getColorModel().getColorSpace(), null);
}
int xSub = param == null ? 1 : param.getSourceXSubsampling();
int ySub = param == null ? 1 : param.getSourceYSubsampling();
DataInput input = wrapInput();
processImageStarted(imageIndex);
for (int y = 0; y < height; y++) {
switch (transferType) {
case DataBuffer.TYPE_BYTE:
readRowByte(destRaster, clippedRow, colorConvert, rowDataByte, samplesPerPixel, input, y, srcRegion, xSub, ySub);
break;
case DataBuffer.TYPE_USHORT:
readRowUShort(destRaster, clippedRow, rowDataUShort, samplesPerPixel, input, y, srcRegion, xSub, ySub);
break;
case DataBuffer.TYPE_FLOAT:
readRowFloat(destRaster, clippedRow, rowDataFloat, samplesPerPixel, input, y, srcRegion, xSub, ySub);
break;
default:
throw new AssertionError("Unsupported transfer type: " + transferType);
}
processImageProgress(100f * y / height);
if (abortRequested()) {
processReadAborted();
break;
}
if (y >= srcRegion.y + srcRegion.height) {
// We're done
break;
}
}
processImageComplete();
return destination;
}
private DataInput wrapInput() throws IIOException {
switch (header.getFileType()) {
case PNM.PBM_PLAIN:
return new DataInputStream(new Plain1BitDecoder(IIOUtil.createStreamAdapter(imageInput), header.getWidth() * header.getSamplesPerPixel()));
case PNM.PGM_PLAIN:
case PNM.PPM_PLAIN:
if (header.getBitsPerSample() <= 8) {
return new DataInputStream(new Plain8BitDecoder(IIOUtil.createStreamAdapter(imageInput)));
}
if (header.getBitsPerSample() <= 16) {
return new DataInputStream(new Plain16BitDecoder(IIOUtil.createStreamAdapter(imageInput)));
}
throw new IIOException("Unsupported bit depth for type: " + asASCII(header.getFileType()));
case PNM.PBM:
case PNM.PGM:
case PNM.PPM:
case PNM.PAM:
case PNM.PFM_GRAY:
case PNM.PFM_RGB:
return imageInput;
default:
throw new AssertionError("Unknown input type: " + asASCII(header.getFileType()));
}
}
private Raster clipRowToRect(final Raster raster, final Rectangle rect, final int[] bands, final int xSub) {
if (rect.contains(raster.getMinX(), 0, raster.getWidth(), 1)
&& xSub == 1
&& bands == null /* TODO: Compare bands with that of raster */) {
return raster;
}
return raster.createChild(rect.x / xSub, 0, rect.width / xSub, 1, 0, 0, bands);
}
private WritableRaster clipToRect(final WritableRaster raster, final Rectangle rect, final int[] bands) {
if (rect.contains(raster.getMinX(), raster.getMinY(), raster.getWidth(), raster.getHeight())
&& bands == null /* TODO: Compare bands with that of raster */) {
return raster;
}
return raster.createWritableChild(rect.x, rect.y, rect.width, rect.height, 0, 0, bands);
}
private void readRowByte(final WritableRaster destRaster,
Raster rowRaster,
final ColorConvertOp colorConvert,
final byte[] rowDataByte,
final int samplesPerPixel,
final DataInput input, final int y,
final Rectangle srcRegion,
final int xSub, final int ySub) throws IOException {
// If subsampled or outside source region, skip entire row
if (y % ySub != 0 || y < srcRegion.y || y >= srcRegion.y + srcRegion.height) {
input.skipBytes(rowDataByte.length);
return;
}
input.readFully(rowDataByte);
// Subsample (horizontal)
subsampleHorizontal(rowDataByte, rowDataByte.length, samplesPerPixel, xSub);
normalize(rowDataByte, 0, rowDataByte.length / xSub);
int destY = (y - srcRegion.y) / ySub;
if (colorConvert != null) {
colorConvert.filter(rowRaster, destRaster.createWritableChild(0, destY, rowRaster.getWidth(), 1, 0, 0, null));
} else {
destRaster.setDataElements(0, destY, rowRaster);
}
}
private void readRowUShort(final WritableRaster destRaster,
Raster rowRaster,
final short[] rowDataUShort,
final int samplesPerPixel, final DataInput input, final int y,
final Rectangle srcRegion, final int xSub, final int ySub) throws IOException {
// If subsampled or outside source region, skip entire row
if (y % ySub != 0 || y < srcRegion.y || y >= srcRegion.y + srcRegion.height) {
input.skipBytes(rowDataUShort.length * 2);
return;
}
readFully(input, rowDataUShort);
// Subsample (horizontal)
subsampleHorizontal(rowDataUShort, rowDataUShort.length, samplesPerPixel, xSub);
normalize(rowDataUShort);
int destY = (y - srcRegion.y) / ySub;
// TODO: ColorConvertOp if needed
destRaster.setDataElements(0, destY, rowRaster);
}
private void readRowFloat(final WritableRaster destRaster,
Raster rowRaster,
final float[] rowDataFloat,
final int samplesPerPixel, final DataInput input, final int y,
final Rectangle srcRegion, final int xSub, final int ySub) throws IOException {
// If subsampled or outside source region, skip entire row
if (y % ySub != 0 || y < srcRegion.y || y >= srcRegion.y + srcRegion.height) {
input.skipBytes(rowDataFloat.length * 4);
return;
}
readFully(input, rowDataFloat);
// Subsample (horizontal)
subsampleHorizontal(rowDataFloat, rowDataFloat.length, samplesPerPixel, xSub);
normalize(rowDataFloat);
int destY = (y - srcRegion.y) / ySub;
// TODO: ColorConvertOp if needed
destRaster.setDataElements(0, destY, rowRaster);
}
// TODO: Candidate util method
private static void readFully(final DataInput input, final short[] shorts) throws IOException {
if (input instanceof ImageInputStream) {
// Optimization for ImageInputStreams, read all in one go
((ImageInputStream) input).readFully(shorts, 0, shorts.length);
}
else {
for (int i = 0; i < shorts.length; i++) {
shorts[i] = input.readShort();
}
}
}
// TODO: Candidate util method
private static void readFully(final DataInput input, final float[] floats) throws IOException {
if (input instanceof ImageInputStream) {
// Optimization for ImageInputStreams, read all in one go
((ImageInputStream) input).readFully(floats, 0, floats.length);
}
else {
for (int i = 0; i < floats.length; i++) {
floats[i] = input.readFloat();
}
}
}
@SuppressWarnings("SuspiciousSystemArraycopy")
private void subsampleHorizontal(final Object data, final int length, final int samplesPerPixel, final int xSub) {
if (xSub == 1) {
return;
}
// TODO: Super-special 1 bit subsampling handling for PBM
for (int x = 0; x < length / xSub; x += samplesPerPixel) {
System.arraycopy(data, x * xSub, data, x, samplesPerPixel);
}
}
private void normalize(final byte[] rowData, final int start, final int length) {
switch (header.getTupleType()) {
case BLACKANDWHITE:
case BLACKANDWHITE_ALPHA:
// Do nothing
break;
case BLACKANDWHITE_WHITE_IS_ZERO:
// Invert
for (int i = start; i < length; i++) {
rowData[i] = (byte) ~rowData[i];
}
break;
case GRAYSCALE:
case GRAYSCALE_ALPHA:
case RGB:
case RGB_ALPHA:
case CMYK:
case CMYK_ALPHA:
// Normalize
for (int i = start; i < length; i++) {
rowData[i] = (byte) ((rowData[i] * PNM.MAX_VAL_8BIT) / header.getMaxSample());
}
break;
}
}
private void normalize(final short[] rowData) {
// Normalize
for (int i = 0; i < rowData.length; i++) {
rowData[i] = (short) ((rowData[i] * PNM.MAX_VAL_16BIT) / header.getMaxSample());
}
}
private void normalize(final float[] rowData) {
// TODO: Do the real thing, find min/max and normalize to range 0...255? But only if not reading raster..? Only support reading as raster?
// Normalize
for (int i = 0; i < rowData.length; i++) {
// if (rowData[i] > 275f /*header.getMaxSampleFloat()*/) {
// System.out.println("rowData[" + i + "]: " + rowData[i]);
// }
// rowData[i] = rowData[i] / 275f /*header.getMaxSampleFloat()*/;
}
}
@Override public IIOMetadata getImageMetadata(final int imageIndex) throws IOException {
checkBounds(imageIndex);
readHeader();
return new PNMMetadata(header);
}
public static void main(String[] args) throws IOException {
PNMImageReader reader = new PNMImageReader(null);
for (String arg : args) {
File in = new File(arg);
reader.setInput(ImageIO.createImageInputStream(in));
ImageReadParam param = reader.getDefaultReadParam();
param.setDestinationType(reader.getImageTypes(0).next());
// param.setSourceSubsampling(2, 3, 0, 0);
//
// int width = reader.getWidth(0);
// int height = reader.getHeight(0);
//
// param.setSourceRegion(new Rectangle(width / 4, height / 4, width / 2, height / 2));
// param.setSourceRegion(new Rectangle(width / 2, height / 2));
showIt(reader.read(0, param), in.getName());
// new XMLSerializer(System.out, System.getProperty("file.encoding")).serialize(reader.getImageMetadata(0).getAsTree(IIOMetadataFormatImpl.standardMetadataFormatName), false);
// File reference = new File(in.getParent() + "/../reference", in.getName().replaceAll("\\.p(a|b|g|p)m", ".png"));
// if (reference.exists()) {
// System.err.println("reference.getAbsolutePath(): " + reference.getAbsolutePath());
// showIt(ImageIO.read(reference), reference.getName());
// }
// break;
}
}
}
@@ -0,0 +1,92 @@
package com.twelvemonkeys.imageio.plugins.pnm;
import java.io.IOException;
import java.util.Locale;
import javax.imageio.ImageReader;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import com.twelvemonkeys.imageio.spi.ProviderInfo;
import com.twelvemonkeys.imageio.util.IIOUtil;
public final class PNMImageReaderSpi extends ImageReaderSpi {
/**
* Creates a {@code PNMImageReaderSpi}.
*/
public PNMImageReaderSpi() {
this(IIOUtil.getProviderInfo(PNMImageReaderSpi.class));
}
private PNMImageReaderSpi(final ProviderInfo providerInfo) {
super(
providerInfo.getVendorName(),
providerInfo.getVersion(),
new String[]{
"pnm", "pbm", "pgm", "ppm", "pam",
"PNM", "PBM", "PGM", "PPM", "PAM"
},
new String[]{"pbm", "pgm", "ppm", "pam"},
new String[]{
// No official IANA record exists, these are conventional
"image/x-portable-pixmap",
"image/x-portable-anymap",
"image/x-portable-arbitrarymap" // PAM
},
"com.twelvemkonkeys.imageio.plugins.pnm.PNMImageReader",
new Class[] {ImageInputStream.class},
new String[]{
"com.twelvemkonkeys.imageio.plugins.pnm.PNMImageWriterSpi",
"com.twelvemkonkeys.imageio.plugins.pnm.PAMImageWriterSpi"
},
true, // supports standard stream metadata
null, null, // native stream format name and class
null, null, // extra stream formats
true, // supports standard image metadata
null, null,
null, null // extra image metadata formats
);
}
@Override public boolean canDecodeInput(final Object source) throws IOException {
if (!(source instanceof ImageInputStream)) {
return false;
}
ImageInputStream stream = (ImageInputStream) source;
stream.mark();
try {
short magic = stream.readShort();
switch (magic) {
case PNM.PBM_PLAIN:
case PNM.PBM:
case PNM.PGM_PLAIN:
case PNM.PGM:
case PNM.PPM_PLAIN:
case PNM.PPM:
case PNM.PFM_GRAY:
case PNM.PFM_RGB:
return true;
case PNM.PAM:
return stream.readInt() != PNM.XV_THUMBNAIL_MAGIC;
default:
return false;
}
}
finally {
stream.reset();
}
}
@Override public ImageReader createReaderInstance(final Object extension) throws IOException {
return new PNMImageReader(this);
}
@Override public String getDescription(final Locale locale) {
return "NetPBM Portable Any Map (PNM and PAM) image reader";
}
}
@@ -0,0 +1,121 @@
package com.twelvemonkeys.imageio.plugins.pnm;
import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.awt.image.Raster;
import java.awt.image.SampleModel;
import java.io.File;
import java.io.IOException;
import java.util.Locale;
import javax.imageio.IIOException;
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.metadata.IIOMetadataFormatImpl;
import javax.imageio.metadata.IIOMetadataNode;
import javax.imageio.spi.ImageWriterSpi;
import com.twelvemonkeys.imageio.ImageWriterBase;
import org.w3c.dom.NodeList;
public final class PNMImageWriter extends ImageWriterBase {
PNMImageWriter(final ImageWriterSpi originatingProvider) {
super(originatingProvider);
}
@Override
public IIOMetadata getDefaultImageMetadata(final ImageTypeSpecifier imageType, final ImageWriteParam param) {
return null;
}
@Override
public IIOMetadata convertImageMetadata(final IIOMetadata inData, final ImageTypeSpecifier imageType, final ImageWriteParam param) {
return null;
}
@Override
public boolean canWriteRasters() {
return true;
}
@Override
public void write(final IIOMetadata streamMetadata, final IIOImage image, final ImageWriteParam param) throws IOException {
// 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, getOriginatingProvider(), imageOutput);
// TODO: Sub region
// TODO: Subsampling
// TODO: Source bands
processImageStarted(0);
writeImageData(image);
processImageComplete();
}
private void writeImageData(final IIOImage image) throws IOException {
// - 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);
SampleModel sampleModel = tile.getSampleModel();
DataBuffer dataBuffer = tile.getDataBuffer();
int tileWidth = tile.getWidth();
int tileHeight = tile.getHeight();
final int transferType = sampleModel.getTransferType();
Object data = null;
for (int y = 0; y < tileHeight; y++) {
data = sampleModel.getDataElements(0, y, tileWidth, 1, data, dataBuffer);
// TODO: Support other (short, float) data types
if (transferType == DataBuffer.TYPE_BYTE) {
imageOutput.write((byte[]) data);
}
else if (transferType == DataBuffer.TYPE_USHORT) {
short[] shortData = (short[]) data;
imageOutput.writeShorts(shortData, 0, shortData.length);
}
processImageProgress(y * 100f / tileHeight); // TODO: Take tile y into account
if (abortRequested()) {
processWriteAborted();
break;
}
}
}
public static void main(String[] args) throws IOException {
File input = new File(args[0]);
File output = new File(input.getParentFile(), input.getName().replace('.', '_') + ".ppm");
BufferedImage image = ImageIO.read(input);
if (image == null) {
System.err.println("input Image == null");
System.exit(-1);
}
System.out.println("image: " + image);
ImageWriter writer = new PNMImageWriterSpi().createWriterInstance();
if (!output.exists()) {
writer.setOutput(ImageIO.createImageOutputStream(output));
writer.write(image);
}
else {
System.err.println("Output file " + output + " already exists.");
System.exit(-1);
}
}
}
@@ -0,0 +1,58 @@
package com.twelvemonkeys.imageio.plugins.pnm;
import java.util.Locale;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageWriter;
import javax.imageio.spi.ImageWriterSpi;
import javax.imageio.stream.ImageOutputStream;
import com.twelvemonkeys.imageio.spi.ProviderInfo;
import com.twelvemonkeys.imageio.util.IIOUtil;
public final class PNMImageWriterSpi extends ImageWriterSpi {
// TODO: Consider one Spi for each sub-format, as it makes no sense for the writer to write PPM if client code requested PBM or PGM format.
// ...Then again, what if user asks for PNM? :-P
/**
* Creates a {@code PNMImageWriterSpi}.
*/
public PNMImageWriterSpi() {
this(IIOUtil.getProviderInfo(PNMImageWriterSpi.class));
}
private PNMImageWriterSpi(final ProviderInfo pProviderInfo) {
super(
pProviderInfo.getVendorName(),
pProviderInfo.getVersion(),
new String[]{
"pnm", "pbm", "pgm", "ppm",
"PNM", "PBM", "PGM", "PPM"
},
new String[]{"pbm", "pgm", "ppm"},
new String[]{
// No official IANA record exists, these are conventional
"image/x-portable-pixmap",
"image/x-portable-anymap"
},
"com.twelvemonkeys.imageio.plugins.pnm.PNMImageWriter",
new Class[] {ImageOutputStream.class},
new String[] {"com.twelvemonkeys.imageio.plugins.pnm.PNMImageReaderSpi"},
true, null, null, null, null,
true, null, null, null, null
);
}
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 ImageWriter createWriterInstance(final Object pExtension) {
return new PNMImageWriter(this);
}
@Override public String getDescription(final Locale locale) {
return "NetPBM Portable Any Map (PNM) image writer";
}
}
@@ -0,0 +1,182 @@
package com.twelvemonkeys.imageio.plugins.pnm;
import java.awt.Transparency;
import java.awt.image.DataBuffer;
import java.nio.ByteOrder;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataFormatImpl;
import javax.imageio.metadata.IIOMetadataNode;
import org.w3c.dom.Node;
final class PNMMetadata extends IIOMetadata {
// TODO: Clean up & extend AbstractMetadata (after moving from PSD -> Core)
private final PNMHeader header;
PNMMetadata(final PNMHeader header) {
this.header = header;
standardFormatSupported = true;
}
@Override public boolean isReadOnly() {
return true;
}
@Override public Node getAsTree(final String formatName) {
if (formatName.equals(IIOMetadataFormatImpl.standardMetadataFormatName)) {
return getStandardTree();
}
else {
throw new IllegalArgumentException("Unsupported metadata format: " + formatName);
}
}
@Override public void mergeTree(final String formatName, final Node root) {
if (isReadOnly()) {
throw new IllegalStateException("Metadata is read-only");
}
}
@Override public void reset() {
if (isReadOnly()) {
throw new IllegalStateException("Metadata is read-only");
}
}
@Override protected IIOMetadataNode getStandardChromaNode() {
IIOMetadataNode chroma = new IIOMetadataNode("Chroma");
IIOMetadataNode csType = new IIOMetadataNode("ColorSpaceType");
switch (header.getTupleType()) {
case BLACKANDWHITE:
case BLACKANDWHITE_ALPHA:
case BLACKANDWHITE_WHITE_IS_ZERO:
case GRAYSCALE:
case GRAYSCALE_ALPHA:
csType.setAttribute("name", "GRAY");
break;
case RGB:
case RGB_ALPHA:
csType.setAttribute("name", "RGB");
break;
case CMYK:
case CMYK_ALPHA:
csType.setAttribute("name", "CMYK");
break;
}
if (csType.getAttribute("name") != null) {
chroma.appendChild(csType);
}
IIOMetadataNode numChannels = new IIOMetadataNode("NumChannels");
numChannels.setAttribute("value", Integer.toString(header.getSamplesPerPixel()));
chroma.appendChild(numChannels);
// TODO: Might make sense to set gamma?
IIOMetadataNode blackIsZero = new IIOMetadataNode("BlackIsZero");
blackIsZero.setAttribute("value", header.getTupleType() == TupleType.BLACKANDWHITE_WHITE_IS_ZERO ? "FALSE" : "TRUE");
chroma.appendChild(blackIsZero);
return chroma;
}
// No compression
@Override protected IIOMetadataNode getStandardDataNode() {
IIOMetadataNode node = new IIOMetadataNode("Data");
IIOMetadataNode sampleFormat = new IIOMetadataNode("SampleFormat");
sampleFormat.setAttribute("value", header.getTransferType() == DataBuffer.TYPE_FLOAT ? "Real" : "UnsignedIntegral");
node.appendChild(sampleFormat);
IIOMetadataNode bitsPerSample = new IIOMetadataNode("BitsPerSample");
bitsPerSample.setAttribute("value", createListValue(header.getSamplesPerPixel(), Integer.toString(header.getBitsPerSample())));
node.appendChild(bitsPerSample);
IIOMetadataNode significantBitsPerSample = new IIOMetadataNode("SignificantBitsPerSample");
significantBitsPerSample.setAttribute("value", createListValue(header.getSamplesPerPixel(), Integer.toString(computeSignificantBits())));
node.appendChild(significantBitsPerSample);
String msb = header.getByteOrder() == ByteOrder.BIG_ENDIAN ? "0" : Integer.toString(header.getBitsPerSample() - 1);
IIOMetadataNode sampleMSB = new IIOMetadataNode("SampleMSB");
sampleMSB.setAttribute("value", createListValue(header.getSamplesPerPixel(), msb));
return node;
}
private int computeSignificantBits() {
if (header.getTransferType() == DataBuffer.TYPE_FLOAT) {
return header.getBitsPerSample();
}
int significantBits = 0;
int maxSample = header.getMaxSample();
while (maxSample > 0) {
maxSample >>>= 1;
significantBits++;
}
return significantBits;
}
private String createListValue(final int itemCount, final String... values) {
StringBuilder buffer = new StringBuilder();
for (int i = 0; i < itemCount; i++) {
if (buffer.length() > 0) {
buffer.append(' ');
}
buffer.append(values[i % values.length]);
}
return buffer.toString();
}
@Override protected IIOMetadataNode getStandardDimensionNode() {
IIOMetadataNode dimension = new IIOMetadataNode("Dimension");
IIOMetadataNode imageOrientation = new IIOMetadataNode("ImageOrientation");
imageOrientation.setAttribute("value", "Normal");
dimension.appendChild(imageOrientation);
return dimension;
}
// No document node
@Override protected IIOMetadataNode getStandardTextNode() {
if (!header.getComments().isEmpty()) {
IIOMetadataNode text = new IIOMetadataNode("Text");
for (String comment : header.getComments()) {
IIOMetadataNode textEntry = new IIOMetadataNode("TextEntry");
textEntry.setAttribute("keyword", "comment");
textEntry.setAttribute("value", comment);
text.appendChild(textEntry);
}
return text;
}
return null;
}
// No tiling
@Override protected IIOMetadataNode getStandardTransparencyNode() {
IIOMetadataNode transparency = new IIOMetadataNode("Transparency");
IIOMetadataNode alpha = new IIOMetadataNode("Alpha");
alpha.setAttribute("value", header.getTransparency() == Transparency.OPAQUE ? "none" : "nonpremultiplied");
transparency.appendChild(alpha);
return transparency;
}
}
@@ -0,0 +1,52 @@
package com.twelvemonkeys.imageio.plugins.pnm;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import com.twelvemonkeys.util.StringTokenIterator;
final class Plain16BitDecoder extends InputStream {
private final BufferedReader reader;
private StringTokenIterator currentLine;
private int leftOver = -1;
public Plain16BitDecoder(final InputStream in) {
reader = new BufferedReader(new InputStreamReader(in, Charset.forName("ASCII")));
}
@Override public int read() throws IOException {
if (leftOver != -1) {
int next = leftOver;
leftOver = -1;
return next;
}
// Each number is one byte. Skip whitespace.
if (currentLine == null || !currentLine.hasNext()) {
String line = reader.readLine();
if (line == null) {
return -1;
}
currentLine = new StringTokenIterator(line);
if (!currentLine.hasNext()) {
return -1;
}
}
int next = Integer.parseInt(currentLine.next()) & 0xffff;
leftOver = next & 0xff;
return (next >> 8) & 0xff;
}
@Override public void close() throws IOException {
reader.close();
}
}
@@ -0,0 +1,52 @@
package com.twelvemonkeys.imageio.plugins.pnm;
import java.io.IOException;
import java.io.InputStream;
final class Plain1BitDecoder extends InputStream {
private final InputStream stream;
private final int samplesPerRow; // Padded to byte boundary
private int pos = 0;
public Plain1BitDecoder(final InputStream in, final int samplesPerRow) {
this.stream = in;
this.samplesPerRow = samplesPerRow;
}
@Override public int read() throws IOException {
// Each 0 or 1 represents one bit, whitespace is ignored. Padded to byte boundary for each row.
// NOTE: White is 0, black is 1!
int result = 0;
for (int bitPos = 7; bitPos >= 0; bitPos--) {
int read;
while ((read = stream.read()) != -1 && Character.isWhitespace(read)) {
// Skip whitespace
}
if (read == -1) {
if (bitPos == 7) {
return -1;
}
break;
}
int val = read - '0';
result |= val << bitPos;
if (++pos >= samplesPerRow) {
pos = 0;
break;
}
}
return result;
}
@Override public void close() throws IOException {
stream.close();
}
}
@@ -0,0 +1,41 @@
package com.twelvemonkeys.imageio.plugins.pnm;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import com.twelvemonkeys.util.StringTokenIterator;
final class Plain8BitDecoder extends InputStream {
private final BufferedReader reader;
private StringTokenIterator currentLine;
public Plain8BitDecoder(final InputStream in) {
reader = new BufferedReader(new InputStreamReader(in, Charset.forName("ASCII")));
}
@Override public int read() throws IOException {
// Each number is one byte. Skip whitespace.
if (currentLine == null || !currentLine.hasNext()) {
String line = reader.readLine();
if (line == null) {
return -1;
}
currentLine = new StringTokenIterator(line);
if (!currentLine.hasNext()) {
return -1;
}
}
return Integer.parseInt(currentLine.next()) & 0xff;
}
@Override public void close() throws IOException {
reader.close();
}
}
@@ -0,0 +1,54 @@
package com.twelvemonkeys.imageio.plugins.pnm;
import java.awt.Transparency;
enum TupleType {
// Official:
/** B/W, but uses 1 byte (8 bits) per pixel. Black is zero (oposite of PBM) */
BLACKANDWHITE(1, 1, PNM.MAX_VAL_1BIT, Transparency.OPAQUE),
/** B/W + bit mask, uses 2 bytes per pixel. Black is zero (oposite of PBM) */
BLACKANDWHITE_ALPHA(2, PNM.MAX_VAL_1BIT, PNM.MAX_VAL_1BIT, Transparency.BITMASK),
/** Grayscale, as PGM. */
GRAYSCALE(1, 2, PNM.MAX_VAL_16BIT, Transparency.OPAQUE),
/** Grayscale + alpha. YA order. */
GRAYSCALE_ALPHA(2, 2, PNM.MAX_VAL_16BIT, Transparency.TRANSLUCENT),
/** RGB color, as PPM. RGB order. */
RGB(3, 1, PNM.MAX_VAL_16BIT, Transparency.OPAQUE),
/** RGB color + alpha. RGBA order. */
RGB_ALPHA(4, 1, PNM.MAX_VAL_16BIT, Transparency.TRANSLUCENT),
// De facto (documented on the interwebs):
/** CMYK color. CMYK order. */
CMYK(4, 2, PNM.MAX_VAL_16BIT, Transparency.OPAQUE),
/** CMYK color + alpha. CMYKA order. */
CMYK_ALPHA(5, 1, PNM.MAX_VAL_16BIT, Transparency.TRANSLUCENT),
// Custom for PBM compatibility
/** 1 bit B/W. White is zero (as PBM) */
BLACKANDWHITE_WHITE_IS_ZERO(1, 1, PNM.MAX_VAL_1BIT, Transparency.OPAQUE);
private final int samplesPerPixel;
private final int minMaxSample;
private final int maxMaxSample;
private final int transparency;
TupleType(int samplesPerPixel, int minMaxSample, int maxMaxSample, int transparency) {
this.samplesPerPixel = samplesPerPixel;
this.minMaxSample = minMaxSample;
this.maxMaxSample = maxMaxSample;
this.transparency = transparency;
}
public int getTransparency() {
return transparency;
}
public int getSamplesPerPixel() {
return samplesPerPixel;
}
public boolean isValidMaxSample(int maxSample) {
return maxSample >= minMaxSample && maxSample <= maxMaxSample;
}
}
@@ -0,0 +1 @@
com.twelvemonkeys.imageio.plugins.pnm.PNMImageReaderSpi
@@ -0,0 +1,2 @@
com.twelvemonkeys.imageio.plugins.pnm.PNMImageWriterSpi
com.twelvemonkeys.imageio.plugins.pnm.PAMImageWriterSpi