1
2
3
4
5
6
7
8
9
10
11
12
13
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
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
122
123 public XbmImageParser() {
124
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;
244 } else if ("short".equals(token)) {
245 inputWidth = 16;
246 hexWidth = 6;
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 }