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.AbstractImageParser; 040import org.apache.commons.imaging.ImageFormat; 041import org.apache.commons.imaging.ImageFormats; 042import org.apache.commons.imaging.ImageInfo; 043import org.apache.commons.imaging.ImagingException; 044import org.apache.commons.imaging.bytesource.ByteSource; 045import org.apache.commons.imaging.common.Allocator; 046import org.apache.commons.imaging.common.BasicCParser; 047import org.apache.commons.imaging.common.ImageMetadata; 048 049public class XbmImageParser extends AbstractImageParser<XbmImagingParameters> { 050 051 private static final class XbmHeader { 052 final int height; 053 final int width; 054 int xHot = -1; 055 int yHot = -1; 056 057 XbmHeader(final int width, final int height, final int xHot, final int yHot) { 058 this.width = width; 059 this.height = height; 060 this.xHot = xHot; 061 this.yHot = yHot; 062 } 063 064 public void dump(final PrintWriter pw) { 065 pw.println("XbmHeader"); 066 pw.println("Width: " + width); 067 pw.println("Height: " + height); 068 if (xHot != -1 && yHot != -1) { 069 pw.println("X hot: " + xHot); 070 pw.println("Y hot: " + yHot); 071 } 072 } 073 } 074 075 private static final class XbmParseResult { 076 BasicCParser cParser; 077 XbmHeader xbmHeader; 078 } 079 080 private static final String[] ACCEPTED_EXTENSIONS = ImageFormats.XBM.getExtensions(); 081 082 private static final String DEFAULT_EXTENSION = ImageFormats.XBM.getDefaultExtension(); 083 084 private static int parseCIntegerLiteral(final String value) { 085 if (value.startsWith("0")) { 086 if (value.length() >= 2) { 087 if (value.charAt(1) == 'x' || value.charAt(1) == 'X') { 088 return Integer.parseInt(value.substring(2), 16); 089 } 090 return Integer.parseInt(value.substring(1), 8); 091 } 092 return 0; 093 } 094 return Integer.parseInt(value); 095 } 096 097 private static String randomName() { 098 final UUID uuid = UUID.randomUUID(); 099 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}