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

25
imageio/imageio-pnm/license.txt Executable file
View File

@ -0,0 +1,25 @@
Copyright (c) 2014, Harald Kuhr
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name "TwelveMonkeys" nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

29
imageio/imageio-pnm/pom.xml Executable file
View File

@ -0,0 +1,29 @@
<?xml version="1.0"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio</artifactId>
<version>3.1-SNAPSHOT</version>
</parent>
<artifactId>imageio-pnm</artifactId>
<name>TwelveMonkeys :: ImageIO :: PNM plugin</name>
<description>
ImageIO plugin for NetPBM Portable Any Map (PNM)
</description>
<dependencies>
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio-core</artifactId>
</dependency>
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio-core</artifactId>
<classifier>tests</classifier>
</dependency>
</dependencies>
</project>

View File

@ -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);
}
}
}

View File

@ -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));
}
}
}
}

View File

@ -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);
}
}

View File

@ -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));
}
}

View File

@ -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";
}
}

View File

@ -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;
}
}

View File

@ -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');;
}

View File

@ -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 +
'}';
}
}

View File

@ -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);
}
}

View File

@ -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));
}
}
}

View File

@ -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;
}
}
}

View File

@ -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";
}
}

View File

@ -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);
}
}
}

View File

@ -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";
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1 @@
com.twelvemonkeys.imageio.plugins.pnm.PNMImageReaderSpi

View File

@ -0,0 +1,2 @@
com.twelvemonkeys.imageio.plugins.pnm.PNMImageWriterSpi
com.twelvemonkeys.imageio.plugins.pnm.PAMImageWriterSpi

View File

@ -0,0 +1,110 @@
package com.twelvemonkeys.imageio.plugins.pnm;
import com.twelvemonkeys.imageio.util.ImageReaderAbstractTestCase;
import org.junit.Test;
import javax.imageio.ImageReader;
import javax.imageio.spi.ImageReaderSpi;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.*;
public class PNMImageReaderTest extends ImageReaderAbstractTestCase<PNMImageReader>{
@Override protected List<TestData> getTestData() {
return Arrays.asList(
new TestData(getClassLoaderResource("/ppm/lena.ppm"), new Dimension(128, 128)), // P6 (PPM RAW)
new TestData(getClassLoaderResource("/ppm/colors.ppm"), new Dimension(3, 2)), // P3 (PPM PLAIN)
new TestData(getClassLoaderResource("/pbm/j.pbm"), new Dimension(6, 10)), // P1 (PBM PLAIN)
new TestData(getClassLoaderResource("/pgm/feep.pgm"), new Dimension(24, 7)), // P2 (PGM PLAIN)
new TestData(getClassLoaderResource("/pgm/feep16.pgm"), new Dimension(24, 7)), // P2 (PGM PLAIN, 16 bits/sample)
new TestData(getClassLoaderResource("/pgm/house.l.pgm"), new Dimension(367, 241)), // P5 (PGM RAW)
new TestData(getClassLoaderResource("/ppm/lighthouse_rgb48.ppm"), new Dimension(768, 512)), // P6 (PPM RAW, 16 bits/sample)
new TestData(getClassLoaderResource("/pam/lena.pam"), new Dimension(128, 128)), // P7 RGB
new TestData(getClassLoaderResource("/pam/rgba.pam"), new Dimension(4, 2)) // P7 RGB_ALPHA
);
}
@Override protected ImageReaderSpi createProvider() {
return new PNMImageReaderSpi();
}
@Override protected Class<PNMImageReader> getReaderClass() {
return PNMImageReader.class;
}
@Override protected PNMImageReader createReader() {
return new PNMImageReader(createProvider());
}
@Override protected List<String> getFormatNames() {
return Arrays.asList(
"pnm", "pbm", "pgm", "ppm", "pam",
"PNM", "PBM", "PGM", "PPM", "PAM"
);
}
@Override protected List<String> getSuffixes() {
return Arrays.asList(
"pbm", "pgm", "ppm", "pam"
);
}
@Override protected List<String> getMIMETypes() {
return Arrays.asList(
"image/x-portable-pixmap",
"image/x-portable-anymap",
"image/x-portable-arbitrarymap"
);
}
@Test
public void testColorsVsReference() throws IOException {
ImageReader reader = createReader();
TestData data = new TestData(getClassLoaderResource("/ppm/colors.ppm"), new Dimension(3, 2));
reader.setInput(data.getInputStream());
BufferedImage expected = new BufferedImage(3, 2, BufferedImage.TYPE_3BYTE_BGR);
expected.setRGB(0, 0, new Color(255, 0, 0).getRGB());
expected.setRGB(1, 0, new Color(0, 255, 0).getRGB());
expected.setRGB(2, 0, new Color(0, 0, 255).getRGB());
expected.setRGB(0, 1, new Color(255, 255, 0).getRGB());
expected.setRGB(1, 1, new Color(255, 255, 255).getRGB());
expected.setRGB(2, 1, new Color(0, 0, 0).getRGB());
assertImageDataEquals("Images differ from reference", expected, reader.read(0));
}
@Test
public void testRGBAVsReference() throws IOException {
ImageReader reader = createReader();
TestData data = new TestData(getClassLoaderResource("/pam/rgba.pam"), new Dimension(4, 2));
reader.setInput(data.getInputStream());
BufferedImage expected = new BufferedImage(4, 2, BufferedImage.TYPE_4BYTE_ABGR);
expected.setRGB(0, 0, new Color(0, 0, 255).getRGB());
expected.setRGB(1, 0, new Color(0, 255, 0).getRGB());
expected.setRGB(2, 0, new Color(255, 0, 0).getRGB());
expected.setRGB(3, 0, new Color(255, 255, 255).getRGB());
expected.setRGB(0, 1, new Color(0, 0, 255, 127).getRGB());
expected.setRGB(1, 1, new Color(0, 255, 0, 127).getRGB());
expected.setRGB(2, 1, new Color(255, 0, 0, 127).getRGB());
expected.setRGB(3, 1, new Color(255, 255, 255, 127).getRGB());
assertImageDataEquals("Images differ from reference", expected, reader.read(0));
}
@Test
public void testXVThumbNotIncorrectlyRecognizedAsPAM() throws IOException {
ImageReaderSpi provider = createProvider();
assertTrue("Should recognize PAM format", provider.canDecodeInput(new TestData(getClassLoaderResource("/pam/rgba.pam"), new Dimension()).getInputStream())); // Sanity
assertFalse("Should distinguish xv-thumbs from PAM format", provider.canDecodeInput(new TestData(getClassLoaderResource("/xv-thumb/xv-thumb.xvt"), new Dimension()).getInputStream()));
}
}

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,13 @@
P1
# This is an example bitmap of the letter "J"
6 10
0 0 0 0 1 0
0 0 0 0 1 0
0 0 0 0 1 0
0 0 0 0 1 0
0 0 0 0 1 0
0 0 0 0 1 0
1 0 0 0 1 0
0 1 1 1 0 0
0 0 0 0 0 0
0 0 0 0 0 0

