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 @Override
121 public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) throws ImagingException, IOException {
122 readXbmHeader(byteSource).dump(pw);
123 return true;
124 }
125
126 @Override
127 protected String[] getAcceptedExtensions() {
128 return ACCEPTED_EXTENSIONS;
129 }
130
131 @Override
132 protected ImageFormat[] getAcceptedTypes() {
133 return new ImageFormat[] { ImageFormats.XBM,
134 };
135 }
136
137 @Override
138 public final BufferedImage getBufferedImage(final ByteSource byteSource, final XbmImagingParameters params) throws ImagingException, IOException {
139 final XbmParseResult result = parseXbmHeader(byteSource);
140 return readXbmImage(result.xbmHeader, result.cParser);
141 }
142
143 @Override
144 public String getDefaultExtension() {
145 return DEFAULT_EXTENSION;
146 }
147
148 @Override
149 public XbmImagingParameters getDefaultParameters() {
150 return new XbmImagingParameters();
151 }
152
153 @Override
154 public byte[] getIccProfileBytes(final ByteSource byteSource, final XbmImagingParameters params) throws ImagingException, IOException {
155 return null;
156 }
157
158 @Override
159 public ImageInfo getImageInfo(final ByteSource byteSource, final XbmImagingParameters params) throws ImagingException, IOException {
160 final XbmHeader xbmHeader = readXbmHeader(byteSource);
161 return new ImageInfo("XBM", 1, new ArrayList<>(), ImageFormats.XBM, "X BitMap", xbmHeader.height, "image/x-xbitmap", 1, 0, 0, 0, 0, xbmHeader.width,
162 false, false, false, ImageInfo.ColorType.BW, ImageInfo.CompressionAlgorithm.NONE);
163 }
164
165 @Override
166 public Dimension getImageSize(final ByteSource byteSource, final XbmImagingParameters params) throws ImagingException, IOException {
167 final XbmHeader xbmHeader = readXbmHeader(byteSource);
168 return new Dimension(xbmHeader.width, xbmHeader.height);
169 }
170
171 @Override
172 public ImageMetadata getMetadata(final ByteSource byteSource, final XbmImagingParameters params) throws ImagingException, IOException {
173 return null;
174 }
175
176 @Override
177 public String getName() {
178 return "X BitMap";
179 }
180
181 private XbmParseResult parseXbmHeader(final ByteSource byteSource) throws ImagingException, IOException {
182 try (InputStream is = byteSource.getInputStream()) {
183 final Map<String, String> defines = new HashMap<>();
184 final ByteArrayOutputStream preprocessedFile = BasicCParser.preprocess(is, null, defines);
185 int width = -1;
186 int height = -1;
187 int xHot = -1;
188 int yHot = -1;
189 for (final Entry<String, String> entry : defines.entrySet()) {
190 final String name = entry.getKey();
191 if (name.endsWith("_width")) {
192 width = parseCIntegerLiteral(entry.getValue());
193 } else if (name.endsWith("_height")) {
194 height = parseCIntegerLiteral(entry.getValue());
195 } else if (name.endsWith("_x_hot")) {
196 xHot = parseCIntegerLiteral(entry.getValue());
197 } else if (name.endsWith("_y_hot")) {
198 yHot = parseCIntegerLiteral(entry.getValue());
199 }
200 }
201 if (width == -1) {
202 throw new ImagingException("width not found");
203 }
204 if (height == -1) {
205 throw new ImagingException("height not found");
206 }
207
208 final XbmParseResult xbmParseResult = new XbmParseResult();
209 xbmParseResult.cParser = new BasicCParser(new ByteArrayInputStream(preprocessedFile.toByteArray()));
210 xbmParseResult.xbmHeader = new XbmHeader(width, height, xHot, yHot);
211 return xbmParseResult;
212 }
213 }
214
215 private XbmHeader readXbmHeader(final ByteSource byteSource) throws ImagingException, IOException {
216 return parseXbmHeader(byteSource).xbmHeader;
217 }
218
219 private BufferedImage readXbmImage(final XbmHeader xbmHeader, final BasicCParser cParser) throws ImagingException, IOException {
220 String token;
221 token = cParser.nextToken();
222 if (!"static".equals(token)) {
223 throw new ImagingException("Parsing XBM file failed, no 'static' token");
224 }
225 token = cParser.nextToken();
226 if (token == null) {
227 throw new ImagingException("Parsing XBM file failed, no 'unsigned' " + "or 'char' or 'short' token");
228 }
229 if ("unsigned".equals(token)) {
230 token = cParser.nextToken();
231 }
232 final int inputWidth;
233 final int hexWidth;
234 if ("char".equals(token)) {
235 inputWidth = 8;
236 hexWidth = 4;
237 } else if ("short".equals(token)) {
238 inputWidth = 16;
239 hexWidth = 6;
240 } else {
241 throw new ImagingException("Parsing XBM file failed, no 'char' or 'short' token");
242 }
243 final String name = cParser.nextToken();
244 if (name == null) {
245 throw new ImagingException("Parsing XBM file failed, no variable name");
246 }
247 if (name.charAt(0) != '_' && !Character.isLetter(name.charAt(0))) {
248 throw new ImagingException("Parsing XBM file failed, variable name " + "doesn't start with letter or underscore");
249 }
250 for (int i = 0; i < name.length(); i++) {
251 final char c = name.charAt(i);
252 if (!Character.isLetterOrDigit(c) && c != '_') {
253 throw new ImagingException("Parsing XBM file failed, variable name " + "contains non-letter non-digit non-underscore");
254 }
255 }
256 token = cParser.nextToken();
257 if (!"[".equals(token)) {
258 throw new ImagingException("Parsing XBM file failed, no '[' token");
259 }
260 token = cParser.nextToken();
261 if (!"]".equals(token)) {
262 throw new ImagingException("Parsing XBM file failed, no ']' token");
263 }
264 token = cParser.nextToken();
265 if (!"=".equals(token)) {
266 throw new ImagingException("Parsing XBM file failed, no '=' token");
267 }
268 token = cParser.nextToken();
269 if (!"{".equals(token)) {
270 throw new ImagingException("Parsing XBM file failed, no '{' token");
271 }
272
273 final int rowLength = (xbmHeader.width + 7) / 8;
274 final byte[] imageData = Allocator.byteArray(rowLength * xbmHeader.height);
275 int i = 0;
276 for (int y = 0; y < xbmHeader.height; y++) {
277 for (int x = 0; x < xbmHeader.width; x += inputWidth) {
278 token = cParser.nextToken();
279 if (token == null || !token.startsWith("0x")) {
280 throw new ImagingException("Parsing XBM file failed, " + "hex value missing");
281 }
282 if (token.length() > hexWidth) {
283 throw new ImagingException("Parsing XBM file failed, " + "hex value too long");
284 }
285 final int value = Integer.parseInt(token.substring(2), 16);
286 final int flipped = Integer.reverse(value) >>> 32 - inputWidth;
287 if (inputWidth == 16) {
288 imageData[i++] = (byte) (flipped >>> 8);
289 if (x + 8 < xbmHeader.width) {
290 imageData[i++] = (byte) flipped;
291 }
292 } else {
293 imageData[i++] = (byte) flipped;
294 }
295
296 token = cParser.nextToken();
297 if (token == null) {
298 throw new ImagingException("Parsing XBM file failed, " + "premature end of file");
299 }
300 if (!",".equals(token) && (i < imageData.length || !"}".equals(token))) {
301 throw new ImagingException("Parsing XBM file failed, " + "punctuation error");
302 }
303 }
304 }
305
306 final int[] palette = { 0xffffff, 0x000000 };
307 final ColorModel colorModel = new IndexColorModel(1, 2, palette, 0, false, -1, DataBuffer.TYPE_BYTE);
308 final DataBufferByte dataBuffer = new DataBufferByte(imageData, imageData.length);
309 final WritableRaster raster = Raster.createPackedRaster(dataBuffer, xbmHeader.width, xbmHeader.height, 1, null);
310
311 return new BufferedImage(colorModel, raster, colorModel.isAlphaPremultiplied(), new Properties());
312 }
313
314 @Override
315 public void writeImage(final BufferedImage src, final OutputStream os, final XbmImagingParameters params) throws ImagingException, IOException {
316 final String name = randomName();
317
318 os.write(("#define " + name + "_width " + src.getWidth() + "\n").getBytes(StandardCharsets.US_ASCII));
319 os.write(("#define " + name + "_height " + src.getHeight() + "\n").getBytes(StandardCharsets.US_ASCII));
320 os.write(("static unsigned char " + name + "_bits[] = {").getBytes(StandardCharsets.US_ASCII));
321
322 int bitcache = 0;
323 int bitsInCache = 0;
324 String separator = "\n ";
325 int written = 0;
326 for (int y = 0; y < src.getHeight(); y++) {
327 for (int x = 0; x < src.getWidth(); x++) {
328 final int argb = src.getRGB(x, y);
329 final int red = 0xff & argb >> 16;
330 final int green = 0xff & argb >> 8;
331 final int blue = 0xff & argb >> 0;
332 int sample = (red + green + blue) / 3;
333 if (sample > 127) {
334 sample = 0;
335 } else {
336 sample = 1;
337 }
338 bitcache |= sample << bitsInCache;
339 ++bitsInCache;
340 if (bitsInCache == 8) {
341 os.write(separator.getBytes(StandardCharsets.US_ASCII));
342 separator = ",";
343 if (written == 12) {
344 os.write("\n ".getBytes(StandardCharsets.US_ASCII));
345 written = 0;
346 }
347 os.write(toPrettyHex(bitcache).getBytes(StandardCharsets.US_ASCII));
348 bitcache = 0;
349 bitsInCache = 0;
350 ++written;
351 }
352 }
353 if (bitsInCache != 0) {
354 os.write(separator.getBytes(StandardCharsets.US_ASCII));
355 separator = ",";
356 if (written == 12) {
357 os.write("\n ".getBytes(StandardCharsets.US_ASCII));
358 written = 0;
359 }
360 os.write(toPrettyHex(bitcache).getBytes(StandardCharsets.US_ASCII));
361 bitcache = 0;
362 bitsInCache = 0;
363 ++written;
364 }
365 }
366
367 os.write("\n};\n".getBytes(StandardCharsets.US_ASCII));
368 }
369 }