    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}