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