View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.imaging.formats.rgbe;
19  
20  import java.io.Closeable;
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.nio.ByteOrder;
24  import java.util.regex.Matcher;
25  import java.util.regex.Pattern;
26  
27  import org.apache.commons.imaging.ImagingException;
28  import org.apache.commons.imaging.bytesource.ByteSource;
29  import org.apache.commons.imaging.common.Allocator;
30  import org.apache.commons.imaging.common.BinaryFunctions;
31  import org.apache.commons.imaging.common.ByteConversions;
32  import org.apache.commons.imaging.common.GenericImageMetadata;
33  import org.apache.commons.imaging.common.ImageMetadata;
34  
35  final class RgbeInfo implements Closeable {
36  
37      // #?RADIANCE
38      private static final byte[] HEADER = { 0x23, 0x3F, 0x52, 0x41, 0x44, 0x49, 0x41, 0x4E, 0x43, 0x45 };
39      private static final Pattern RESOLUTION_STRING = Pattern.compile("-Y (\\d+) \\+X (\\d+)");
40      private static final byte[] TWO_TWO = { 0x2, 0x2 };
41  
42      private static void decompress(final InputStream in, final byte[] out) throws IOException, ImagingException {
43          int position = 0;
44          final int total = out.length;
45          while (position < total) {
46              final int n = in.read();
47              if (n < 0) {
48                  throw new ImagingException("Error decompressing RGBE file");
49              }
50              if (n > 128) {
51                  final int value = in.read();
52                  for (int i = 0; i < (n & 0x7f); i++) {
53                      out[position++] = (byte) value;
54                  }
55              } else {
56                  for (int i = 0; i < n; i++) {
57                      out[position++] = (byte) in.read();
58                  }
59              }
60          }
61      }
62  
63      private final InputStream in;
64      private GenericImageMetadata metadata;
65      private int width = -1;
66      private int height = -1;
67  
68      RgbeInfo(final ByteSource byteSource) throws IOException {
69          this.in = byteSource.getInputStream();
70      }
71  
72      @Override
73      public void close() throws IOException {
74          in.close();
75      }
76  
77      int getHeight() throws IOException, ImagingException {
78          if (-1 == height) {
79              readDimensions();
80          }
81          return height;
82      }
83  
84      ImageMetadata getMetadata() throws IOException, ImagingException {
85          if (null == metadata) {
86              readMetadata();
87          }
88          return metadata;
89      }
90  
91      float[][] getPixelData() throws IOException, ImagingException {
92          // Read into local variables to ensure that we have seeked into the file
93          // far enough
94          final int ht = getHeight();
95          final int wd = getWidth();
96          if (wd >= 32768) {
97              throw new ImagingException("Scan lines must be less than 32768 bytes long");
98          }
99          final byte[] scanLineBytes = ByteConversions.toBytes((short) wd, ByteOrder.BIG_ENDIAN);
100         final byte[] rgbe = Allocator.byteArray(wd * 4);
101         final float[][] out = new float[3][Allocator.check(wd * ht)];
102         for (int i = 0; i < ht; i++) {
103             BinaryFunctions.readAndVerifyBytes(in, TWO_TWO, "Scan line " + i + " expected to start with 0x2 0x2");
104             BinaryFunctions.readAndVerifyBytes(in, scanLineBytes, "Scan line " + i + " length expected");
105             decompress(in, rgbe);
106             for (int channel = 0; channel < 3; channel++) {
107                 final int channelOffset = channel * wd;
108                 final int eOffset = 3 * wd;
109                 for (int p = 0; p < wd; p++) {
110                     final int mantissa = rgbe[p + eOffset] & 0xff;
111                     final int pos = p + i * wd;
112                     if (0 == mantissa) {
113                         out[channel][pos] = 0;
114                     } else {
115                         final float mult = (float) Math.pow(2, mantissa - (128 + 8));
116                         out[channel][pos] = ((rgbe[p + channelOffset] & 0xff) + 0.5f) * mult;
117                     }
118                 }
119             }
120         }
121         return out;
122     }
123 
124     int getWidth() throws IOException, ImagingException {
125         if (-1 == width) {
126             readDimensions();
127         }
128         return width;
129     }
130 
131     private void readDimensions() throws IOException, ImagingException {
132         getMetadata(); // Ensure we've read past this
133         final InfoHeader header = new InfoHeader(in);
134         final String resolution = header.line();
135         final Matcher matcher = RESOLUTION_STRING.matcher(resolution);
136         if (!matcher.matches()) {
137             throw new ImagingException("Invalid HDR resolution string. Only \"-Y N +X M\" is supported. Found \"" + resolution + "\"");
138         }
139         height = Integer.parseInt(matcher.group(1));
140         width = Integer.parseInt(matcher.group(2));
141     }
142 
143     private void readMetadata() throws IOException, ImagingException {
144         BinaryFunctions.readAndVerifyBytes(in, HEADER, "Not a valid HDR: Incorrect Header");
145         final InfoHeader reader = new InfoHeader(in);
146         if (!reader.line().isEmpty()) {
147             throw new ImagingException("Not a valid HDR: Incorrect Header");
148         }
149         metadata = new GenericImageMetadata();
150         String info = reader.line();
151         while (!info.isEmpty()) {
152             final int equals = info.indexOf('=');
153             if (equals > 0) {
154                 final String variable = info.substring(0, equals);
155                 final String value = info.substring(equals + 1);
156                 if ("FORMAT".equals(value) && !"32-bit_rle_rgbe".equals(value)) {
157                     throw new ImagingException("Only 32-bit_rle_rgbe images are supported, trying to read " + value);
158                 }
159                 metadata.add(variable, value);
160             } else {
161                 metadata.add("<command>", info);
162             }
163             info = reader.line();
164         }
165     }
166 }