    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.ImageFormat;
    040import org.apache.commons.imaging.ImageFormats;
    041import org.apache.commons.imaging.ImageInfo;
    042import org.apache.commons.imaging.ImageParser;
    043import org.apache.commons.imaging.ImageReadException;
    044import org.apache.commons.imaging.ImageWriteException;
    045import org.apache.commons.imaging.common.BasicCParser;
    046import org.apache.commons.imaging.common.ImageMetadata;
    047import org.apache.commons.imaging.common.bytesource.ByteSource;
    048
    049public class XbmImageParser extends ImageParser<XbmImagingParameters> {
    050    private static final String DEFAULT_EXTENSION = ImageFormats.XBM.getDefaultExtension();
    051    private static final String[] ACCEPTED_EXTENSIONS = ImageFormats.XBM.getExtensions();
    052
    053    @Override
    054    public XbmImagingParameters getDefaultParameters() {
    055        return new XbmImagingParameters();
    056    }
    057
    058    @Override
    059    public String getName() {
    060        return "X BitMap";
    061    }
    062
    063    @Override
    064    public String getDefaultExtension() {
    065        return DEFAULT_EXTENSION;
    066    }
    067
    068    @Override
    069    protected String[] getAcceptedExtensions() {
    070        return ACCEPTED_EXTENSIONS;
    071    }
    072
    073    @Override
    074    protected ImageFormat[] getAcceptedTypes() {
    075        return new ImageFormat[] { ImageFormats.XBM, //
    076        };
    077    }
    078
    079    @Override
    080    public ImageMetadata getMetadata(final ByteSource byteSource, final XbmImagingParameters params)
    081            throws ImageReadException, IOException {
    082        return null;
    083    }
    084
    085    @Override
    086    public ImageInfo getImageInfo(final ByteSource byteSource, final XbmImagingParameters params)
    087            throws ImageReadException, IOException {
    088        final XbmHeader xbmHeader = readXbmHeader(byteSource);
    089        return new ImageInfo("XBM", 1, new ArrayList<>(),
    090                ImageFormats.XBM, "X BitMap", xbmHeader.height,
    091                "image/x-xbitmap", 1, 0, 0, 0, 0, xbmHeader.width, false,
    092                false, false, ImageInfo.ColorType.BW,
    093                ImageInfo.CompressionAlgorithm.NONE);
    094    }
    095
    096    @Override
    097    public Dimension getImageSize(final ByteSource byteSource, final XbmImagingParameters params)
    098            throws ImageReadException, IOException {
    099        final XbmHeader xbmHeader = readXbmHeader(byteSource);
    100        return new Dimension(xbmHeader.width, xbmHeader.height);
    101    }
    102
    103    @Override
    104    public byte[] getICCProfileBytes(final ByteSource byteSource, final XbmImagingParameters params)
    105            throws ImageReadException, IOException {
    106        return null;
    107    }
    108
    109    private static class XbmHeader {
    110        final int width;
    111        final int height;
    112        int xHot = -1;
    113        int yHot = -1;
    114
    115        XbmHeader(final int width, final int height, final int xHot, final int yHot) {
    116            this.width = width;
    117            this.height = height;
    118            this.xHot = xHot;
    119            this.yHot = yHot;
    120        }
    121
    122        public void dump(final PrintWriter pw) {
    123            pw.println("XbmHeader");
    124            pw.println("Width: " + width);
    125            pw.println("Height: " + height);
    126            if (xHot != -1 && yHot != -1) {
    127                pw.println("X hot: " + xHot);
    128                pw.println("Y hot: " + yHot);
    129            }
    130        }
    131    }
    132
    133    private static class XbmParseResult {
    134        XbmHeader xbmHeader;
    135        BasicCParser cParser;
    136    }
    137
    138    private XbmHeader readXbmHeader(final ByteSource byteSource)
    139            throws ImageReadException, IOException {
    140        return parseXbmHeader(byteSource).xbmHeader;
    141    }
    142
    143    private XbmParseResult parseXbmHeader(final ByteSource byteSource)
    144            throws ImageReadException, IOException {
    145        try (InputStream is = byteSource.getInputStream()) {
    146            final Map<String, String> defines = new HashMap<>();
    147            final ByteArrayOutputStream preprocessedFile = BasicCParser.preprocess(
    148                    is, null, defines);
    149            int width = -1;
    150            int height = -1;
    151            int xHot = -1;
    152            int yHot = -1;
    153            for (final Entry<String, String> entry : defines.entrySet()) {
    154                final String name = entry.getKey();
    155                if (name.endsWith("_width")) {
    156                    width = parseCIntegerLiteral(entry.getValue());
    157                } else if (name.endsWith("_height")) {
    158                    height = parseCIntegerLiteral(entry.getValue());
    159                } else if (name.endsWith("_x_hot")) {
    160                    xHot = parseCIntegerLiteral(entry.getValue());
    161                } else if (name.endsWith("_y_hot")) {
    162                    yHot = parseCIntegerLiteral(entry.getValue());
    163                }
    164             }
    165            if (width == -1) {
    166                throw new ImageReadException("width not found");
    167            }
    168            if (height == -1) {
    169                throw new ImageReadException("height not found");
    170            }
    171
    172            final XbmParseResult xbmParseResult = new XbmParseResult();
    173            xbmParseResult.cParser = new BasicCParser(new ByteArrayInputStream(
    174                    preprocessedFile.toByteArray()));
    175            xbmParseResult.xbmHeader = new XbmHeader(width, height, xHot, yHot);
    176            return xbmParseResult;
    177        }
    178    }
    179
    180    private static int parseCIntegerLiteral(final String value) {
    181        if (value.startsWith("0")) {
    182            if (value.length() >= 2) {
    183                if (value.charAt(1) == 'x' || value.charAt(1) == 'X') {
    184                    return Integer.parseInt(value.substring(2), 16);
    185                }
    186                return Integer.parseInt(value.substring(1), 8);
    187            }
    188            return 0;
    189        }
    190        return Integer.parseInt(value);
    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 XbmImagingParameters 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, XbmImagingParameters params)
    346            throws ImageWriteException, IOException {
    347        final String name = randomName();
    348
    349        os.write(("#define " + name + "_width " + src.getWidth() + "\n").getBytes(StandardCharsets.US_ASCII));
    350        os.write(("#define " + name + "_height " + src.getHeight() + "\n").getBytes(StandardCharsets.US_ASCII));
    351        os.write(("static unsigned char " + name + "_bits[] = {").getBytes(StandardCharsets.US_ASCII));
    352
    353        int bitcache = 0;
    354        int bitsInCache = 0;
    355        String separator = "\n  ";
    356        int written = 0;
    357        for (int y = 0; y < src.getHeight(); y++) {
    358            for (int x = 0; x < src.getWidth(); x++) {
    359                final int argb = src.getRGB(x, y);
    360                final int red = 0xff & (argb >> 16);
    361                final int green = 0xff & (argb >> 8);
    362                final int blue = 0xff & (argb >> 0);
    363                int sample = (red + green + blue) / 3;
    364                if (sample > 127) {
    365                    sample = 0;
    366                } else {
    367                    sample = 1;
    368                }
    369                bitcache |= (sample << bitsInCache);
    370                ++bitsInCache;
    371                if (bitsInCache == 8) {
    372                    os.write(separator.getBytes(StandardCharsets.US_ASCII));
    373                    separator = ",";
    374                    if (written == 12) {
    375                        os.write("\n  ".getBytes(StandardCharsets.US_ASCII));
    376                        written = 0;
    377                    }
    378                    os.write(toPrettyHex(bitcache).getBytes(StandardCharsets.US_ASCII));
    379                    bitcache = 0;
    380                    bitsInCache = 0;
    381                    ++written;
    382                }
    383            }
    384            if (bitsInCache != 0) {
    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
    398        os.write("\n};\n".getBytes(StandardCharsets.US_ASCII));
    399    }
    400}