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 java.awt.Dimension; 018import java.awt.image.BufferedImage; 019import java.awt.image.ColorModel; 020import java.awt.image.DataBuffer; 021import java.awt.image.DataBufferByte; 022import java.awt.image.IndexColorModel; 023import java.awt.image.Raster; 024import java.awt.image.WritableRaster; 025import java.io.ByteArrayInputStream; 026import java.io.ByteArrayOutputStream; 027import java.io.IOException; 028import java.io.InputStream; 029import java.io.OutputStream; 030import java.io.PrintWriter; 031import java.nio.charset.StandardCharsets; 032import java.util.ArrayList; 033import java.util.HashMap; 034import java.util.Map; 035import java.util.Map.Entry; 036import java.util.Properties; 037import java.util.UUID; 038 039import org.apache.commons.imaging.ImageFormat; 040import org.apache.commons.imaging.ImageFormats; 041import org.apache.commons.imaging.ImageInfo; 042import org.apache.commons.imaging.ImageParser; 043import org.apache.commons.imaging.ImageReadException; 044import org.apache.commons.imaging.ImageWriteException; 045import org.apache.commons.imaging.common.BasicCParser; 046import org.apache.commons.imaging.common.ImageMetadata; 047import org.apache.commons.imaging.common.bytesource.ByteSource; 048 049public class XbmImageParser extends ImageParser<XbmImagingParameters> { 050 private static final String DEFAULT_EXTENSION = ImageFormats.XBM.getDefaultExtension(); 051 private static final String[] ACCEPTED_EXTENSIONS = ImageFormats.XBM.getExtensions(); 052 053 @Override 054 public XbmImagingParameters getDefaultParameters() { 055 return new XbmImagingParameters(); 056 } 057 058 @Override 059 public String getName() { 060 return "X BitMap"; 061 } 062 063 @Override 064 public String getDefaultExtension() { 065 return DEFAULT_EXTENSION; 066 } 067 068 @Override 069 protected String[] getAcceptedExtensions() { 070 return ACCEPTED_EXTENSIONS; 071 } 072 073 @Override 074 protected ImageFormat[] getAcceptedTypes() { 075 return new ImageFormat[] { ImageFormats.XBM, // 076 }; 077 } 078 079 @Override 080 public ImageMetadata getMetadata(final ByteSource byteSource, final XbmImagingParameters params) 081 throws ImageReadException, IOException { 082 return null; 083 } 084 085 @Override 086 public ImageInfo getImageInfo(final ByteSource byteSource, final XbmImagingParameters params) 087 throws ImageReadException, IOException { 088 final XbmHeader xbmHeader = readXbmHeader(byteSource); 089 return new ImageInfo("XBM", 1, new ArrayList<>(), 090 ImageFormats.XBM, "X BitMap", xbmHeader.height, 091 "image/x-xbitmap", 1, 0, 0, 0, 0, xbmHeader.width, false, 092 false, false, ImageInfo.ColorType.BW, 093 ImageInfo.CompressionAlgorithm.NONE); 094 } 095 096 @Override 097 public Dimension getImageSize(final ByteSource byteSource, final XbmImagingParameters params) 098 throws ImageReadException, IOException { 099 final XbmHeader xbmHeader = readXbmHeader(byteSource); 100 return new Dimension(xbmHeader.width, xbmHeader.height); 101 } 102 103 @Override 104 public byte[] getICCProfileBytes(final ByteSource byteSource, final XbmImagingParameters params) 105 throws ImageReadException, IOException { 106 return null; 107 } 108 109 private static class XbmHeader { 110 final int width; 111 final int height; 112 int xHot = -1; 113 int yHot = -1; 114 115 XbmHeader(final int width, final int height, final int xHot, final int yHot) { 116 this.width = width; 117 this.height = height; 118 this.xHot = xHot; 119 this.yHot = yHot; 120 } 121 122 public void dump(final PrintWriter pw) { 123 pw.println("XbmHeader"); 124 pw.println("Width: " + width); 125 pw.println("Height: " + height); 126 if (xHot != -1 && yHot != -1) { 127 pw.println("X hot: " + xHot); 128 pw.println("Y hot: " + yHot); 129 } 130 } 131 } 132 133 private static class XbmParseResult { 134 XbmHeader xbmHeader; 135 BasicCParser cParser; 136 } 137 138 private XbmHeader readXbmHeader(final ByteSource byteSource) 139 throws ImageReadException, IOException { 140 return parseXbmHeader(byteSource).xbmHeader; 141 } 142 143 private XbmParseResult parseXbmHeader(final ByteSource byteSource) 144 throws ImageReadException, IOException { 145 try (InputStream is = byteSource.getInputStream()) { 146 final Map<String, String> defines = new HashMap<>(); 147 final ByteArrayOutputStream preprocessedFile = BasicCParser.preprocess( 148 is, null, defines); 149 int width = -1; 150 int height = -1; 151 int xHot = -1; 152 int yHot = -1; 153 for (final Entry<String, String> entry : defines.entrySet()) { 154 final String name = entry.getKey(); 155 if (name.endsWith("_width")) { 156 width = parseCIntegerLiteral(entry.getValue()); 157 } else if (name.endsWith("_height")) { 158 height = parseCIntegerLiteral(entry.getValue()); 159 } else if (name.endsWith("_x_hot")) { 160 xHot = parseCIntegerLiteral(entry.getValue()); 161 } else if (name.endsWith("_y_hot")) { 162 yHot = parseCIntegerLiteral(entry.getValue()); 163 } 164 } 165 if (width == -1) { 166 throw new ImageReadException("width not found"); 167 } 168 if (height == -1) { 169 throw new ImageReadException("height not found"); 170 } 171 172 final XbmParseResult xbmParseResult = new XbmParseResult(); 173 xbmParseResult.cParser = new BasicCParser(new ByteArrayInputStream( 174 preprocessedFile.toByteArray())); 175 xbmParseResult.xbmHeader = new XbmHeader(width, height, xHot, yHot); 176 return xbmParseResult; 177 } 178 } 179 180 private static int parseCIntegerLiteral(final String value) { 181 if (value.startsWith("0")) { 182 if (value.length() >= 2) { 183 if (value.charAt(1) == 'x' || value.charAt(1) == 'X') { 184 return Integer.parseInt(value.substring(2), 16); 185 } 186 return Integer.parseInt(value.substring(1), 8); 187 } 188 return 0; 189 } 190 return Integer.parseInt(value); 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 XbmImagingParameters 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, XbmImagingParameters params) 346 throws ImageWriteException, IOException { 347 final String name = randomName(); 348 349 os.write(("#define " + name + "_width " + src.getWidth() + "\n").getBytes(StandardCharsets.US_ASCII)); 350 os.write(("#define " + name + "_height " + src.getHeight() + "\n").getBytes(StandardCharsets.US_ASCII)); 351 os.write(("static unsigned char " + name + "_bits[] = {").getBytes(StandardCharsets.US_ASCII)); 352 353 int bitcache = 0; 354 int bitsInCache = 0; 355 String separator = "\n "; 356 int written = 0; 357 for (int y = 0; y < src.getHeight(); y++) { 358 for (int x = 0; x < src.getWidth(); x++) { 359 final int argb = src.getRGB(x, y); 360 final int red = 0xff & (argb >> 16); 361 final int green = 0xff & (argb >> 8); 362 final int blue = 0xff & (argb >> 0); 363 int sample = (red + green + blue) / 3; 364 if (sample > 127) { 365 sample = 0; 366 } else { 367 sample = 1; 368 } 369 bitcache |= (sample << bitsInCache); 370 ++bitsInCache; 371 if (bitsInCache == 8) { 372 os.write(separator.getBytes(StandardCharsets.US_ASCII)); 373 separator = ","; 374 if (written == 12) { 375 os.write("\n ".getBytes(StandardCharsets.US_ASCII)); 376 written = 0; 377 } 378 os.write(toPrettyHex(bitcache).getBytes(StandardCharsets.US_ASCII)); 379 bitcache = 0; 380 bitsInCache = 0; 381 ++written; 382 } 383 } 384 if (bitsInCache != 0) { 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 398 os.write("\n};\n".getBytes(StandardCharsets.US_ASCII)); 399 } 400}