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}