diff --git a/imageio/imageio-hdr/pom.xml b/imageio/imageio-hdr/pom.xml new file mode 100644 index 00000000..dae0b4fb --- /dev/null +++ b/imageio/imageio-hdr/pom.xml @@ -0,0 +1,30 @@ + + + 4.0.0 + + com.twelvemonkeys.imageio + imageio + 3.2-SNAPSHOT + + imageio-hdr + TwelveMonkeys :: ImageIO :: HDR plugin + + ImageIO plugin for Radiance RGBE High Dynaimc Range format (HDR). + + + + + com.twelvemonkeys.imageio + imageio-core + + + com.twelvemonkeys.imageio + imageio-core + tests + + + com.twelvemonkeys.imageio + imageio-metadata + + + diff --git a/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/HDR.java b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/HDR.java new file mode 100644 index 00000000..f85f4b9a --- /dev/null +++ b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/HDR.java @@ -0,0 +1,13 @@ +package com.twelvemonkeys.imageio.plugins.hdr; + +/** + * HDR. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: HDR.java,v 1.0 27/07/15 harald.kuhr Exp$ + */ +interface HDR { + byte[] RADIANCE_MAGIC = new byte[] {'#', '?', 'R', 'A', 'D', 'I', 'A', 'N', 'C', 'E'}; + byte[] RGBE_MAGIC = new byte[] {'#', '?', 'R', 'G', 'B', 'E'}; +} diff --git a/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/HDRHeader.java b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/HDRHeader.java new file mode 100644 index 00000000..b7fe68a9 --- /dev/null +++ b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/HDRHeader.java @@ -0,0 +1,95 @@ +package com.twelvemonkeys.imageio.plugins.hdr; + +import javax.imageio.IIOException; +import javax.imageio.stream.ImageInputStream; +import java.io.IOException; + +/** + * HDRHeader. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: HDRHeader.java,v 1.0 27/07/15 harald.kuhr Exp$ + */ +final class HDRHeader { + private static final String KEY_FORMAT = "FORMAT="; + private static final String KEY_PRIMARIES = "PRIMARIES="; + private static final String KEY_EXPOSURE = "EXPOSURE="; + private static final String KEY_GAMMA = "GAMMA="; + private static final String KEY_SOFTWARE = "SOFTWARE="; + + private int width; + private int height; + + private String software; + + public static HDRHeader read(final ImageInputStream stream) throws IOException { + HDRHeader header = new HDRHeader(); + + while (true) { + String line = stream.readLine().trim(); + + if (line.isEmpty()) { + // This is the last line before the dimensions + break; + } + + if (line.startsWith("#?")) { + // Program specifier, don't need that... + } + else if (line.startsWith("#")) { + // Comment (ignore) + } + else if (line.startsWith(KEY_FORMAT)) { + String format = line.substring(KEY_FORMAT.length()).trim(); + + if (!format.equals("32-bit_rle_rgbe")) { + throw new IIOException("Unsupported format \"" + format + "\"(expected \"32-bit_rle_rgbe\")"); + } + // TODO: Support the 32-bit_rle_xyze format + } + else if (line.startsWith(KEY_PRIMARIES)) { + // TODO: We are going to need these values... + // Should contain 8 (RGB + white point) coordinates + } + else if (line.startsWith(KEY_EXPOSURE)) { + // TODO: We are going to need these values... + } + else if (line.startsWith(KEY_GAMMA)) { + // TODO: We are going to need these values... + } + else if (line.startsWith(KEY_SOFTWARE)) { + header.software = line.substring(KEY_SOFTWARE.length()).trim(); + } + else { + // ...ignore + } + } + + // TODO: Proper parsing of width/height and orientation! + String dimensionsLine = stream.readLine().trim(); + String[] dims = dimensionsLine.split("\\s"); + + if (dims[0].equals("-Y") && dims[2].equals("+X")) { + header.height = Integer.parseInt(dims[1]); + header.width = Integer.parseInt(dims[3]); + + return header; + } + else { + throw new IIOException("Unsupported RGBE orientation (expected \"-Y ... +X ...\")"); + } + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + public String getSoftware() { + return software; + } +} diff --git a/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/HDRImageReadParam.java b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/HDRImageReadParam.java new file mode 100644 index 00000000..6a8ee9ea --- /dev/null +++ b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/HDRImageReadParam.java @@ -0,0 +1,27 @@ +package com.twelvemonkeys.imageio.plugins.hdr; + +import com.twelvemonkeys.imageio.plugins.hdr.tonemap.DefaultToneMapper; +import com.twelvemonkeys.imageio.plugins.hdr.tonemap.ToneMapper; + +import javax.imageio.ImageReadParam; + +/** + * HDRImageReadParam. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: HDRImageReadParam.java,v 1.0 28/07/15 harald.kuhr Exp$ + */ +public final class HDRImageReadParam extends ImageReadParam { + static final ToneMapper DEFAULT_TONE_MAPPER = new DefaultToneMapper(.1f); + + private ToneMapper toneMapper = DEFAULT_TONE_MAPPER; + + public ToneMapper getToneMapper() { + return toneMapper; + } + + public void setToneMapper(final ToneMapper toneMapper) { + this.toneMapper = toneMapper != null ? toneMapper : DEFAULT_TONE_MAPPER; + } +} diff --git a/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/HDRImageReader.java b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/HDRImageReader.java new file mode 100644 index 00000000..0513cf86 --- /dev/null +++ b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/HDRImageReader.java @@ -0,0 +1,166 @@ +package com.twelvemonkeys.imageio.plugins.hdr; + +import com.twelvemonkeys.imageio.ImageReaderBase; +import com.twelvemonkeys.imageio.plugins.hdr.tonemap.ToneMapper; +import com.twelvemonkeys.imageio.util.ImageTypeSpecifiers; + +import javax.imageio.ImageIO; +import javax.imageio.ImageReadParam; +import javax.imageio.ImageTypeSpecifier; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.spi.ImageReaderSpi; +import java.awt.*; +import java.awt.color.ColorSpace; +import java.awt.image.BufferedImage; +import java.awt.image.DataBuffer; +import java.awt.image.WritableRaster; +import java.io.File; +import java.io.IOException; +import java.util.Collections; +import java.util.Iterator; + +/** + * HDRImageReader. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: HDRImageReader.java,v 1.0 27/07/15 harald.kuhr Exp$ + */ +public final class HDRImageReader extends ImageReaderBase { + // Specs: http://radsite.lbl.gov/radiance/refer/filefmts.pdf + + private HDRHeader header; + + protected HDRImageReader(final ImageReaderSpi provider) { + super(provider); + } + + @Override + protected void resetMembers() { + header = null; + } + + private void readHeader() throws IOException { + if (header == null) { + header = HDRHeader.read(imageInput); + + imageInput.flushBefore(imageInput.getStreamPosition()); + } + + imageInput.seek(imageInput.getFlushedPosition()); + } + + @Override + public int getWidth(int imageIndex) throws IOException { + checkBounds(imageIndex); + readHeader(); + + return header.getWidth(); + } + + @Override + public int getHeight(int imageIndex) throws IOException { + checkBounds(imageIndex); + readHeader(); + + return header.getHeight(); + } + + @Override + public Iterator getImageTypes(int imageIndex) throws IOException { + checkBounds(imageIndex); + readHeader(); + + ColorSpace sRGB = ColorSpace.getInstance(ColorSpace.CS_sRGB); + return Collections.singletonList(ImageTypeSpecifiers.createInterleaved(sRGB, new int[] {0, 1, 2}, DataBuffer.TYPE_FLOAT, false, false)).iterator(); + } + + @Override + public BufferedImage read(final int imageIndex, final ImageReadParam param) throws IOException { + checkBounds(imageIndex); + readHeader(); + + int width = getWidth(imageIndex); + int height = getHeight(imageIndex); + + BufferedImage destination = getDestination(param, getImageTypes(imageIndex), width, height); + + Rectangle srcRegion = new Rectangle(); + Rectangle destRegion = new Rectangle(); + computeRegions(param, width, height, destination, srcRegion, destRegion); + + WritableRaster raster = destination.getRaster() + .createWritableChild(destRegion.x, destRegion.y, destRegion.width, destRegion.height, 0, 0, null); + + int xSub = param != null ? param.getSourceXSubsampling() : 1; + int ySub = param != null ? param.getSourceYSubsampling() : 1; + + // Allow pluggable tone mapper via ImageReadParam + ToneMapper toneMapper = param instanceof HDRImageReadParam + ? ((HDRImageReadParam) param).getToneMapper() + : HDRImageReadParam.DEFAULT_TONE_MAPPER; + + byte[] rowRGBE = new byte[width * 4]; + float[] rgb = new float[3]; + + processImageStarted(imageIndex); + + // Process one scanline of RGBE data at a time + for (int srcY = 0; srcY < height; srcY++) { + int dstY = ((srcY - srcRegion.y) / ySub) + destRegion.y; + if (dstY >= destRegion.height) { + break; + } + + RGBE.readPixelsRawRLE(imageInput, rowRGBE, 0, width, 1); + + if (srcY % ySub == 0 && dstY >= destRegion.y) { + for (int srcX = srcRegion.x; srcX < srcRegion.x + srcRegion.width; srcX += xSub) { + int dstX = ((srcX - srcRegion.x) / xSub) + destRegion.x; + if (dstX >= destRegion.width) { + break; + } + + RGBE.rgbe2float(rgb, rowRGBE, srcX * 4); + + // Map/clamp RGB values into visible range, normally [0...1] + toneMapper.map(rgb); + + raster.setDataElements(dstX, dstY, rgb); + } + } + + processImageProgress(srcY * 100f / height); + + if (abortRequested()) { + processReadAborted(); + break; + } + } + + processImageComplete(); + + return destination; + } + + @Override + public ImageReadParam getDefaultReadParam() { + return new HDRImageReadParam(); + } + + @Override + public IIOMetadata getImageMetadata(int imageIndex) throws IOException { + checkBounds(imageIndex); + readHeader(); + + return new HDRMetadata(header); + } + + public static void main(final String[] args) throws IOException { + File file = new File(args[0]); + + BufferedImage image = ImageIO.read(file); + + showIt(image, file.getName()); + } +} diff --git a/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/HDRImageReaderSpi.java b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/HDRImageReaderSpi.java new file mode 100644 index 00000000..0624e2f6 --- /dev/null +++ b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/HDRImageReaderSpi.java @@ -0,0 +1,56 @@ +package com.twelvemonkeys.imageio.plugins.hdr; + +import com.twelvemonkeys.imageio.spi.ImageReaderSpiBase; + +import javax.imageio.ImageReader; +import javax.imageio.stream.ImageInputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.Locale; + +/** + * HDRImageReaderSpi. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: HDRImageReaderSpi.java,v 1.0 27/07/15 harald.kuhr Exp$ + */ +public final class HDRImageReaderSpi extends ImageReaderSpiBase { + public HDRImageReaderSpi() { + super(new HDRProviderInfo()); + } + + @Override + public boolean canDecodeInput(final Object source) throws IOException { + if (!(source instanceof ImageInputStream)) { + return false; + } + + ImageInputStream stream = (ImageInputStream) source; + + stream.mark(); + + try { + // NOTE: All images I have found starts with #?RADIANCE (or has no #? line at all), + // although some sources claim that #?RGBE is also used. + byte[] magic = new byte[HDR.RADIANCE_MAGIC.length]; + stream.readFully(magic); + + return Arrays.equals(HDR.RADIANCE_MAGIC, magic) + || Arrays.equals(HDR.RGBE_MAGIC, Arrays.copyOf(magic, 6)); + } + finally { + stream.reset(); + } + } + + @Override + public ImageReader createReaderInstance(Object extension) throws IOException { + return new HDRImageReader(this); + } + + @Override + public String getDescription(final Locale locale) { + return "Radiance RGBE High Dynaimc Range (HDR) image reader"; + } +} diff --git a/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/HDRMetadata.java b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/HDRMetadata.java new file mode 100755 index 00000000..52cabaeb --- /dev/null +++ b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/HDRMetadata.java @@ -0,0 +1,127 @@ +/* + * 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. + */ + +package com.twelvemonkeys.imageio.plugins.hdr; + +import com.twelvemonkeys.imageio.AbstractMetadata; + +import javax.imageio.metadata.IIOMetadataNode; + +final class HDRMetadata extends AbstractMetadata { + private final HDRHeader header; + + HDRMetadata(final HDRHeader header) { + this.header = header; + } + + @Override + protected IIOMetadataNode getStandardChromaNode() { + IIOMetadataNode chroma = new IIOMetadataNode("Chroma"); + + IIOMetadataNode csType = new IIOMetadataNode("ColorSpaceType"); + chroma.appendChild(csType); + csType.setAttribute("name", "RGB"); + // TODO: Support XYZ + + IIOMetadataNode numChannels = new IIOMetadataNode("NumChannels"); + numChannels.setAttribute("value", "3"); + chroma.appendChild(numChannels); + + IIOMetadataNode blackIsZero = new IIOMetadataNode("BlackIsZero"); + blackIsZero.setAttribute("value", "TRUE"); + chroma.appendChild(blackIsZero); + + return chroma; + } + + // No compression + + @Override + protected IIOMetadataNode getStandardCompressionNode() { + IIOMetadataNode node = new IIOMetadataNode("Compression"); + + IIOMetadataNode compressionTypeName = new IIOMetadataNode("CompressionTypeName"); + compressionTypeName.setAttribute("value", "RLE"); + node.appendChild(compressionTypeName); + + IIOMetadataNode lossless = new IIOMetadataNode("Lossless"); + lossless.setAttribute("value", "TRUE"); + node.appendChild(lossless); + + return node; + } + + @Override + protected IIOMetadataNode getStandardDataNode() { + IIOMetadataNode node = new IIOMetadataNode("Data"); + + IIOMetadataNode sampleFormat = new IIOMetadataNode("SampleFormat"); + sampleFormat.setAttribute("value", "UnsignedIntegral"); + node.appendChild(sampleFormat); + + IIOMetadataNode bitsPerSample = new IIOMetadataNode("BitsPerSample"); + bitsPerSample.setAttribute("value", "8 8 8 8"); + node.appendChild(bitsPerSample); + + return node; + } + + @Override + protected IIOMetadataNode getStandardDimensionNode() { + IIOMetadataNode dimension = new IIOMetadataNode("Dimension"); + + // TODO: Support other orientations + IIOMetadataNode imageOrientation = new IIOMetadataNode("ImageOrientation"); + imageOrientation.setAttribute("value", "Normal"); + dimension.appendChild(imageOrientation); + + return dimension; + } + + // No document node + + @Override + protected IIOMetadataNode getStandardTextNode() { + if (header.getSoftware() != null) { + IIOMetadataNode text = new IIOMetadataNode("Text"); + + IIOMetadataNode textEntry = new IIOMetadataNode("TextEntry"); + textEntry.setAttribute("keyword", "Software"); + textEntry.setAttribute("value", header.getSoftware()); + text.appendChild(textEntry); + + return text; + } + + return null; + } + + // No tiling + + // No transparency +} diff --git a/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/HDRProviderInfo.java b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/HDRProviderInfo.java new file mode 100644 index 00000000..ab1b2b1f --- /dev/null +++ b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/HDRProviderInfo.java @@ -0,0 +1,27 @@ +package com.twelvemonkeys.imageio.plugins.hdr; + +import com.twelvemonkeys.imageio.spi.ReaderWriterProviderInfo; + +/** + * HDRProviderInfo. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: HDRProviderInfo.java,v 1.0 27/07/15 harald.kuhr Exp$ + */ +final class HDRProviderInfo extends ReaderWriterProviderInfo { + protected HDRProviderInfo() { + super( + HDRProviderInfo.class, + new String[] {"HDR", "hdr", "RGBE", "rgbe"}, + new String[] {"hdr", "rgbe", "xyze", "pic"}, + new String[] {"image/vnd.radiance"}, + "com.twelvemonkeys.imageio.plugins.hdr.HDRImageReader", + new String[]{"com.twelvemonkeys.imageio.plugins.hdr.HDRImageReaderSpi"}, + null, + null, + false, null, null, null, null, + true, null, null, null, null + ); + } +} diff --git a/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/RGBE.java b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/RGBE.java new file mode 100644 index 00000000..b64607e1 --- /dev/null +++ b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/RGBE.java @@ -0,0 +1,494 @@ +package com.twelvemonkeys.imageio.plugins.hdr; + +import java.io.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * This file contains code to read and write four byte rgbe file format + * developed by Greg Ward. It handles the conversions between rgbe and + * pixels consisting of floats. The data is assumed to be an array of floats. + * By default there are three floats per pixel in the order red, green, blue. + * (RGBE_DATA_??? values control this.) Only the mimimal header reading and + * writing is implemented. Each routine does error checking and will return + * a status value as defined below. This code is intended as a skeleton so + * feel free to modify it to suit your needs.

+ *

+ * Ported to Java and restructured by Kenneth Russell.
+ * posted to http://www.graphics.cornell.edu/~bjw/
+ * written by Bruce Walter (bjw@graphics.cornell.edu) 5/26/95
+ * based on code written by Greg Ward
+ *

+ * Source: https://java.net/projects/jogl-demos/sources/svn/content/trunk/src/demos/hdr/RGBE.java + */ +final class RGBE { + // Flags indicating which fields in a Header are valid + private static final int VALID_PROGRAMTYPE = 0x01; + private static final int VALID_GAMMA = 0x02; + private static final int VALID_EXPOSURE = 0x04; + + private static final String gammaString = "GAMMA="; + private static final String exposureString = "EXPOSURE="; + + private static final Pattern widthHeightPattern = Pattern.compile("-Y (\\d+) \\+X (\\d+)"); + + public static class Header { + // Indicates which fields are valid + private int valid; + + // Listed at beginning of file to identify it after "#?". + // Defaults to "RGBE" + private String programType; + + // Image has already been gamma corrected with given gamma. + // Defaults to 1.0 (no correction) + private float gamma; + + // A value of 1.0 in an image corresponds to + // watts/steradian/m^2. Defaults to 1.0. + private float exposure; + + // Width and height of image + private int width; + private int height; + + private Header(int valid, + String programType, + float gamma, + float exposure, + int width, + int height) { + this.valid = valid; + this.programType = programType; + this.gamma = gamma; + this.exposure = exposure; + this.width = width; + this.height = height; + } + + public boolean isProgramTypeValid() { + return ((valid & VALID_PROGRAMTYPE) != 0); + } + + public boolean isGammaValid() { + return ((valid & VALID_GAMMA) != 0); + } + + public boolean isExposureValid() { + return ((valid & VALID_EXPOSURE) != 0); + } + + public String getProgramType() { + return programType; + } + + public float getGamma() { + return gamma; + } + + public float getExposure() { + return exposure; + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + public String toString() { + StringBuffer buf = new StringBuffer(); + if (isProgramTypeValid()) { + buf.append(" Program type: "); + buf.append(getProgramType()); + } + buf.append(" Gamma"); + if (isGammaValid()) { + buf.append(" [valid]"); + } + buf.append(": "); + buf.append(getGamma()); + buf.append(" Exposure"); + if (isExposureValid()) { + buf.append(" [valid]"); + } + buf.append(": "); + buf.append(getExposure()); + buf.append(" Width: "); + buf.append(getWidth()); + buf.append(" Height: "); + buf.append(getHeight()); + return buf.toString(); + } + } + + public static Header readHeader(final DataInput in) throws IOException { + int valid = 0; + String programType = null; + float gamma = 1.0f; + float exposure = 1.0f; + int width = 0; + int height = 0; + + String buf = in.readLine(); + if (buf == null) { + throw new IOException("Unexpected EOF reading magic token"); + } + if (buf.charAt(0) == '#' && buf.charAt(1) == '?') { + valid |= VALID_PROGRAMTYPE; + programType = buf.substring(2); + buf = in.readLine(); + if (buf == null) { + throw new IOException("Unexpected EOF reading line after magic token"); + } + } + + boolean foundFormat = false; + boolean done = false; + while (!done) { + if (buf.equals("FORMAT=32-bit_rle_rgbe")) { + foundFormat = true; + } + else if (buf.startsWith(gammaString)) { + valid |= VALID_GAMMA; + gamma = Float.parseFloat(buf.substring(gammaString.length())); + } + else if (buf.startsWith(exposureString)) { + valid |= VALID_EXPOSURE; + exposure = Float.parseFloat(buf.substring(exposureString.length())); + } + else { + Matcher m = widthHeightPattern.matcher(buf); + if (m.matches()) { + width = Integer.parseInt(m.group(2)); + height = Integer.parseInt(m.group(1)); + done = true; + } + } + + if (!done) { + buf = in.readLine(); + if (buf == null) { + throw new IOException("Unexpected EOF reading header"); + } + } + } + + if (!foundFormat) { + throw new IOException("No FORMAT specifier found"); + } + + return new Header(valid, programType, gamma, exposure, width, height); + } + + /** + * Simple read routine. Will not correctly handle run length encoding. + */ + public static void readPixels(DataInput in, float[] data, int numpixels) throws IOException { + byte[] rgbe = new byte[4]; + float[] rgb = new float[3]; + int offset = 0; + + while (numpixels-- > 0) { + in.readFully(rgbe); + + rgbe2float(rgb, rgbe, 0); + + data[offset++] = rgb[0]; + data[offset++] = rgb[1]; + data[offset++] = rgb[2]; + } + } + + public static void readPixelsRaw(DataInput in, byte[] data, int offset, int numpixels) throws IOException { + int numExpected = 4 * numpixels; + in.readFully(data, offset, numExpected); + } + + public static void readPixelsRawRLE(DataInput in, byte[] data, int offset, + int scanline_width, int num_scanlines) throws IOException { + byte[] rgbe = new byte[4]; + byte[] scanline_buffer = null; + int ptr, ptr_end; + int count; + byte[] buf = new byte[2]; + + if ((scanline_width < 8) || (scanline_width > 0x7fff)) { + // run length encoding is not allowed so read flat + readPixelsRaw(in, data, offset, scanline_width * num_scanlines); + } + + // read in each successive scanline + while (num_scanlines > 0) { + in.readFully(rgbe); + + if ((rgbe[0] != 2) || (rgbe[1] != 2) || ((rgbe[2] & 0x80) != 0)) { + // this file is not run length encoded + data[offset++] = rgbe[0]; + data[offset++] = rgbe[1]; + data[offset++] = rgbe[2]; + data[offset++] = rgbe[3]; + readPixelsRaw(in, data, offset, scanline_width * num_scanlines - 1); + } + + if ((((rgbe[2] & 0xFF) << 8) | (rgbe[3] & 0xFF)) != scanline_width) { + throw new IOException("Wrong scanline width " + + (((rgbe[2] & 0xFF) << 8) | (rgbe[3] & 0xFF)) + + ", expected " + scanline_width); + } + + if (scanline_buffer == null) { + scanline_buffer = new byte[4 * scanline_width]; + } + + ptr = 0; + // read each of the four channels for the scanline into the buffer + for (int i = 0; i < 4; i++) { + ptr_end = (i + 1) * scanline_width; + while (ptr < ptr_end) { + in.readFully(buf); + + if ((buf[0] & 0xFF) > 128) { + // a run of the same value + count = (buf[0] & 0xFF) - 128; + if ((count == 0) || (count > ptr_end - ptr)) { + throw new IOException("Bad scanline data"); + } + while (count-- > 0) { + scanline_buffer[ptr++] = buf[1]; + } + } + else { + // a non-run + count = buf[0] & 0xFF; + if ((count == 0) || (count > ptr_end - ptr)) { + throw new IOException("Bad scanline data"); + } + scanline_buffer[ptr++] = buf[1]; + if (--count > 0) { + in.readFully(scanline_buffer, ptr, count); + ptr += count; + } + } + } + } + // copy byte data to output + for (int i = 0; i < scanline_width; i++) { + data[offset++] = scanline_buffer[i]; + data[offset++] = scanline_buffer[i + scanline_width]; + data[offset++] = scanline_buffer[i + 2 * scanline_width]; + data[offset++] = scanline_buffer[i + 3 * scanline_width]; + } + num_scanlines--; + } + } + + /** + * Standard conversion from float pixels to rgbe pixels. + */ + public static void float2rgbe(byte[] rgbe, float red, float green, float blue) { + float v; + int e; + + v = red; + if (green > v) { + v = green; + } + if (blue > v) { + v = blue; + } + if (v < 1e-32f) { + rgbe[0] = rgbe[1] = rgbe[2] = rgbe[3] = 0; + } + else { + FracExp fe = frexp(v); + v = (float) (fe.getFraction() * 256.0 / v); + rgbe[0] = (byte) (red * v); + rgbe[1] = (byte) (green * v); + rgbe[2] = (byte) (blue * v); + rgbe[3] = (byte) (fe.getExponent() + 128); + } + } + + /** + * Standard conversion from rgbe to float pixels. Note: Ward uses + * ldexp(col+0.5,exp-(128+8)). However we wanted pixels in the + * range [0,1] to map back into the range [0,1]. + */ + public static void rgbe2float(float[] rgb, byte[] rgbe, int startRGBEOffset) { + float f; + + if (rgbe[startRGBEOffset + 3] != 0) { // nonzero pixel + f = (float) ldexp(1.0, (rgbe[startRGBEOffset + 3] & 0xFF) - (128 + 8)); + rgb[0] = (rgbe[startRGBEOffset + 0] & 0xFF) * f; + rgb[1] = (rgbe[startRGBEOffset + 1] & 0xFF) * f; + rgb[2] = (rgbe[startRGBEOffset + 2] & 0xFF) * f; + } + else { + rgb[0] = 0; + rgb[1] = 0; + rgb[2] = 0; + } + } + + public static double ldexp(double value, int exp) { + if (!finite(value) || value == 0.0) { + return value; + } + value = scalbn(value, exp); + // No good way to indicate errno (want to avoid throwing + // exceptions because don't know about stability of calculations) + // if(!finite(value)||value==0.0) errno = ERANGE; + return value; + } + + //---------------------------------------------------------------------- + // Internals only below this point + // + + //---------------------------------------------------------------------- + // Math routines, some fdlibm-derived + // + + static class FracExp { + private double fraction; + private int exponent; + + public FracExp(double fraction, int exponent) { + this.fraction = fraction; + this.exponent = exponent; + } + + public double getFraction() { + return fraction; + } + + public int getExponent() { + return exponent; + } + } + + private static final double two54 = 1.80143985094819840000e+16; // 43500000 00000000 + private static final double twom54 = 5.55111512312578270212e-17; // 0x3C900000 0x00000000 + private static final double huge = 1.0e+300; + private static final double tiny = 1.0e-300; + + private static int hi(double x) { + long bits = Double.doubleToRawLongBits(x); + return (int) (bits >>> 32); + } + + private static int lo(double x) { + long bits = Double.doubleToRawLongBits(x); + return (int) bits; + } + + private static double fromhilo(int hi, int lo) { + return Double.longBitsToDouble((((long) hi) << 32) | + (((long) lo) & 0xFFFFFFFFL)); + } + + private static FracExp frexp(double x) { + int hx = hi(x); + int ix = 0x7fffffff & hx; + int lx = lo(x); + int e = 0; + if (ix >= 0x7ff00000 || ((ix | lx) == 0)) { + return new FracExp(x, e); // 0,inf,nan + } + if (ix < 0x00100000) { // subnormal + x *= two54; + hx = hi(x); + ix = hx & 0x7fffffff; + e = -54; + } + e += (ix >> 20) - 1022; + hx = (hx & 0x800fffff) | 0x3fe00000; + lx = lo(x); + return new FracExp(fromhilo(hx, lx), e); + } + + private static boolean finite(double x) { + int hx; + hx = hi(x); + return (((hx & 0x7fffffff) - 0x7ff00000) >> 31) != 0; + } + + /** + * copysign(double x, double y)
+ * copysign(x,y) returns a value with the magnitude of x and + * with the sign bit of y. + */ + private static double copysign(double x, double y) { + return fromhilo((hi(x) & 0x7fffffff) | (hi(y) & 0x80000000), lo(x)); + } + + /** + * scalbn (double x, int n)
+ * scalbn(x,n) returns x* 2**n computed by exponent + * manipulation rather than by actually performing an + * exponentiation or a multiplication. + */ + private static double scalbn(double x, int n) { + int hx = hi(x); + int lx = lo(x); + int k = (hx & 0x7ff00000) >> 20; // extract exponent + if (k == 0) { // 0 or subnormal x + if ((lx | (hx & 0x7fffffff)) == 0) { + return x; // +-0 + } + x *= two54; + hx = hi(x); + k = ((hx & 0x7ff00000) >> 20) - 54; + if (n < -50000) { + return tiny * x; // underflow + } + } + if (k == 0x7ff) { + return x + x; // NaN or Inf + } + k = k + n; + if (k > 0x7fe) { + return huge * copysign(huge, x); // overflow + } + if (k > 0) { + // normal result + return fromhilo((hx & 0x800fffff) | (k << 20), lo(x)); + } + if (k <= -54) { + if (n > 50000) { + // in case integer overflow in n+k + return huge * copysign(huge, x); // overflow + } + else { + return tiny * copysign(tiny, x); // underflow + } + } + k += 54; // subnormal result + x = fromhilo((hx & 0x800fffff) | (k << 20), lo(x)); + return x * twom54; + } + + //---------------------------------------------------------------------- + // Test harness + // + + public static void main(String[] args) { + for (int i = 0; i < args.length; i++) { + try { + DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream(args[i]))); + Header header = RGBE.readHeader(in); + System.err.println("Header for file \"" + args[i] + "\":"); + System.err.println(" " + header); + byte[] data = new byte[header.getWidth() * header.getHeight() * 4]; + readPixelsRawRLE(in, data, 0, header.getWidth(), header.getHeight()); + in.close(); + } + catch (IOException e) { + e.printStackTrace(); + } + } + } +} diff --git a/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/tonemap/DefaultToneMapper.java b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/tonemap/DefaultToneMapper.java new file mode 100644 index 00000000..740c0913 --- /dev/null +++ b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/tonemap/DefaultToneMapper.java @@ -0,0 +1,35 @@ +package com.twelvemonkeys.imageio.plugins.hdr.tonemap; + +/** + * DefaultToneMapper. + *

+ * Normalizes values to range [0...1] using: + * + *

Vout = Vin / (Vin + C)

+ * + * Where C is constant. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: DefaultToneMapper.java,v 1.0 28/07/15 harald.kuhr Exp$ + */ +public final class DefaultToneMapper implements ToneMapper { + + private final float constant; + + public DefaultToneMapper() { + this(1); + } + + public DefaultToneMapper(final float constant) { + this.constant = constant; + } + + @Override + public void map(final float[] rgb) { + // Default Vo = Vi / (Vi + 1) + for (int i = 0; i < rgb.length; i++) { + rgb[i] = rgb[i] / (rgb[i] + constant); + } + } +} diff --git a/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/tonemap/GammaToneMapper.java b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/tonemap/GammaToneMapper.java new file mode 100644 index 00000000..7a8ba6fb --- /dev/null +++ b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/tonemap/GammaToneMapper.java @@ -0,0 +1,38 @@ +package com.twelvemonkeys.imageio.plugins.hdr.tonemap; + +/** + * GammaToneMapper. + *

+ * Normalizes values to range [0...1] using: + * + *

Vout = A Vin\u03b3

+ * + * Where A is constant and \u03b3 is the gamma. + * Values > 1 are clamped. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: GammaToneMapper.java,v 1.0 28/07/15 harald.kuhr Exp$ + */ +public final class GammaToneMapper implements ToneMapper { + + private final float constant; + private final float gamma; + + public GammaToneMapper() { + this(0.5f, .25f); + } + + public GammaToneMapper(final float constant, final float gamma) { + this.constant = constant; + this.gamma = gamma; + } + + @Override + public void map(final float[] rgb) { + // Gamma Vo = A * Vi^y + for (int i = 0; i < rgb.length; i++) { + rgb[i] = Math.min(1f, (float) (constant * Math.pow(rgb[i], gamma))); + } + } +} diff --git a/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/tonemap/NullToneMapper.java b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/tonemap/NullToneMapper.java new file mode 100644 index 00000000..ea6ad349 --- /dev/null +++ b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/tonemap/NullToneMapper.java @@ -0,0 +1,18 @@ +package com.twelvemonkeys.imageio.plugins.hdr.tonemap; + +/** + * NullToneMapper. + *

+ * This {@code ToneMapper} does *not* normalize or clamp values + * to range [0...1], but leaves the values as-is. + * Useful for applications that implements custom tone mapping. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: NullToneMapper.java,v 1.0 28/07/15 harald.kuhr Exp$ + */ +public final class NullToneMapper implements ToneMapper { + @Override + public void map(float[] rgb) { + } +} diff --git a/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/tonemap/ToneMapper.java b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/tonemap/ToneMapper.java new file mode 100644 index 00000000..2f06374b --- /dev/null +++ b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/tonemap/ToneMapper.java @@ -0,0 +1,12 @@ +package com.twelvemonkeys.imageio.plugins.hdr.tonemap; + +/** + * ToneMapper. + * + * @author Harald Kuhr + * @author last modified by $Author: harald.kuhr$ + * @version $Id: ToneMapper.java,v 1.0 28/07/15 harald.kuhr Exp$ + */ +public interface ToneMapper { + void map(float[] rgb); +} diff --git a/imageio/imageio-hdr/src/main/resources/META-INF/services/javax.imageio.spi.ImageReaderSpi b/imageio/imageio-hdr/src/main/resources/META-INF/services/javax.imageio.spi.ImageReaderSpi new file mode 100755 index 00000000..7af7febe --- /dev/null +++ b/imageio/imageio-hdr/src/main/resources/META-INF/services/javax.imageio.spi.ImageReaderSpi @@ -0,0 +1 @@ +com.twelvemonkeys.imageio.plugins.hdr.HDRImageReaderSpi diff --git a/imageio/imageio-hdr/src/test/java/com/twelvemonkeys/imageio/plugins/hdr/HDRImageReaderTest.java b/imageio/imageio-hdr/src/test/java/com/twelvemonkeys/imageio/plugins/hdr/HDRImageReaderTest.java new file mode 100755 index 00000000..2a57572f --- /dev/null +++ b/imageio/imageio-hdr/src/test/java/com/twelvemonkeys/imageio/plugins/hdr/HDRImageReaderTest.java @@ -0,0 +1,85 @@ +/* + * 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. + */ + +package com.twelvemonkeys.imageio.plugins.hdr; + +import com.twelvemonkeys.imageio.util.ImageReaderAbstractTest; + +import javax.imageio.spi.ImageReaderSpi; +import java.awt.*; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * TGAImageReaderTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: TGAImageReaderTest.java,v 1.0 03.07.14 22:28 haraldk Exp$ + */ +public class HDRImageReaderTest extends ImageReaderAbstractTest { + @Override + protected List getTestData() { + return Arrays.asList( + new TestData(getClassLoaderResource("/hdr/memorial_o876.hdr"), new Dimension(512, 768)) + ); + } + + @Override + protected ImageReaderSpi createProvider() { + return new HDRImageReaderSpi(); + } + + @Override + protected Class getReaderClass() { + return HDRImageReader.class; + } + + @Override + protected HDRImageReader createReader() { + return new HDRImageReader(createProvider()); + } + + @Override + protected List getFormatNames() { + return Arrays.asList("HDR", "hdr", "RGBE", "rgbe"); + } + + @Override + protected List getSuffixes() { + return Arrays.asList("hdr", "rgbe", "xyze"); + } + + @Override + protected List getMIMETypes() { + return Collections.singletonList( + "image/vnd.radiance" + ); + } +} diff --git a/imageio/imageio-hdr/src/test/resources/hdr/memorial_o876.hdr b/imageio/imageio-hdr/src/test/resources/hdr/memorial_o876.hdr new file mode 100644 index 00000000..c135ae99 Binary files /dev/null and b/imageio/imageio-hdr/src/test/resources/hdr/memorial_o876.hdr differ diff --git a/imageio/pom.xml b/imageio/pom.xml index 019c00ac..1e58b424 100644 --- a/imageio/pom.xml +++ b/imageio/pom.xml @@ -30,6 +30,7 @@ imageio-bmp + imageio-hdr imageio-icns imageio-iff imageio-jpeg