package org.hipi.image.io; import org.hipi.image.HipiImageHeader; import org.hipi.image.HipiImageHeader.HipiImageFormat; import org.hipi.image.HipiImageHeader.HipiColorSpace; import org.hipi.image.HipiImage; import org.hipi.image.HipiImage.HipiImageType; import org.hipi.image.RasterImage; import org.hipi.image.HipiImageFactory; import org.hipi.image.PixelArray; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.util.ArrayList; /** * Extends {@link ImageCodec} and serves as both an {@link ImageDecoder} and * {@link ImageEncoder} for the PPM image storage format. */ public class PpmCodec extends ImageCodec { private static final PpmCodec staticObject = new PpmCodec(); public static PpmCodec getInstance() { return staticObject; } private class PpmHeader { public int width; public int height; public int numBands; public int maxValue; public ArrayList<String> comments = new ArrayList<String>(); public int streamOffset; // byte offset to start of image data public byte[] headerBytes = new byte[255]; } // private class PpmHeader private PpmHeader internalDecodeHeader(InputStream inputStream) throws IOException { PpmHeader ppmHeader = new PpmHeader(); ppmHeader.numBands = 3; inputStream.read(ppmHeader.headerBytes); // Only P6 supported. if (ppmHeader.headerBytes[0] != 'P' || ppmHeader.headerBytes[1] != '6') { byte[] format = new byte[2]; format[0] = ppmHeader.headerBytes[0]; format[1] = ppmHeader.headerBytes[1]; throw new IOException(String.format("PPM file has invalid or unsupported format [%s]. Only P6 is currently supported.", new String(format, "UTF-8"))); } int off = 3; // TODO: Fix this totally broken way to parse header. It assumes // the header ends after the third integer is parsed. This ignores // valid comment structure. ppmHeader.width = 0; while (ppmHeader.headerBytes[off] >= '0' && ppmHeader.headerBytes[off] <= '9') { ppmHeader.width = ppmHeader.width * 10 + (ppmHeader.headerBytes[off++] - '0'); } off++; ppmHeader.height = 0; while (ppmHeader.headerBytes[off] >= '0' && ppmHeader.headerBytes[off] <= '9') { ppmHeader.height = ppmHeader.height * 10 + (ppmHeader.headerBytes[off++] - '0'); } off++; ppmHeader.maxValue = 0; while (ppmHeader.headerBytes[off] >= '0' && ppmHeader.headerBytes[off] <= '9') { ppmHeader.maxValue = ppmHeader.maxValue * 10 + (ppmHeader.headerBytes[off++] - '0'); } off++; ppmHeader.streamOffset = off; // TODO: Add support for extracting header comments and return in // List<String> comments field. return ppmHeader; } public HipiImageHeader decodeHeader(InputStream inputStream, boolean includeExifData) throws IOException, IllegalArgumentException { PpmHeader ppmHeader = internalDecodeHeader(inputStream); if (ppmHeader.maxValue != 255) { throw new IOException(String.format("Only 8-bit PPMs are currently supported. Max value reported in PPM header is [%d].", ppmHeader.maxValue)); } if (includeExifData) { // TODO: Eventually, populate exifData map with comments // extracted from PPM header. throw new IllegalArgumentException("Support for extracting EXIF data from PPM files not implemented."); } return new HipiImageHeader(HipiImageFormat.PPM, HipiColorSpace.RGB, ppmHeader.width, ppmHeader.height, 3, null, null); } /* public HipiImage decodeImage(InputStream inputStream, HipiImageHeader imageHeader, HipiImageFactory imageFactory) throws IllegalArgumentException, IOException { if (!(imageFactory.getType() == HipiImageType.FLOAT || imageFactory.getType() == HipiImageType.BYTE)) { throw new IllegalArgumentException("PPM decoder supports only FloatImage and ByteImage output types."); } PpmHeader ppmHeader = internalDecodeHeader(inputStream); if (ppmHeader.maxValue != 255) { throw new IOException(String.format("Only 8-bit PPMs are currently supported. Max value reported in PPM header is [%d].", ppmHeader.maxValue)); } // Check that image dimensions in header match those in JPEG if (ppmHeader.width != imageHeader.getWidth() || ppmHeader.height != imageHeader.getHeight()) { throw new IllegalArgumentException("Image dimensions in header do not match those in PPM."); } if (ppmHeader.numBands != imageHeader.getNumBands()) { throw new IllegalArgumentException("Number of image bands specified in header does not match number found in PPM."); } int off = ppmHeader.streamOffset; // Create output image RasterImage image = null; try { image = (RasterImage)imageFactory.createImage(imageHeader); } catch (Exception e) { System.err.println(String.format("Unrecoverable exception while creating image object [%s]", e.getMessage())); e.printStackTrace(); System.exit(1); } PixelArray pa = image.getPixelArray(); int w = imageHeader.getWidth(); int h = imageHeader.getHeight(); byte[] rest = new byte[w * h * 3 - (255 - off)]; inputStream.read(rest); for (int i = 0; i < 255 - off; i++) { pa.setElemNonLinSRGB(i, ppmHeader.headerBytes[i + off] & 0xff); } for (int i = 0; i < w * h * 3 - (255 - off); i++) { pa.setElemNonLinSRGB(i + 255 - off, rest[i] & 0xff); } return image; } */ public void encodeImage(HipiImage image, OutputStream outputStream) throws IllegalArgumentException, IOException { if (!(RasterImage.class.isAssignableFrom(image.getClass()))) { throw new IllegalArgumentException("PPM encoder supports only RasterImage input types."); } if (image.getWidth() <= 0 || image.getHeight() <= 0) { throw new IllegalArgumentException("Invalid image resolution."); } if (image.getColorSpace() != HipiColorSpace.RGB) { throw new IllegalArgumentException("PPM encoder supports only linear RGB color space."); } if (image.getNumBands() != 3) { throw new IllegalArgumentException("PPM encoder supports only three band images."); } int w = image.getWidth(); int h = image.getHeight(); // http://netpbm.sourceforge.net/doc/ppm.html PrintWriter writer = new PrintWriter(outputStream); writer.print("P6\r"); writer.print(w + " " + h + "\r"); writer.print("255\r"); writer.flush(); PixelArray pa = ((RasterImage)image).getPixelArray(); byte[] raw = new byte[w*h*3]; for (int i=0; i<w*h*3; i++) { raw[i] = (byte)pa.getElemNonLinSRGB(i); } outputStream.write(raw); } }