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}