001/* 002 * Licensed under the Apache License, Version 2.0 (the "License"); 003 * you may not use this file except in compliance with the License. 004 * You may obtain a copy of the License at 005 * 006 * http://www.apache.org/licenses/LICENSE-2.0 007 * 008 * Unless required by applicable law or agreed to in writing, software 009 * distributed under the License is distributed on an "AS IS" BASIS, 010 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 011 * See the License for the specific language governing permissions and 012 * limitations under the License. 013 * under the License. 014 */ 015package org.apache.commons.imaging.formats.xbm; 016 017import static org.apache.commons.imaging.ImagingConstants.PARAM_KEY_FORMAT; 018 019import java.awt.Dimension; 020import java.awt.image.BufferedImage; 021import java.awt.image.ColorModel; 022import java.awt.image.DataBuffer; 023import java.awt.image.DataBufferByte; 024import java.awt.image.IndexColorModel; 025import java.awt.image.Raster; 026import java.awt.image.WritableRaster; 027import java.io.ByteArrayInputStream; 028import java.io.ByteArrayOutputStream; 029import java.io.IOException; 030import java.io.InputStream; 031import java.io.OutputStream; 032import java.io.PrintWriter; 033import java.nio.charset.StandardCharsets; 034import java.util.ArrayList; 035import java.util.HashMap; 036import java.util.Map; 037import java.util.Map.Entry; 038import java.util.Properties; 039import java.util.UUID; 040 041import org.apache.commons.imaging.ImageFormat; 042import org.apache.commons.imaging.ImageFormats; 043import org.apache.commons.imaging.ImageInfo; 044import org.apache.commons.imaging.ImageParser; 045import org.apache.commons.imaging.ImageReadException; 046import org.apache.commons.imaging.ImageWriteException; 047import org.apache.commons.imaging.common.BasicCParser; 048import org.apache.commons.imaging.common.ImageMetadata; 049import org.apache.commons.imaging.common.bytesource.ByteSource; 050 051public class XbmImageParser extends ImageParser { 052 private static final String DEFAULT_EXTENSION = ".xbm"; 053 private static final String[] ACCEPTED_EXTENSIONS = { ".xbm", }; 054 055 @Override 056 public String getName() { 057 return "X BitMap"; 058 } 059 060 @Override 061 public String getDefaultExtension() { 062 return DEFAULT_EXTENSION; 063 } 064 065 @Override 066 protected String[] getAcceptedExtensions() { 067 return ACCEPTED_EXTENSIONS; 068 } 069 070 @Override 071 protected ImageFormat[] getAcceptedTypes() { 072 return new ImageFormat[] { ImageFormats.XBM, // 073 }; 074 } 075 076 @Override 077 public ImageMetadata getMetadata(final ByteSource byteSource, final Map<String, Object> params) 078 throws ImageReadException, IOException { 079 return null; 080 } 081 082 @Override 083 public ImageInfo getImageInfo(final ByteSource byteSource, final Map<String, Object> params) 084 throws ImageReadException, IOException { 085 final XbmHeader xbmHeader = readXbmHeader(byteSource); 086 return new ImageInfo("XBM", 1, new ArrayList<String>(), 087 ImageFormats.XBM, "X BitMap", xbmHeader.height, 088 "image/x-xbitmap", 1, 0, 0, 0, 0, xbmHeader.width, false, 089 false, false, ImageInfo.ColorType.BW, 090 ImageInfo.CompressionAlgorithm.NONE); 091 } 092 093 @Override 094 public Dimension getImageSize(final ByteSource byteSource, final Map<String, Object> params) 095 throws ImageReadException, IOException { 096 final XbmHeader xbmHeader = readXbmHeader(byteSource); 097 return new Dimension(xbmHeader.width, xbmHeader.height); 098 } 099 100 @Override 101 public byte[] getICCProfileBytes(final ByteSource byteSource, final Map<String, Object> params) 102 throws ImageReadException, IOException { 103 return null; 104 } 105 106 private static class XbmHeader { 107 int width; 108 int height; 109 int xHot = -1; 110 int yHot = -1; 111 112 XbmHeader(final int width, final int height, final int xHot, final int yHot) { 113 this.width = width; 114 this.height = height; 115 this.xHot = xHot; 116 this.yHot = yHot; 117 } 118 119 public void dump(final PrintWriter pw) { 120 pw.println("XbmHeader"); 121 pw.println("Width: " + width); 122 pw.println("Height: " + height); 123 if (xHot != -1 && yHot != -1) { 124 pw.println("X hot: " + xHot); 125 pw.println("Y hot: " + yHot); 126 } 127 } 128 } 129 130 private static class XbmParseResult { 131 XbmHeader xbmHeader; 132 BasicCParser cParser; 133 } 134 135 private XbmHeader readXbmHeader(final ByteSource byteSource) 136 throws ImageReadException, IOException { 137 return parseXbmHeader(byteSource).xbmHeader; 138 } 139 140 private XbmParseResult parseXbmHeader(final ByteSource byteSource) 141 throws ImageReadException, IOException { 142 try (InputStream is = byteSource.getInputStream()) { 143 final Map<String, String> defines = new HashMap<>(); 144 final ByteArrayOutputStream preprocessedFile = BasicCParser.preprocess( 145 is, null, defines); 146 int width = -1; 147 int height = -1; 148 int xHot = -1; 149 int yHot = -1; 150 for (final Entry<String, String> entry : defines.entrySet()) { 151 final String name = entry.getKey(); 152 if (name.endsWith("_width")) { 153 width = parseCIntegerLiteral(entry.getValue()); 154 } else if (name.endsWith("_height")) { 155 height = parseCIntegerLiteral(entry.getValue()); 156 } else if (name.endsWith("_x_hot")) { 157 xHot = parseCIntegerLiteral(entry.getValue()); 158 } else if (name.endsWith("_y_hot")) { 159 yHot = parseCIntegerLiteral(entry.getValue()); 160 } 161 } 162 if (width == -1) { 163 throw new ImageReadException("width not found"); 164 } 165 if (height == -1) { 166 throw new ImageReadException("height not found"); 167 } 168 169 final XbmParseResult xbmParseResult = new XbmParseResult(); 170 xbmParseResult.cParser = new BasicCParser(new ByteArrayInputStream( 171 preprocessedFile.toByteArray())); 172 xbmParseResult.xbmHeader = new XbmHeader(width, height, xHot, yHot); 173 return xbmParseResult; 174 } 175 } 176 177 private static int parseCIntegerLiteral(final String value) { 178 if (value.startsWith("0")) { 179 if (value.length() >= 2) { 180 if (value.charAt(1) == 'x' || value.charAt(1) == 'X') { 181 return Integer.parseInt(value.substring(2), 16); 182 } else { 183 return Integer.parseInt(value.substring(1), 8); 184 } 185 } else { 186 return 0; 187 } 188 } else { 189 return Integer.parseInt(value); 190 } 191 } 192 193 private BufferedImage readXbmImage(final XbmHeader xbmHeader, final BasicCParser cParser) 194 throws ImageReadException, IOException { 195 String token; 196 token = cParser.nextToken(); 197 if (!"static".equals(token)) { 198 throw new ImageReadException( 199 "Parsing XBM file failed, no 'static' token"); 200 } 201 token = cParser.nextToken(); 202 if (token == null) { 203 throw new ImageReadException( 204 "Parsing XBM file failed, no 'unsigned' " 205 + "or 'char' or 'short' token"); 206 } 207 if ("unsigned".equals(token)) { 208 token = cParser.nextToken(); 209 } 210 final int inputWidth; 211 final int hexWidth; 212 if ("char".equals(token)) { 213 inputWidth = 8; 214 hexWidth = 4; // 0xab 215 } else if ("short".equals(token)) { 216 inputWidth = 16; 217 hexWidth = 6; // 0xabcd 218 } else { 219 throw new ImageReadException( 220 "Parsing XBM file failed, no 'char' or 'short' token"); 221 } 222 final String name = cParser.nextToken(); 223 if (name == null) { 224 throw new ImageReadException( 225 "Parsing XBM file failed, no variable name"); 226 } 227 if (name.charAt(0) != '_' && !Character.isLetter(name.charAt(0))) { 228 throw new ImageReadException( 229 "Parsing XBM file failed, variable name " 230 + "doesn't start with letter or underscore"); 231 } 232 for (int i = 0; i < name.length(); i++) { 233 final char c = name.charAt(i); 234 if (!Character.isLetterOrDigit(c) && c != '_') { 235 throw new ImageReadException( 236 "Parsing XBM file failed, variable name " 237 + "contains non-letter non-digit non-underscore"); 238 } 239 } 240 token = cParser.nextToken(); 241 if (!"[".equals(token)) { 242 throw new ImageReadException( 243 "Parsing XBM file failed, no '[' token"); 244 } 245 token = cParser.nextToken(); 246 if (!"]".equals(token)) { 247 throw new ImageReadException( 248 "Parsing XBM file failed, no ']' token"); 249 } 250 token = cParser.nextToken(); 251 if (!"=".equals(token)) { 252 throw new ImageReadException( 253 "Parsing XBM file failed, no '=' token"); 254 } 255 token = cParser.nextToken(); 256 if (!"{".equals(token)) { 257 throw new ImageReadException( 258 "Parsing XBM file failed, no '{' token"); 259 } 260 261 final int rowLength = (xbmHeader.width + 7) / 8; 262 final byte[] imageData = new byte[rowLength * xbmHeader.height]; 263 int i = 0; 264 for (int y = 0; y < xbmHeader.height; y++) { 265 for (int x = 0; x < xbmHeader.width; x += inputWidth) { 266 token = cParser.nextToken(); 267 if (token == null || !token.startsWith("0x")) { 268 throw new ImageReadException("Parsing XBM file failed, " 269 + "hex value missing"); 270 } 271 if (token.length() > hexWidth) { 272 throw new ImageReadException("Parsing XBM file failed, " 273 + "hex value too long"); 274 } 275 final int value = Integer.parseInt(token.substring(2), 16); 276 final int flipped = Integer.reverse(value) >>> (32 - inputWidth); 277 if (inputWidth == 16) { 278 imageData[i++] = (byte) (flipped >>> 8); 279 if ((x + 8) < xbmHeader.width) { 280 imageData[i++] = (byte) flipped; 281 } 282 } else { 283 imageData[i++] = (byte) flipped; 284 } 285 286 token = cParser.nextToken(); 287 if (token == null) { 288 throw new ImageReadException("Parsing XBM file failed, " 289 + "premature end of file"); 290 } 291 if (!",".equals(token) 292 && ((i < imageData.length) || !"}".equals(token))) { 293 throw new ImageReadException("Parsing XBM file failed, " 294 + "punctuation error"); 295 } 296 } 297 } 298 299 final int[] palette = { 0xffffff, 0x000000 }; 300 final ColorModel colorModel = new IndexColorModel(1, 2, palette, 0, false, -1, DataBuffer.TYPE_BYTE); 301 final DataBufferByte dataBuffer = new DataBufferByte(imageData, imageData.length); 302 final WritableRaster raster = Raster.createPackedRaster(dataBuffer, xbmHeader.width, xbmHeader.height, 1, null); 303 304 return new BufferedImage(colorModel, raster, colorModel.isAlphaPremultiplied(), new Properties()); 305 } 306 307 @Override 308 public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) 309 throws ImageReadException, IOException { 310 readXbmHeader(byteSource).dump(pw); 311 return true; 312 } 313 314 @Override 315 public final BufferedImage getBufferedImage(final ByteSource byteSource, 316 final Map<String, Object> params) throws ImageReadException, IOException { 317 final XbmParseResult result = parseXbmHeader(byteSource); 318 return readXbmImage(result.xbmHeader, result.cParser); 319 } 320 321 private static String randomName() { 322 final UUID uuid = UUID.randomUUID(); 323 final StringBuilder stringBuilder = new StringBuilder("a"); 324 long bits = uuid.getMostSignificantBits(); 325 // Long.toHexString() breaks for very big numbers 326 for (int i = 64 - 8; i >= 0; i -= 8) { 327 stringBuilder.append(Integer.toHexString((int) ((bits >> i) & 0xff))); 328 } 329 bits = uuid.getLeastSignificantBits(); 330 for (int i = 64 - 8; i >= 0; i -= 8) { 331 stringBuilder.append(Integer.toHexString((int) ((bits >> i) & 0xff))); 332 } 333 return stringBuilder.toString(); 334 } 335 336 private static String toPrettyHex(final int value) { 337 final String s = Integer.toHexString(0xff & value); 338 if (s.length() == 2) { 339 return "0x" + s; 340 } 341 return "0x0" + s; 342 } 343 344 @Override 345 public void writeImage(final BufferedImage src, final OutputStream os, Map<String, Object> params) 346 throws ImageWriteException, IOException { 347 // make copy of params; we'll clear keys as we consume them. 348 params = (params == null) ? new HashMap<>() : new HashMap<>(params); 349 350 // clear format key. 351 if (params.containsKey(PARAM_KEY_FORMAT)) { 352 params.remove(PARAM_KEY_FORMAT); 353 } 354 355 if (!params.isEmpty()) { 356 final Object firstKey = params.keySet().iterator().next(); 357 throw new ImageWriteException("Unknown parameter: " + firstKey); 358 } 359 360 final String name = randomName(); 361 362 os.write(("#define " + name + "_width " + src.getWidth() + "\n").getBytes(StandardCharsets.US_ASCII)); 363 os.write(("#define " + name + "_height " + src.getHeight() + "\n").getBytes(StandardCharsets.US_ASCII)); 364 os.write(("static unsigned char " + name + "_bits[] = {").getBytes(StandardCharsets.US_ASCII)); 365 366 int bitcache = 0; 367 int bitsInCache = 0; 368 String separator = "\n "; 369 int written = 0; 370 for (int y = 0; y < src.getHeight(); y++) { 371 for (int x = 0; x < src.getWidth(); x++) { 372 final int argb = src.getRGB(x, y); 373 final int red = 0xff & (argb >> 16); 374 final int green = 0xff & (argb >> 8); 375 final int blue = 0xff & (argb >> 0); 376 int sample = (red + green + blue) / 3; 377 if (sample > 127) { 378 sample = 0; 379 } else { 380 sample = 1; 381 } 382 bitcache |= (sample << bitsInCache); 383 ++bitsInCache; 384 if (bitsInCache == 8) { 385 os.write(separator.getBytes(StandardCharsets.US_ASCII)); 386 separator = ","; 387 if (written == 12) { 388 os.write("\n ".getBytes(StandardCharsets.US_ASCII)); 389 written = 0; 390 } 391 os.write(toPrettyHex(bitcache).getBytes(StandardCharsets.US_ASCII)); 392 bitcache = 0; 393 bitsInCache = 0; 394 ++written; 395 } 396 } 397 if (bitsInCache != 0) { 398 os.write(separator.getBytes(StandardCharsets.US_ASCII)); 399 separator = ","; 400 if (written == 12) { 401 os.write("\n ".getBytes(StandardCharsets.US_ASCII)); 402 written = 0; 403 } 404 os.write(toPrettyHex(bitcache).getBytes(StandardCharsets.US_ASCII)); 405 bitcache = 0; 406 bitsInCache = 0; 407 ++written; 408 } 409 } 410 411 os.write("\n};\n".getBytes(StandardCharsets.US_ASCII)); 412 } 413}