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