1/*
2 * Copyright (c) 2009-2010 jMonkeyEngine
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are
7 * met:
8 *
9 * * Redistributions of source code must retain the above copyright
10 *   notice, this list of conditions and the following disclaimer.
11 *
12 * * Redistributions in binary form must reproduce the above copyright
13 *   notice, this list of conditions and the following disclaimer in the
14 *   documentation and/or other materials provided with the distribution.
15 *
16 * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
17 *   may be used to endorse or promote products derived from this software
18 *   without specific prior written permission.
19 *
20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
22 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
23 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
24 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
25 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
26 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
27 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
28 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
29 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 */
32
33package com.jme3.texture.plugins;
34
35import com.jme3.asset.AssetInfo;
36import com.jme3.asset.AssetLoader;
37import com.jme3.asset.TextureKey;
38import com.jme3.math.FastMath;
39import com.jme3.texture.Image;
40import com.jme3.texture.Image.Format;
41import com.jme3.util.BufferUtils;
42import java.io.IOException;
43import java.io.InputStream;
44import java.nio.ByteBuffer;
45import java.util.logging.Level;
46import java.util.logging.Logger;
47
48public class HDRLoader implements AssetLoader {
49
50    private static final Logger logger = Logger.getLogger(HDRLoader.class.getName());
51
52    private boolean writeRGBE = false;
53    private ByteBuffer rleTempBuffer;
54    private ByteBuffer dataStore;
55    private final float[] tempF = new float[3];
56
57    public HDRLoader(boolean writeRGBE){
58        this.writeRGBE = writeRGBE;
59    }
60
61    public HDRLoader(){
62    }
63
64    public static void convertFloatToRGBE(byte[] rgbe, float red, float green, float blue){
65        double max = red;
66        if (green > max) max = green;
67        if (blue > max) max = blue;
68        if (max < 1.0e-32){
69            rgbe[0] = rgbe[1] = rgbe[2] = rgbe[3] = 0;
70        }else{
71            double exp = Math.ceil( Math.log10(max) / Math.log10(2) );
72            double divider = Math.pow(2.0, exp);
73            rgbe[0] = (byte) ((red   / divider) * 255.0);
74            rgbe[1] = (byte) ((green / divider) * 255.0);
75            rgbe[2] = (byte) ((blue  / divider) * 255.0);
76            rgbe[3] = (byte) (exp + 128.0);
77      }
78    }
79
80    public static void convertRGBEtoFloat(byte[] rgbe, float[] rgbf){
81        int R = rgbe[0] & 0xFF,
82            G = rgbe[1] & 0xFF,
83            B = rgbe[2] & 0xFF,
84            E = rgbe[3] & 0xFF;
85
86        float e = (float) Math.pow(2f, E - (128 + 8) );
87        rgbf[0] = R * e;
88        rgbf[1] = G * e;
89        rgbf[2] = B * e;
90    }
91
92    public static void convertRGBEtoFloat2(byte[] rgbe, float[] rgbf){
93        int R = rgbe[0] & 0xFF,
94            G = rgbe[1] & 0xFF,
95            B = rgbe[2] & 0xFF,
96            E = rgbe[3] & 0xFF;
97
98        float e = (float) Math.pow(2f, E - 128);
99        rgbf[0] = (R / 256.0f) * e;
100        rgbf[1] = (G / 256.0f) * e;
101        rgbf[2] = (B / 256.0f) * e;
102    }
103
104    public static void convertRGBEtoFloat3(byte[] rgbe, float[] rgbf){
105        int R = rgbe[0] & 0xFF,
106            G = rgbe[1] & 0xFF,
107            B = rgbe[2] & 0xFF,
108            E = rgbe[3] & 0xFF;
109
110        float e = (float) Math.pow(2f, E - (128 + 8) );
111        rgbf[0] = R * e;
112        rgbf[1] = G * e;
113        rgbf[2] = B * e;
114    }
115
116    private short flip(int in){
117        return (short) ((in << 8 & 0xFF00) | (in >> 8));
118    }
119
120    private void writeRGBE(byte[] rgbe){
121        if (writeRGBE){
122            dataStore.put(rgbe);
123        }else{
124            convertRGBEtoFloat(rgbe, tempF);
125            dataStore.putShort(FastMath.convertFloatToHalf(tempF[0]))
126                     .putShort(FastMath.convertFloatToHalf(tempF[1])).
127                      putShort(FastMath.convertFloatToHalf(tempF[2]));
128        }
129    }
130
131    private String readString(InputStream is) throws IOException{
132        StringBuilder sb = new StringBuilder();
133        while (true){
134            int i = is.read();
135            if (i == 0x0a || i == -1) // new line or EOF
136                return sb.toString();
137
138            sb.append((char)i);
139        }
140    }
141
142    private boolean decodeScanlineRLE(InputStream in, int width) throws IOException{
143        // must deocde RLE data into temp buffer before converting to float
144        if (rleTempBuffer == null){
145            rleTempBuffer = BufferUtils.createByteBuffer(width * 4);
146        }else{
147            rleTempBuffer.clear();
148            if (rleTempBuffer.remaining() < width * 4)
149                rleTempBuffer = BufferUtils.createByteBuffer(width * 4);
150        }
151
152	// read each component seperately
153        for (int i = 0; i < 4; i++) {
154            // read WIDTH bytes for the channel
155            for (int j = 0; j < width;) {
156                int code = in.read();
157                if (code > 128) { // run
158                    code -= 128;
159                    int val = in.read();
160                    while ((code--) != 0) {
161                        rleTempBuffer.put( (j++) * 4 + i , (byte)val);
162                        //scanline[j++][i] = val;
163                    }
164                } else {	// non-run
165                    while ((code--) != 0) {
166                        int val = in.read();
167                        rleTempBuffer.put( (j++) * 4 + i, (byte)val);
168                        //scanline[j++][i] = in.read();
169                    }
170                }
171            }
172        }
173
174        rleTempBuffer.rewind();
175        byte[] rgbe = new byte[4];
176//        float[] temp = new float[3];
177
178        // decode temp buffer into float data
179        for (int i = 0; i < width; i++){
180            rleTempBuffer.get(rgbe);
181            writeRGBE(rgbe);
182        }
183
184        return true;
185    }
186
187    private boolean decodeScanlineUncompressed(InputStream in, int width) throws IOException{
188        byte[] rgbe = new byte[4];
189
190        for (int i = 0; i < width; i+=3){
191            if (in.read(rgbe) < 1)
192                return false;
193
194            writeRGBE(rgbe);
195        }
196        return true;
197    }
198
199    private void decodeScanline(InputStream in, int width) throws IOException{
200        if (width < 8 || width > 0x7fff){
201            // too short/long for RLE compression
202            decodeScanlineUncompressed(in, width);
203        }
204
205        // check format
206        byte[] data = new byte[4];
207        in.read(data);
208        if (data[0] != 0x02 || data[1] != 0x02 || (data[2] & 0x80) != 0){
209            // not RLE data
210            decodeScanlineUncompressed(in, width-1);
211        }else{
212            // check scanline width
213            int readWidth = (data[2] & 0xFF) << 8 | (data[3] & 0xFF);
214            if (readWidth != width)
215                throw new IOException("Illegal scanline width in HDR file: "+width+" != "+readWidth);
216
217            // RLE data
218            decodeScanlineRLE(in, width);
219        }
220    }
221
222    public Image load(InputStream in, boolean flipY) throws IOException{
223        float gamma = -1f;
224        float exposure = -1f;
225        float[] colorcorr = new float[]{ -1f, -1f, -1f };
226
227        int width = -1, height = -1;
228        boolean verifiedFormat = false;
229
230        while (true){
231            String ln = readString(in);
232            ln = ln.trim();
233            if (ln.startsWith("#") || ln.equals("")){
234                if (ln.equals("#?RADIANCE") || ln.equals("#?RGBE"))
235                    verifiedFormat = true;
236
237                continue; // comment or empty statement
238            } else if (ln.startsWith("+") || ln.startsWith("-")){
239                // + or - mark image resolution and start of data
240                String[] resData = ln.split("\\s");
241                if (resData.length != 4){
242                    throw new IOException("Invalid resolution string in HDR file");
243                }
244
245                if (!resData[0].equals("-Y") || !resData[2].equals("+X")){
246                    logger.warning("Flipping/Rotating attributes ignored!");
247                }
248
249                //if (resData[0].endsWith("X")){
250                    // first width then height
251                //    width = Integer.parseInt(resData[1]);
252                //    height = Integer.parseInt(resData[3]);
253                //}else{
254                    width = Integer.parseInt(resData[3]);
255                    height = Integer.parseInt(resData[1]);
256                //}
257
258                break;
259            } else {
260                // regular command
261                int index = ln.indexOf("=");
262                if (index < 1){
263                    logger.log(Level.FINE, "Ignored string: {0}", ln);
264                    continue;
265                }
266
267                String var = ln.substring(0, index).trim().toLowerCase();
268                String value = ln.substring(index+1).trim().toLowerCase();
269                if (var.equals("format")){
270                    if (!value.equals("32-bit_rle_rgbe") && !value.equals("32-bit_rle_xyze")){
271                        throw new IOException("Unsupported format in HDR picture");
272                    }
273                }else if (var.equals("exposure")){
274                    exposure = Float.parseFloat(value);
275                }else if (var.equals("gamma")){
276                    gamma = Float.parseFloat(value);
277                }else{
278                    logger.log(Level.WARNING, "HDR Command ignored: {0}", ln);
279                }
280            }
281        }
282
283        assert width != -1 && height != -1;
284
285        if (!verifiedFormat)
286            logger.warning("Unsure if specified image is Radiance HDR");
287
288        // some HDR images can get pretty big
289        System.gc();
290
291        // each pixel times size of component times # of components
292        Format pixelFormat;
293        if (writeRGBE){
294            pixelFormat = Format.RGBA8;
295        }else{
296            pixelFormat = Format.RGB16F;
297        }
298
299        dataStore = BufferUtils.createByteBuffer(width * height * pixelFormat.getBitsPerPixel());
300
301        int bytesPerPixel = pixelFormat.getBitsPerPixel() / 8;
302        int scanLineBytes = bytesPerPixel * width;
303        for (int y = height - 1; y >= 0; y--) {
304            if (flipY)
305                dataStore.position(scanLineBytes * y);
306
307            decodeScanline(in, width);
308        }
309        in.close();
310
311        dataStore.rewind();
312        return new Image(pixelFormat, width, height, dataStore);
313    }
314
315    public Object load(AssetInfo info) throws IOException {
316        if (!(info.getKey() instanceof TextureKey))
317            throw new IllegalArgumentException("Texture assets must be loaded using a TextureKey");
318
319        boolean flip = ((TextureKey) info.getKey()).isFlipY();
320        InputStream in = null;
321        try {
322            in = info.openStream();
323            Image img = load(in, flip);
324            return img;
325        } finally {
326            if (in != null){
327                in.close();
328            }
329        }
330    }
331
332}
333