View Javadoc
1   /*
2    *  Licensed under the Apache License, Version 2.0 (the "License");
3    *  you may not use this file except in compliance with the License.
4    *  You may obtain a copy of the License at
5    *
6    *       http://www.apache.org/licenses/LICENSE-2.0
7    *
8    *  Unless required by applicable law or agreed to in writing, software
9    *  distributed under the License is distributed on an "AS IS" BASIS,
10   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11   *  See the License for the specific language governing permissions and
12   *  limitations under the License.
13   *  under the License.
14   */
15  package org.apache.commons.imaging.formats.xbm;
16  
17  import java.awt.Dimension;
18  import java.awt.image.BufferedImage;
19  import java.awt.image.ColorModel;
20  import java.awt.image.DataBuffer;
21  import java.awt.image.DataBufferByte;
22  import java.awt.image.IndexColorModel;
23  import java.awt.image.Raster;
24  import java.awt.image.WritableRaster;
25  import java.io.ByteArrayInputStream;
26  import java.io.ByteArrayOutputStream;
27  import java.io.IOException;
28  import java.io.InputStream;
29  import java.io.OutputStream;
30  import java.io.PrintWriter;
31  import java.nio.charset.StandardCharsets;
32  import java.util.ArrayList;
33  import java.util.HashMap;
34  import java.util.Map;
35  import java.util.Map.Entry;
36  import java.util.Properties;
37  import java.util.UUID;
38  
39  import org.apache.commons.imaging.AbstractImageParser;
40  import org.apache.commons.imaging.ImageFormat;
41  import org.apache.commons.imaging.ImageFormats;
42  import org.apache.commons.imaging.ImageInfo;
43  import org.apache.commons.imaging.ImagingException;
44  import org.apache.commons.imaging.bytesource.ByteSource;
45  import org.apache.commons.imaging.common.Allocator;
46  import org.apache.commons.imaging.common.BasicCParser;
47  import org.apache.commons.imaging.common.ImageMetadata;
48  
49  public class XbmImageParser extends AbstractImageParser<XbmImagingParameters> {
50  
51      private static final class XbmHeader {
52          final int height;
53          final int width;
54          int xHot = -1;
55          int yHot = -1;
56  
57          XbmHeader(final int width, final int height, final int xHot, final int yHot) {
58              this.width = width;
59              this.height = height;
60              this.xHot = xHot;
61              this.yHot = yHot;
62          }
63  
64          public void dump(final PrintWriter pw) {
65              pw.println("XbmHeader");
66              pw.println("Width: " + width);
67              pw.println("Height: " + height);
68              if (xHot != -1 && yHot != -1) {
69                  pw.println("X hot: " + xHot);
70                  pw.println("Y hot: " + yHot);
71              }
72          }
73      }
74  
75      private static final class XbmParseResult {
76          BasicCParser cParser;
77          XbmHeader xbmHeader;
78      }
79  
80      private static final String[] ACCEPTED_EXTENSIONS = ImageFormats.XBM.getExtensions();
81  
82      private static final String DEFAULT_EXTENSION = ImageFormats.XBM.getDefaultExtension();
83  
84      private static int parseCIntegerLiteral(final String value) {
85          if (value.startsWith("0")) {
86              if (value.length() >= 2) {
87                  if (value.charAt(1) == 'x' || value.charAt(1) == 'X') {
88                      return Integer.parseInt(value.substring(2), 16);
89                  }
90                  return Integer.parseInt(value.substring(1), 8);
91              }
92              return 0;
93          }
94          return Integer.parseInt(value);
95      }
96  
97      private static String randomName() {
98          final UUID uuid = UUID.randomUUID();
99          final StringBuilder stringBuilder = new StringBuilder("a");
100         long bits = uuid.getMostSignificantBits();
101         // Long.toHexString() breaks for very big numbers
102         for (int i = 64 - 8; i >= 0; i -= 8) {
103             stringBuilder.append(Integer.toHexString((int) (bits >> i & 0xff)));
104         }
105         bits = uuid.getLeastSignificantBits();
106         for (int i = 64 - 8; i >= 0; i -= 8) {
107             stringBuilder.append(Integer.toHexString((int) (bits >> i & 0xff)));
108         }
109         return stringBuilder.toString();
110     }
111 
112     private static String toPrettyHex(final int value) {
113         final String s = Integer.toHexString(0xff & value);
114         if (s.length() == 2) {
115             return "0x" + s;
116         }
117         return "0x0" + s;
118     }
119 
120     /**
121      * Constructs a new instance with the big-endian byte order.
122      */
123     public XbmImageParser() {
124         // empty
125     }
126 
127     @Override
128     public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) throws ImagingException, IOException {
129         readXbmHeader(byteSource).dump(pw);
130         return true;
131     }
132 
133     @Override
134     protected String[] getAcceptedExtensions() {
135         return ACCEPTED_EXTENSIONS;
136     }
137 
138     @Override
139     protected ImageFormat[] getAcceptedTypes() {
140         return new ImageFormat[] { ImageFormats.XBM, //
141         };
142     }
143 
144     @Override
145     public final BufferedImage getBufferedImage(final ByteSource byteSource, final XbmImagingParameters params) throws ImagingException, IOException {
146         final XbmParseResult result = parseXbmHeader(byteSource);
147         return readXbmImage(result.xbmHeader, result.cParser);
148     }
149 
150     @Override
151     public String getDefaultExtension() {
152         return DEFAULT_EXTENSION;
153     }
154 
155     @Override
156     public XbmImagingParameters getDefaultParameters() {
157         return new XbmImagingParameters();
158     }
159 
160     @Override
161     public byte[] getIccProfileBytes(final ByteSource byteSource, final XbmImagingParameters params) throws ImagingException, IOException {
162         return null;
163     }
164 
165     @Override
166     public ImageInfo getImageInfo(final ByteSource byteSource, final XbmImagingParameters params) throws ImagingException, IOException {
167         final XbmHeader xbmHeader = readXbmHeader(byteSource);
168         return new ImageInfo("XBM", 1, new ArrayList<>(), ImageFormats.XBM, "X BitMap", xbmHeader.height, "image/x-xbitmap", 1, 0, 0, 0, 0, xbmHeader.width,
169                 false, false, false, ImageInfo.ColorType.BW, ImageInfo.CompressionAlgorithm.NONE);
170     }
171 
172     @Override
173     public Dimension getImageSize(final ByteSource byteSource, final XbmImagingParameters params) throws ImagingException, IOException {
174         final XbmHeader xbmHeader = readXbmHeader(byteSource);
175         return new Dimension(xbmHeader.width, xbmHeader.height);
176     }
177 
178     @Override
179     public ImageMetadata getMetadata(final ByteSource byteSource, final XbmImagingParameters params) throws ImagingException, IOException {
180         return null;
181     }
182 
183     @Override
184     public String getName() {
185         return "X BitMap";
186     }
187 
188     private XbmParseResult parseXbmHeader(final ByteSource byteSource) throws ImagingException, IOException {
189         try (InputStream is = byteSource.getInputStream()) {
190             final Map<String, String> defines = new HashMap<>();
191             final ByteArrayOutputStream preprocessedFile = BasicCParser.preprocess(is, null, defines);
192             int width = -1;
193             int height = -1;
194             int xHot = -1;
195             int yHot = -1;
196             for (final Entry<String, String> entry : defines.entrySet()) {
197                 final String name = entry.getKey();
198                 if (name.endsWith("_width")) {
199                     width = parseCIntegerLiteral(entry.getValue());
200                 } else if (name.endsWith("_height")) {
201                     height = parseCIntegerLiteral(entry.getValue());
202                 } else if (name.endsWith("_x_hot")) {
203                     xHot = parseCIntegerLiteral(entry.getValue());
204                 } else if (name.endsWith("_y_hot")) {
205                     yHot = parseCIntegerLiteral(entry.getValue());
206                 }
207             }
208             if (width == -1) {
209                 throw new ImagingException("width not found");
210             }
211             if (height == -1) {
212                 throw new ImagingException("height not found");
213             }
214 
215             final XbmParseResult xbmParseResult = new XbmParseResult();
216             xbmParseResult.cParser = new BasicCParser(new ByteArrayInputStream(preprocessedFile.toByteArray()));
217             xbmParseResult.xbmHeader = new XbmHeader(width, height, xHot, yHot);
218             return xbmParseResult;
219         }
220     }
221 
222     private XbmHeader readXbmHeader(final ByteSource byteSource) throws ImagingException, IOException {
223         return parseXbmHeader(byteSource).xbmHeader;
224     }
225 
226     private BufferedImage readXbmImage(final XbmHeader xbmHeader, final BasicCParser cParser) throws ImagingException, IOException {
227         String token;
228         token = cParser.nextToken();
229         if (!"static".equals(token)) {
230             throw new ImagingException("Parsing XBM file failed, no 'static' token");
231         }
232         token = cParser.nextToken();
233         if (token == null) {
234             throw new ImagingException("Parsing XBM file failed, no 'unsigned' " + "or 'char' or 'short' token");
235         }
236         if ("unsigned".equals(token)) {
237             token = cParser.nextToken();
238         }
239         final int inputWidth;
240         final int hexWidth;
241         if ("char".equals(token)) {
242             inputWidth = 8;
243             hexWidth = 4; // 0xab
244         } else if ("short".equals(token)) {
245             inputWidth = 16;
246             hexWidth = 6; // 0xabcd
247         } else {
248             throw new ImagingException("Parsing XBM file failed, no 'char' or 'short' token");
249         }
250         final String name = cParser.nextToken();
251         if (name == null) {
252             throw new ImagingException("Parsing XBM file failed, no variable name");
253         }
254         if (name.charAt(0) != '_' && !Character.isLetter(name.charAt(0))) {
255             throw new ImagingException("Parsing XBM file failed, variable name " + "doesn't start with letter or underscore");
256         }
257         for (int i = 0; i < name.length(); i++) {
258             final char c = name.charAt(i);
259             if (!Character.isLetterOrDigit(c) && c != '_') {
260                 throw new ImagingException("Parsing XBM file failed, variable name " + "contains non-letter non-digit non-underscore");
261             }
262         }
263         token = cParser.nextToken();
264         if (!"[".equals(token)) {
265             throw new ImagingException("Parsing XBM file failed, no '[' token");
266         }
267         token = cParser.nextToken();
268         if (!"]".equals(token)) {
269             throw new ImagingException("Parsing XBM file failed, no ']' token");
270         }
271         token = cParser.nextToken();
272         if (!"=".equals(token)) {
273             throw new ImagingException("Parsing XBM file failed, no '=' token");
274         }
275         token = cParser.nextToken();
276         if (!"{".equals(token)) {
277             throw new ImagingException("Parsing XBM file failed, no '{' token");
278         }
279 
280         final int rowLength = (xbmHeader.width + 7) / 8;
281         final byte[] imageData = Allocator.byteArray(rowLength * xbmHeader.height);
282         int i = 0;
283         for (int y = 0; y < xbmHeader.height; y++) {
284             for (int x = 0; x < xbmHeader.width; x += inputWidth) {
285                 token = cParser.nextToken();
286                 if (token == null || !token.startsWith("0x")) {
287                     throw new ImagingException("Parsing XBM file failed, " + "hex value missing");
288                 }
289                 if (token.length() > hexWidth) {
290                     throw new ImagingException("Parsing XBM file failed, " + "hex value too long");
291                 }
292                 final int value = Integer.parseInt(token.substring(2), 16);
293                 final int flipped = Integer.reverse(value) >>> 32 - inputWidth;
294                 if (inputWidth == 16) {
295                     imageData[i++] = (byte) (flipped >>> 8);
296                     if (x + 8 < xbmHeader.width) {
297                         imageData[i++] = (byte) flipped;
298                     }
299                 } else {
300                     imageData[i++] = (byte) flipped;
301                 }
302 
303                 token = cParser.nextToken();
304                 if (token == null) {
305                     throw new ImagingException("Parsing XBM file failed, " + "premature end of file");
306                 }
307                 if (!",".equals(token) && (i < imageData.length || !"}".equals(token))) {
308                     throw new ImagingException("Parsing XBM file failed, " + "punctuation error");
309                 }
310             }
311         }
312 
313         final int[] palette = { 0xffffff, 0x000000 };
314         final ColorModel colorModel = new IndexColorModel(1, 2, palette, 0, false, -1, DataBuffer.TYPE_BYTE);
315         final DataBufferByte dataBuffer = new DataBufferByte(imageData, imageData.length);
316         final WritableRaster raster = Raster.createPackedRaster(dataBuffer, xbmHeader.width, xbmHeader.height, 1, null);
317 
318         return new BufferedImage(colorModel, raster, colorModel.isAlphaPremultiplied(), new Properties());
319     }
320 
321     @Override
322     public void writeImage(final BufferedImage src, final OutputStream os, final XbmImagingParameters params) throws ImagingException, IOException {
323         final String name = randomName();
324 
325         os.write(("#define " + name + "_width " + src.getWidth() + "\n").getBytes(StandardCharsets.US_ASCII));
326         os.write(("#define " + name + "_height " + src.getHeight() + "\n").getBytes(StandardCharsets.US_ASCII));
327         os.write(("static unsigned char " + name + "_bits[] = {").getBytes(StandardCharsets.US_ASCII));
328 
329         int bitcache = 0;
330         int bitsInCache = 0;
331         String separator = "\n  ";
332         int written = 0;
333         for (int y = 0; y < src.getHeight(); y++) {
334             for (int x = 0; x < src.getWidth(); x++) {
335                 final int argb = src.getRGB(x, y);
336                 final int red = 0xff & argb >> 16;
337                 final int green = 0xff & argb >> 8;
338                 final int blue = 0xff & argb >> 0;
339                 int sample = (red + green + blue) / 3;
340                 if (sample > 127) {
341                     sample = 0;
342                 } else {
343                     sample = 1;
344                 }
345                 bitcache |= sample << bitsInCache;
346                 ++bitsInCache;
347                 if (bitsInCache == 8) {
348                     os.write(separator.getBytes(StandardCharsets.US_ASCII));
349                     separator = ",";
350                     if (written == 12) {
351                         os.write("\n  ".getBytes(StandardCharsets.US_ASCII));
352                         written = 0;
353                     }
354                     os.write(toPrettyHex(bitcache).getBytes(StandardCharsets.US_ASCII));
355                     bitcache = 0;
356                     bitsInCache = 0;
357                     ++written;
358                 }
359             }
360             if (bitsInCache != 0) {
361                 os.write(separator.getBytes(StandardCharsets.US_ASCII));
362                 separator = ",";
363                 if (written == 12) {
364                     os.write("\n  ".getBytes(StandardCharsets.US_ASCII));
365                     written = 0;
366                 }
367                 os.write(toPrettyHex(bitcache).getBytes(StandardCharsets.US_ASCII));
368                 bitcache = 0;
369                 bitsInCache = 0;
370                 ++written;
371             }
372         }
373 
374         os.write("\n};\n".getBytes(StandardCharsets.US_ASCII));
375     }
376 }