    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    /**
    121     * Constructs a new instance with the big-endian byte order.
    122     */
    123    public XbmImageParser() {
    124        // empty
    125    }
    126
    127    @Override
    128    public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) throws ImagingException, IOException {
    129        readXbmHeader(byteSource).dump(pw);
    130        return true;
    131    }
    132
    133    @Override
    134    protected String[] getAcceptedExtensions() {
    135        return ACCEPTED_EXTENSIONS;
    136    }
    137
    138    @Override
    139    protected ImageFormat[] getAcceptedTypes() {
    140        return new ImageFormat[] { ImageFormats.XBM, //
    141        };
    142    }
    143
    144    @Override
    145    public final BufferedImage getBufferedImage(final ByteSource byteSource, final XbmImagingParameters params) throws ImagingException, IOException {
    146        final XbmParseResult result = parseXbmHeader(byteSource);
    147        return readXbmImage(result.xbmHeader, result.cParser);
    148    }
    149
    150    @Override
    151    public String getDefaultExtension() {
    152        return DEFAULT_EXTENSION;
    153    }
    154
    155    @Override
    156    public XbmImagingParameters getDefaultParameters() {
    157        return new XbmImagingParameters();
    158    }
    159
    160    @Override
    161    public byte[] getIccProfileBytes(final ByteSource byteSource, final XbmImagingParameters params) throws ImagingException, IOException {
    162        return null;
    163    }
    164
    165    @Override
    166    public ImageInfo getImageInfo(final ByteSource byteSource, final XbmImagingParameters params) throws ImagingException, IOException {
    167        final XbmHeader xbmHeader = readXbmHeader(byteSource);
    168        return new ImageInfo("XBM", 1, new ArrayList<>(), ImageFormats.XBM, "X BitMap", xbmHeader.height, "image/x-xbitmap", 1, 0, 0, 0, 0, xbmHeader.width,
    169                false, false, false, ImageInfo.ColorType.BW, ImageInfo.CompressionAlgorithm.NONE);
    170    }
    171
    172    @Override
    173    public Dimension getImageSize(final ByteSource byteSource, final XbmImagingParameters params) throws ImagingException, IOException {
    174        final XbmHeader xbmHeader = readXbmHeader(byteSource);
    175        return new Dimension(xbmHeader.width, xbmHeader.height);
    176    }
    177
    178    @Override
    179    public ImageMetadata getMetadata(final ByteSource byteSource, final XbmImagingParameters params) throws ImagingException, IOException {
    180        return null;
    181    }
    182
    183    @Override
    184    public String getName() {
    185        return "X BitMap";
    186    }
    187
    188    private XbmParseResult parseXbmHeader(final ByteSource byteSource) throws ImagingException, IOException {
    189        try (InputStream is = byteSource.getInputStream()) {
    190            final Map<String, String> defines = new HashMap<>();
    191            final ByteArrayOutputStream preprocessedFile = BasicCParser.preprocess(is, null, defines);
    192            int width = -1;
    193            int height = -1;
    194            int xHot = -1;
    195            int yHot = -1;
    196            for (final Entry<String, String> entry : defines.entrySet()) {
    197                final String name = entry.getKey();
    198                if (name.endsWith("_width")) {
    199                    width = parseCIntegerLiteral(entry.getValue());
    200                } else if (name.endsWith("_height")) {
    201                    height = parseCIntegerLiteral(entry.getValue());
    202                } else if (name.endsWith("_x_hot")) {
    203                    xHot = parseCIntegerLiteral(entry.getValue());
    204                } else if (name.endsWith("_y_hot")) {
    205                    yHot = parseCIntegerLiteral(entry.getValue());
    206                }
    207            }
    208            if (width == -1) {
    209                throw new ImagingException("width not found");
    210            }
    211            if (height == -1) {
    212                throw new ImagingException("height not found");
    213            }
    214
    215            final XbmParseResult xbmParseResult = new XbmParseResult();
    216            xbmParseResult.cParser = new BasicCParser(new ByteArrayInputStream(preprocessedFile.toByteArray()));
    217            xbmParseResult.xbmHeader = new XbmHeader(width, height, xHot, yHot);
    218            return xbmParseResult;
    219        }
    220    }
    221
    222    private XbmHeader readXbmHeader(final ByteSource byteSource) throws ImagingException, IOException {
    223        return parseXbmHeader(byteSource).xbmHeader;
    224    }
    225
    226    private BufferedImage readXbmImage(final XbmHeader xbmHeader, final BasicCParser cParser) throws ImagingException, IOException {
    227        String token;
    228        token = cParser.nextToken();
    229        if (!"static".equals(token)) {
    230            throw new ImagingException("Parsing XBM file failed, no 'static' token");
    231        }
    232        token = cParser.nextToken();
    233        if (token == null) {
    234            throw new ImagingException("Parsing XBM file failed, no 'unsigned' " + "or 'char' or 'short' token");
    235        }
    236        if ("unsigned".equals(token)) {
    237            token = cParser.nextToken();
    238        }
    239        final int inputWidth;
    240        final int hexWidth;
    241        if ("char".equals(token)) {
    242            inputWidth = 8;
    243            hexWidth = 4; // 0xab
    244        } else if ("short".equals(token)) {
    245            inputWidth = 16;
    246            hexWidth = 6; // 0xabcd
    247        } else {
    248            throw new ImagingException("Parsing XBM file failed, no 'char' or 'short' token");
    249        }
    250        final String name = cParser.nextToken();
    251        if (name == null) {
    252            throw new ImagingException("Parsing XBM file failed, no variable name");
    253        }
    254        if (name.charAt(0) != '_' && !Character.isLetter(name.charAt(0))) {
    255            throw new ImagingException("Parsing XBM file failed, variable name " + "doesn't start with letter or underscore");
    256        }
    257        for (int i = 0; i < name.length(); i++) {
    258            final char c = name.charAt(i);
    259            if (!Character.isLetterOrDigit(c) && c != '_') {
    260                throw new ImagingException("Parsing XBM file failed, variable name " + "contains non-letter non-digit non-underscore");
    261            }
    262        }
    263        token = cParser.nextToken();
    264        if (!"[".equals(token)) {
    265            throw new ImagingException("Parsing XBM file failed, no '[' token");
    266        }
    267        token = cParser.nextToken();
    268        if (!"]".equals(token)) {
    269            throw new ImagingException("Parsing XBM file failed, no ']' token");
    270        }
    271        token = cParser.nextToken();
    272        if (!"=".equals(token)) {
    273            throw new ImagingException("Parsing XBM file failed, no '=' token");
    274        }
    275        token = cParser.nextToken();
    276        if (!"{".equals(token)) {
    277            throw new ImagingException("Parsing XBM file failed, no '{' token");
    278        }
    279
    280        final int rowLength = (xbmHeader.width + 7) / 8;
    281        final byte[] imageData = Allocator.byteArray(rowLength * xbmHeader.height);
    282        int i = 0;
    283        for (int y = 0; y < xbmHeader.height; y++) {
    284            for (int x = 0; x < xbmHeader.width; x += inputWidth) {
    285                token = cParser.nextToken();
    286                if (token == null || !token.startsWith("0x")) {
    287                    throw new ImagingException("Parsing XBM file failed, " + "hex value missing");
    288                }
    289                if (token.length() > hexWidth) {
    290                    throw new ImagingException("Parsing XBM file failed, " + "hex value too long");
    291                }
    292                final int value = Integer.parseInt(token.substring(2), 16);
    293                final int flipped = Integer.reverse(value) >>> 32 - inputWidth;
    294                if (inputWidth == 16) {
    295                    imageData[i++] = (byte) (flipped >>> 8);
    296                    if (x + 8 < xbmHeader.width) {
    297                        imageData[i++] = (byte) flipped;
    298                    }
    299                } else {
    300                    imageData[i++] = (byte) flipped;
    301                }
    302
    303                token = cParser.nextToken();
    304                if (token == null) {
    305                    throw new ImagingException("Parsing XBM file failed, " + "premature end of file");
    306                }
    307                if (!",".equals(token) && (i < imageData.length || !"}".equals(token))) {
    308                    throw new ImagingException("Parsing XBM file failed, " + "punctuation error");
    309                }
    310            }
    311        }
    312
    313        final int[] palette = { 0xffffff, 0x000000 };
    314        final ColorModel colorModel = new IndexColorModel(1, 2, palette, 0, false, -1, DataBuffer.TYPE_BYTE);
    315        final DataBufferByte dataBuffer = new DataBufferByte(imageData, imageData.length);
    316        final WritableRaster raster = Raster.createPackedRaster(dataBuffer, xbmHeader.width, xbmHeader.height, 1, null);
    317
    318        return new BufferedImage(colorModel, raster, colorModel.isAlphaPremultiplied(), new Properties());
    319    }
    320
    321    @Override
    322    public void writeImage(final BufferedImage src, final OutputStream os, final XbmImagingParameters params) throws ImagingException, IOException {
    323        final String name = randomName();
    324
    325        os.write(("#define " + name + "_width " + src.getWidth() + "\n").getBytes(StandardCharsets.US_ASCII));
    326        os.write(("#define " + name + "_height " + src.getHeight() + "\n").getBytes(StandardCharsets.US_ASCII));
    327        os.write(("static unsigned char " + name + "_bits[] = {").getBytes(StandardCharsets.US_ASCII));
    328
    329        int bitcache = 0;
    330        int bitsInCache = 0;
    331        String separator = "\n  ";
    332        int written = 0;
    333        for (int y = 0; y < src.getHeight(); y++) {
    334            for (int x = 0; x < src.getWidth(); x++) {
    335                final int argb = src.getRGB(x, y);
    336                final int red = 0xff & argb >> 16;
    337                final int green = 0xff & argb >> 8;
    338                final int blue = 0xff & argb >> 0;
    339                int sample = (red + green + blue) / 3;
    340                if (sample > 127) {
    341                    sample = 0;
    342                } else {
    343                    sample = 1;
    344                }
    345                bitcache |= sample << bitsInCache;
    346                ++bitsInCache;
    347                if (bitsInCache == 8) {
    348                    os.write(separator.getBytes(StandardCharsets.US_ASCII));
    349                    separator = ",";
    350                    if (written == 12) {
    351                        os.write("\n  ".getBytes(StandardCharsets.US_ASCII));
    352                        written = 0;
    353                    }
    354                    os.write(toPrettyHex(bitcache).getBytes(StandardCharsets.US_ASCII));
    355                    bitcache = 0;
    356                    bitsInCache = 0;
    357                    ++written;
    358                }
    359            }
    360            if (bitsInCache != 0) {
    361                os.write(separator.getBytes(StandardCharsets.US_ASCII));
    362                separator = ",";
    363                if (written == 12) {
    364                    os.write("\n  ".getBytes(StandardCharsets.US_ASCII));
    365                    written = 0;
    366                }
    367                os.write(toPrettyHex(bitcache).getBytes(StandardCharsets.US_ASCII));
    368                bitcache = 0;
    369                bitsInCache = 0;
    370                ++written;
    371            }
    372        }
    373
    374        os.write("\n};\n".getBytes(StandardCharsets.US_ASCII));
    375    }
    376}