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  package org.apache.commons.imaging.formats.rgbe;
18  
19  import java.io.Closeable;
20  import java.io.IOException;
21  import java.io.InputStream;
22  import java.nio.ByteOrder;
23  import java.util.regex.Matcher;
24  import java.util.regex.Pattern;
25  
26  import org.apache.commons.imaging.ImagingException;
27  import org.apache.commons.imaging.bytesource.ByteSource;
28  import org.apache.commons.imaging.common.Allocator;
29  import org.apache.commons.imaging.common.BinaryFunctions;
30  import org.apache.commons.imaging.common.ByteConversions;
31  import org.apache.commons.imaging.common.GenericImageMetadata;
32  import org.apache.commons.imaging.common.ImageMetadata;
33  
34  final class RgbeInfo implements Closeable {
35      // #?RADIANCE
36      private static final byte[] HEADER = { 0x23, 0x3F, 0x52, 0x41, 0x44, 0x49, 0x41, 0x4E, 0x43, 0x45 };
37      private static final Pattern RESOLUTION_STRING = Pattern.compile("-Y (\\d+) \\+X (\\d+)");
38  
39      private static final byte[] TWO_TWO = { 0x2, 0x2 };
40  
41      private static void decompress(final InputStream in, final byte[] out) throws IOException, ImagingException {
42          int position = 0;
43          final int total = out.length;
44  
45          while (position < total) {
46              final int n = in.read();
47  
48              if (n < 0) {
49                  throw new ImagingException("Error decompressing RGBE file");
50              }
51  
52              if (n > 128) {
53                  final int value = in.read();
54  
55                  for (int i = 0; i < (n & 0x7f); i++) {
56                      out[position++] = (byte) value;
57                  }
58              } else {
59                  for (int i = 0; i < n; i++) {
60                      out[position++] = (byte) in.read();
61                  }
62              }
63          }
64      }
65  
66      private final InputStream in;
67      private GenericImageMetadata metadata;
68      private int width = -1;
69  
70      private int height = -1;
71  
72      RgbeInfo(final ByteSource byteSource) throws IOException {
73          this.in = byteSource.getInputStream();
74      }
75  
76      @Override
77      public void close() throws IOException {
78          in.close();
79      }
80  
81      int getHeight() throws IOException, ImagingException {
82          if (-1 == height) {
83              readDimensions();
84          }
85  
86          return height;
87      }
88  
89      ImageMetadata getMetadata() throws IOException, ImagingException {
90          if (null == metadata) {
91              readMetadata();
92          }
93  
94          return metadata;
95      }
96  
97      public float[][] getPixelData() throws IOException, ImagingException {
98          // Read into local variables to ensure that we have seeked into the file
99          // far enough
100         final int ht = getHeight();
101         final int wd = getWidth();
102 
103         if (wd >= 32768) {
104             throw new ImagingException("Scan lines must be less than 32768 bytes long");
105         }
106 
107         final byte[] scanLineBytes = ByteConversions.toBytes((short) wd, ByteOrder.BIG_ENDIAN);
108         final byte[] rgbe = Allocator.byteArray(wd * 4);
109         final float[][] out = new float[3][Allocator.check(wd * ht)];
110 
111         for (int i = 0; i < ht; i++) {
112             BinaryFunctions.readAndVerifyBytes(in, TWO_TWO, "Scan line " + i + " expected to start with 0x2 0x2");
113             BinaryFunctions.readAndVerifyBytes(in, scanLineBytes, "Scan line " + i + " length expected");
114 
115             decompress(in, rgbe);
116 
117             for (int channel = 0; channel < 3; channel++) {
118                 final int channelOffset = channel * wd;
119                 final int eOffset = 3 * wd;
120 
121                 for (int p = 0; p < wd; p++) {
122                     final int mantissa = rgbe[p + eOffset] & 0xff;
123                     final int pos = p + i * wd;
124 
125                     if (0 == mantissa) {
126                         out[channel][pos] = 0;
127                     } else {
128                         final float mult = (float) Math.pow(2, mantissa - (128 + 8));
129                         out[channel][pos] = ((rgbe[p + channelOffset] & 0xff) + 0.5f) * mult;
130                     }
131                 }
132             }
133         }
134 
135         return out;
136     }
137 
138     int getWidth() throws IOException, ImagingException {
139         if (-1 == width) {
140             readDimensions();
141         }
142 
143         return width;
144     }
145 
146     private void readDimensions() throws IOException, ImagingException {
147         getMetadata(); // Ensure we've read past this
148 
149         final InfoHeaderReader reader = new InfoHeaderReader(in);
150         final String resolution = reader.readNextLine();
151         final Matcher matcher = RESOLUTION_STRING.matcher(resolution);
152 
153         if (!matcher.matches()) {
154             throw new ImagingException("Invalid HDR resolution string. Only \"-Y N +X M\" is supported. Found \"" + resolution + "\"");
155         }
156 
157         height = Integer.parseInt(matcher.group(1));
158         width = Integer.parseInt(matcher.group(2));
159     }
160 
161     private void readMetadata() throws IOException, ImagingException {
162         BinaryFunctions.readAndVerifyBytes(in, HEADER, "Not a valid HDR: Incorrect Header");
163 
164         final InfoHeaderReader reader = new InfoHeaderReader(in);
165 
166         if (!reader.readNextLine().isEmpty()) {
167             throw new ImagingException("Not a valid HDR: Incorrect Header");
168         }
169 
170         metadata = new GenericImageMetadata();
171 
172         String info = reader.readNextLine();
173 
174         while (!info.isEmpty()) {
175             final int equals = info.indexOf('=');
176 
177             if (equals > 0) {
178                 final String variable = info.substring(0, equals);
179                 final String value = info.substring(equals + 1);
180 
181                 if ("FORMAT".equals(value) && !"32-bit_rle_rgbe".equals(value)) {
182                     throw new ImagingException("Only 32-bit_rle_rgbe images are supported, trying to read " + value);
183                 }
184 
185                 metadata.add(variable, value);
186             } else {
187                 metadata.add("<command>", info);
188             }
189 
190             info = reader.readNextLine();
191         }
192     }
193 }