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 @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; // 0xab 237 } else if ("short".equals(token)) { 238 inputWidth = 16; 239 hexWidth = 6; // 0xabcd 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}