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}