    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.wbmp;
    016
    017import static org.apache.commons.imaging.ImagingConstants.PARAM_KEY_FORMAT;
    018import static org.apache.commons.imaging.common.BinaryFunctions.readByte;
    019import static org.apache.commons.imaging.common.BinaryFunctions.readBytes;
    020
    021import java.awt.Dimension;
    022import java.awt.image.BufferedImage;
    023import java.awt.image.DataBuffer;
    024import java.awt.image.DataBufferByte;
    025import java.awt.image.IndexColorModel;
    026import java.awt.image.Raster;
    027import java.awt.image.WritableRaster;
    028import java.io.IOException;
    029import java.io.InputStream;
    030import java.io.OutputStream;
    031import java.io.PrintWriter;
    032import java.util.ArrayList;
    033import java.util.HashMap;
    034import java.util.Map;
    035import java.util.Properties;
    036
    037import org.apache.commons.imaging.ImageFormat;
    038import org.apache.commons.imaging.ImageFormats;
    039import org.apache.commons.imaging.ImageInfo;
    040import org.apache.commons.imaging.ImageParser;
    041import org.apache.commons.imaging.ImageReadException;
    042import org.apache.commons.imaging.ImageWriteException;
    043import org.apache.commons.imaging.common.ImageMetadata;
    044import org.apache.commons.imaging.common.bytesource.ByteSource;
    045
    046public class WbmpImageParser extends ImageParser {
    047    private static final String DEFAULT_EXTENSION = ".wbmp";
    048    private static final String[] ACCEPTED_EXTENSIONS = { ".wbmp", };
    049
    050    @Override
    051    public String getName() {
    052        return "Wireless Application Protocol Bitmap Format";
    053    }
    054
    055    @Override
    056    public String getDefaultExtension() {
    057        return DEFAULT_EXTENSION;
    058    }
    059
    060    @Override
    061    protected String[] getAcceptedExtensions() {
    062        return ACCEPTED_EXTENSIONS;
    063    }
    064
    065    @Override
    066    protected ImageFormat[] getAcceptedTypes() {
    067        return new ImageFormat[] { ImageFormats.WBMP, //
    068        };
    069    }
    070
    071    @Override
    072    public ImageMetadata getMetadata(final ByteSource byteSource, final Map<String, Object> params)
    073            throws ImageReadException, IOException {
    074        return null;
    075    }
    076
    077    @Override
    078    public ImageInfo getImageInfo(final ByteSource byteSource, final Map<String, Object> params)
    079            throws ImageReadException, IOException {
    080        final WbmpHeader wbmpHeader = readWbmpHeader(byteSource);
    081        return new ImageInfo("WBMP", 1, new ArrayList<String>(),
    082                ImageFormats.WBMP,
    083                "Wireless Application Protocol Bitmap", wbmpHeader.height,
    084                "image/vnd.wap.wbmp", 1, 0, 0, 0, 0, wbmpHeader.width, false,
    085                false, false, ImageInfo.ColorType.BW,
    086                ImageInfo.CompressionAlgorithm.NONE);
    087    }
    088
    089    @Override
    090    public Dimension getImageSize(final ByteSource byteSource, final Map<String, Object> params)
    091            throws ImageReadException, IOException {
    092        final WbmpHeader wbmpHeader = readWbmpHeader(byteSource);
    093        return new Dimension(wbmpHeader.width, wbmpHeader.height);
    094    }
    095
    096    @Override
    097    public byte[] getICCProfileBytes(final ByteSource byteSource, final Map<String, Object> params)
    098            throws ImageReadException, IOException {
    099        return null;
    100    }
    101
    102    static class WbmpHeader {
    103        int typeField;
    104        byte fixHeaderField;
    105        int width;
    106        int height;
    107
    108        WbmpHeader(final int typeField, final byte fixHeaderField, final int width,
    109                final int height) {
    110            this.typeField = typeField;
    111            this.fixHeaderField = fixHeaderField;
    112            this.width = width;
    113            this.height = height;
    114        }
    115
    116        public void dump(final PrintWriter pw) {
    117            pw.println("WbmpHeader");
    118            pw.println("TypeField: " + typeField);
    119            pw.println("FixHeaderField: 0x"
    120                    + Integer.toHexString(0xff & fixHeaderField));
    121            pw.println("Width: " + width);
    122            pw.println("Height: " + height);
    123        }
    124    }
    125
    126    private int readMultiByteInteger(final InputStream is) throws ImageReadException,
    127            IOException {
    128        int value = 0;
    129        int nextByte;
    130        int totalBits = 0;
    131        do {
    132            nextByte = readByte("Header", is, "Error reading WBMP header");
    133            value <<= 7;
    134            value |= nextByte & 0x7f;
    135            totalBits += 7;
    136            if (totalBits > 31) {
    137                throw new ImageReadException(
    138                        "Overflow reading WBMP multi-byte field");
    139            }
    140        } while ((nextByte & 0x80) != 0);
    141        return value;
    142    }
    143
    144    private void writeMultiByteInteger(final OutputStream os, final int value)
    145            throws IOException {
    146        boolean wroteYet = false;
    147        for (int position = 4 * 7; position > 0; position -= 7) {
    148            final int next7Bits = 0x7f & (value >>> position);
    149            if (next7Bits != 0 || wroteYet) {
    150                os.write(0x80 | next7Bits);
    151                wroteYet = true;
    152            }
    153        }
    154        os.write(0x7f & value);
    155    }
    156
    157    private WbmpHeader readWbmpHeader(final ByteSource byteSource)
    158            throws ImageReadException, IOException {
    159        try (InputStream is = byteSource.getInputStream()) {
    160            return readWbmpHeader(is);
    161        }
    162    }
    163
    164    private WbmpHeader readWbmpHeader(final InputStream is)
    165            throws ImageReadException, IOException {
    166        final int typeField = readMultiByteInteger(is);
    167        if (typeField != 0) {
    168            throw new ImageReadException("Invalid/unsupported WBMP type "
    169                    + typeField);
    170        }
    171
    172        final byte fixHeaderField = readByte("FixHeaderField", is,
    173                "Invalid WBMP File");
    174        if ((fixHeaderField & 0x9f) != 0) {
    175            throw new ImageReadException(
    176                    "Invalid/unsupported WBMP FixHeaderField 0x"
    177                            + Integer.toHexString(0xff & fixHeaderField));
    178        }
    179
    180        final int width = readMultiByteInteger(is);
    181
    182        final int height = readMultiByteInteger(is);
    183
    184        return new WbmpHeader(typeField, fixHeaderField, width, height);
    185    }
    186
    187    @Override
    188    public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource)
    189            throws ImageReadException, IOException {
    190        readWbmpHeader(byteSource).dump(pw);
    191        return true;
    192    }
    193
    194    private BufferedImage readImage(final WbmpHeader wbmpHeader, final InputStream is)
    195            throws IOException {
    196        final int rowLength = (wbmpHeader.width + 7) / 8;
    197        final byte[] image = readBytes("Pixels", is,
    198                rowLength * wbmpHeader.height, "Error reading image pixels");
    199        final DataBufferByte dataBuffer = new DataBufferByte(image, image.length);
    200        final WritableRaster raster = Raster.createPackedRaster(dataBuffer,
    201                wbmpHeader.width, wbmpHeader.height, 1, null);
    202        final int[] palette = { 0x000000, 0xffffff };
    203        final IndexColorModel colorModel = new IndexColorModel(1, 2, palette, 0,
    204                false, -1, DataBuffer.TYPE_BYTE);
    205        return new BufferedImage(colorModel, raster,
    206                colorModel.isAlphaPremultiplied(), new Properties());
    207    }
    208
    209    @Override
    210    public final BufferedImage getBufferedImage(final ByteSource byteSource,
    211            final Map<String, Object> params) throws ImageReadException, IOException {
    212        try (InputStream is = byteSource.getInputStream()) {
    213            final WbmpHeader wbmpHeader = readWbmpHeader(is);
    214            final BufferedImage ret = readImage(wbmpHeader, is);
    215            return ret;
    216        }
    217    }
    218
    219    @Override
    220    public void writeImage(final BufferedImage src, final OutputStream os, Map<String, Object> params)
    221            throws ImageWriteException, IOException {
    222        // make copy of params; we'll clear keys as we consume them.
    223        params = (params == null) ? new HashMap<>() : new HashMap<>(params);
    224
    225        // clear format key.
    226        if (params.containsKey(PARAM_KEY_FORMAT)) {
    227            params.remove(PARAM_KEY_FORMAT);
    228        }
    229
    230        if (!params.isEmpty()) {
    231            final Object firstKey = params.keySet().iterator().next();
    232            throw new ImageWriteException("Unknown parameter: " + firstKey);
    233        }
    234
    235        writeMultiByteInteger(os, 0); // typeField
    236        os.write(0); // fixHeaderField
    237        writeMultiByteInteger(os, src.getWidth());
    238        writeMultiByteInteger(os, src.getHeight());
    239
    240        for (int y = 0; y < src.getHeight(); y++) {
    241            int pixel = 0;
    242            int nextBit = 0x80;
    243            for (int x = 0; x < src.getWidth(); x++) {
    244                final int argb = src.getRGB(x, y);
    245                final int red = 0xff & (argb >> 16);
    246                final int green = 0xff & (argb >> 8);
    247                final int blue = 0xff & (argb >> 0);
    248                final int sample = (red + green + blue) / 3;
    249                if (sample > 127) {
    250                    pixel |= nextBit;
    251                }
    252                nextBit >>>= 1;
    253                if (nextBit == 0) {
    254                    os.write(pixel);
    255                    pixel = 0;
    256                    nextBit = 0x80;
    257                }
    258            }
    259            if (nextBit != 0x80) {
    260                os.write(pixel);
    261            }
    262        }
    263    }
    264}