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}