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}