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}