Binary file not shown.

View File

@ -0,0 +1,11 @@
P2
# Shows the word "FEEP" (example from Netpbm man page on PGM)
24 7
15
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 3 3 3 3 0 0 7 7 7 7 0 0 11 11 11 11 0 0 15 15 15 15 0
0 3 0 0 0 0 0 7 0 0 0 0 0 11 0 0 0 0 0 15 0 0 15 0
0 3 3 3 0 0 0 7 7 7 0 0 0 11 11 11 0 0 0 15 15 15 15 0
0 3 0 0 0 0 0 7 0 0 0 0 0 11 0 0 0 0 0 15 0 0 0 0
0 3 0 0 0 0 0 7 7 7 7 0 0 11 11 11 11 0 0 15 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

View File

@ -0,0 +1,14 @@
P2
# Shows the word "FEEP" (modified example from Netpbm man page on PGM)
24 7
4095
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 1023 1023 1023 1023 0 0 2047 2047 2047 2047 0 0 3071
3071 3071 3071 0 0 4095 4095 4095 4095 0
0 1023 0 0 0 0 0 2047 0 0 0 0 0 3071 0 0 0 0 0 4095
0 0 4095 0 0 1023 1023 1023 0 0 0 2047 2047 2047 0 0 0
3071 3071 3071 0 0 0 4095 4095 4095 4095 0
0 1023 0 0 0 0 0 2047 0 0 0 0 0 3071 0 0 0 0 0 4095
0 0 0 0 0 1023 0 0 0 0 0 2047 2047 2047 2047 0 0 3071
3071 3071 3071 0 0 4095 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,7 @@
P3
# The P3 means colors are in ASCII, then 3 columns and 2 rows,
# then 255 for max color, then RGB triplets
3 2
255
255 0 0 0 255 0 0 0 255
255 255 0 255 255 255 0 0 0

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,2 @@
P7 332
Garbage here, that we don't really care about.

View File

@ -34,6 +34,7 @@
<module>imageio-jpeg</module>
<module>imageio-pdf</module>
<module>imageio-pict</module>
<module>imageio-pnm</module>
<module>imageio-psd</module>
<module>imageio-thumbsdb</module>
<module>imageio-tiff</module>