1 /*
   2  * Copyright (c) 2003, 2017, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package com.sun.imageio.plugins.wbmp;
  27 
  28 import java.awt.Point;
  29 import java.awt.Rectangle;
  30 import java.awt.image.ColorModel;
  31 import java.awt.image.DataBuffer;
  32 import java.awt.image.DataBufferByte;
  33 import java.awt.image.IndexColorModel;
  34 import java.awt.image.MultiPixelPackedSampleModel;
  35 import java.awt.image.Raster;
  36 import java.awt.image.RenderedImage;
  37 import java.awt.image.SampleModel;
  38 import java.awt.image.WritableRaster;
  39 
  40 import java.io.IOException;
  41 
  42 import javax.imageio.IIOImage;
  43 import javax.imageio.IIOException;
  44 import javax.imageio.ImageTypeSpecifier;
  45 import javax.imageio.ImageWriteParam;
  46 import javax.imageio.ImageWriter;
  47 import javax.imageio.metadata.IIOMetadata;
  48 import javax.imageio.metadata.IIOMetadataFormatImpl;
  49 import javax.imageio.metadata.IIOInvalidTreeException;
  50 import javax.imageio.spi.ImageWriterSpi;
  51 import javax.imageio.stream.ImageOutputStream;
  52 
  53 import com.sun.imageio.plugins.common.I18N;
  54 
  55 /**
  56  * The Java Image IO plugin writer for encoding a binary RenderedImage into
  57  * a WBMP format.
  58  *
  59  * The encoding process may clip, subsample using the parameters
  60  * specified in the {@code ImageWriteParam}.
  61  */
  62 public class WBMPImageWriter extends ImageWriter {
  63     /** The output stream to write into */
  64     private ImageOutputStream stream = null;
  65 
  66     // Get the number of bits required to represent an int.
  67     private static int getNumBits(int intValue) {
  68         int numBits = 32;
  69         int mask = 0x80000000;
  70         while(mask != 0 && (intValue & mask) == 0) {
  71             numBits--;
  72             mask >>>= 1;
  73         }
  74         return numBits;
  75     }
  76 
  77     // Convert an int value to WBMP multi-byte format.
  78     private static byte[] intToMultiByte(int intValue) {
  79         int numBitsLeft = getNumBits(intValue);
  80         byte[] multiBytes = new byte[(numBitsLeft + 6)/7];
  81 
  82         int maxIndex = multiBytes.length - 1;
  83         for(int b = 0; b <= maxIndex; b++) {
  84             multiBytes[b] = (byte)((intValue >>> ((maxIndex - b)*7))&0x7f);
  85             if(b != maxIndex) {
  86                 multiBytes[b] |= (byte)0x80;
  87             }
  88         }
  89 
  90         return multiBytes;
  91     }
  92 
  93     /** Constructs {@code WBMPImageWriter} based on the provided
  94      *  {@code ImageWriterSpi}.
  95      */
  96     public WBMPImageWriter(ImageWriterSpi originator) {
  97         super(originator);
  98     }
  99 
 100     @Override
 101     public void setOutput(Object output) {
 102         super.setOutput(output); // validates output
 103         if (output != null) {
 104             if (!(output instanceof ImageOutputStream))
 105                 throw new IllegalArgumentException(I18N.getString("WBMPImageWriter"));
 106             this.stream = (ImageOutputStream)output;
 107         } else
 108             this.stream = null;
 109     }
 110 
 111     @Override
 112     public IIOMetadata getDefaultStreamMetadata(ImageWriteParam param) {
 113         return null;
 114     }
 115 
 116     @Override
 117     public IIOMetadata getDefaultImageMetadata(ImageTypeSpecifier imageType,
 118                                                ImageWriteParam param) {
 119         WBMPMetadata meta = new WBMPMetadata();
 120         meta.wbmpType = 0; // default wbmp level
 121         return meta;
 122     }
 123 
 124     @Override
 125     public IIOMetadata convertStreamMetadata(IIOMetadata inData,
 126                                              ImageWriteParam param) {
 127         return null;
 128     }
 129 
 130     @Override
 131     public IIOMetadata convertImageMetadata(IIOMetadata metadata,
 132                                             ImageTypeSpecifier type,
 133                                             ImageWriteParam param) {
 134         return null;
 135     }
 136 
 137     public boolean canWriteRasters() {
 138         return true;
 139     }
 140 
 141     @Override
 142     public void write(IIOMetadata streamMetadata,
 143                       IIOImage image,
 144                       ImageWriteParam param) throws IOException {
 145 
 146         if (stream == null) {
 147             throw new IllegalStateException(I18N.getString("WBMPImageWriter3"));
 148         }
 149 
 150         if (image == null) {
 151             throw new IllegalArgumentException(I18N.getString("WBMPImageWriter4"));
 152         }
 153 
 154         clearAbortRequest();
 155         processImageStarted(0);
 156         if (param == null)
 157             param = getDefaultWriteParam();
 158 
 159         RenderedImage input = null;
 160         Raster inputRaster = null;
 161         boolean writeRaster = image.hasRaster();
 162         Rectangle sourceRegion = param.getSourceRegion();
 163         SampleModel sampleModel = null;
 164 
 165         if (writeRaster) {
 166             inputRaster = image.getRaster();
 167             sampleModel = inputRaster.getSampleModel();
 168         } else {
 169             input = image.getRenderedImage();
 170             sampleModel = input.getSampleModel();
 171 
 172             inputRaster = input.getData();
 173         }
 174 
 175         checkSampleModel(sampleModel);
 176         if (sourceRegion == null)
 177             sourceRegion = inputRaster.getBounds();
 178         else
 179             sourceRegion = sourceRegion.intersection(inputRaster.getBounds());
 180 
 181         if (sourceRegion.isEmpty())
 182             throw new RuntimeException(I18N.getString("WBMPImageWriter1"));
 183 
 184         int scaleX = param.getSourceXSubsampling();
 185         int scaleY = param.getSourceYSubsampling();
 186         int xOffset = param.getSubsamplingXOffset();
 187         int yOffset = param.getSubsamplingYOffset();
 188 
 189         sourceRegion.translate(xOffset, yOffset);
 190         sourceRegion.width -= xOffset;
 191         sourceRegion.height -= yOffset;
 192 
 193         int minX = sourceRegion.x / scaleX;
 194         int minY = sourceRegion.y / scaleY;
 195         int w = (sourceRegion.width + scaleX - 1) / scaleX;
 196         int h = (sourceRegion.height + scaleY - 1) / scaleY;
 197 
 198         Rectangle destinationRegion = new Rectangle(minX, minY, w, h);
 199         sampleModel = sampleModel.createCompatibleSampleModel(w, h);
 200 
 201         SampleModel destSM= sampleModel;
 202 
 203         // If the data are not formatted nominally then reformat.
 204         if(sampleModel.getDataType() != DataBuffer.TYPE_BYTE ||
 205            !(sampleModel instanceof MultiPixelPackedSampleModel) ||
 206            ((MultiPixelPackedSampleModel)sampleModel).getDataBitOffset() != 0) {
 207            destSM =
 208                 new MultiPixelPackedSampleModel(DataBuffer.TYPE_BYTE,
 209                                                 w, h, 1,
 210                                                 w + 7 >> 3, 0);
 211         }
 212 
 213         if (!destinationRegion.equals(sourceRegion)) {
 214             if (scaleX == 1 && scaleY == 1)
 215                 inputRaster = inputRaster.createChild(inputRaster.getMinX(),
 216                                                       inputRaster.getMinY(),
 217                                                       w, h, minX, minY, null);
 218             else {
 219                 WritableRaster ras = Raster.createWritableRaster(destSM,
 220                                                                  new Point(minX, minY));
 221 
 222                 byte[] data = ((DataBufferByte)ras.getDataBuffer()).getData();
 223 
 224                 for(int j = minY, y = sourceRegion.y, k = 0;
 225                     j < minY + h; j++, y += scaleY) {
 226 
 227                     for (int i = 0, x = sourceRegion.x;
 228                         i <w; i++, x +=scaleX) {
 229                         int v = inputRaster.getSample(x, y, 0);
 230                         data[k + (i >> 3)] |= v << (7 - (i & 7));
 231                     }
 232                     k += w + 7 >> 3;
 233                 }
 234                 inputRaster = ras;
 235             }
 236         }
 237 
 238         // If the data are not formatted nominally then reformat.
 239         if(!destSM.equals(inputRaster.getSampleModel())) {
 240             WritableRaster raster =
 241                 Raster.createWritableRaster(destSM,
 242                                             new Point(inputRaster.getMinX(),
 243                                                       inputRaster.getMinY()));
 244             raster.setRect(inputRaster);
 245             inputRaster = raster;
 246         }
 247 
 248         // Check whether the image is white-is-zero.
 249         boolean isWhiteZero = false;
 250         if(!writeRaster && input.getColorModel() instanceof IndexColorModel) {
 251             IndexColorModel icm = (IndexColorModel)input.getColorModel();
 252             isWhiteZero = icm.getRed(0) > icm.getRed(1);
 253         }
 254 
 255         // Get the line stride, bytes per row, and data array.
 256         int lineStride =
 257             ((MultiPixelPackedSampleModel)destSM).getScanlineStride();
 258         int bytesPerRow = (w + 7)/8;
 259         byte[] bdata = ((DataBufferByte)inputRaster.getDataBuffer()).getData();
 260 
 261         // Write WBMP header.
 262         stream.write(0); // TypeField
 263         stream.write(0); // FixHeaderField
 264         stream.write(intToMultiByte(w)); // width
 265         stream.write(intToMultiByte(h)); // height
 266 
 267         // Write the data.
 268         if(!isWhiteZero && lineStride == bytesPerRow) {
 269             // Write the entire image.
 270             stream.write(bdata, 0, h * bytesPerRow);
 271             processImageProgress(100.0F);
 272         } else {
 273             // Write the image row-by-row.
 274             int offset = 0;
 275             if(!isWhiteZero) {
 276                 // Black-is-zero
 277                 for(int row = 0; row < h; row++) {
 278                     if (abortRequested())
 279                         break;
 280                     stream.write(bdata, offset, bytesPerRow);
 281                     offset += lineStride;
 282                     processImageProgress(100.0F * row / h);
 283                 }
 284             } else {
 285                 // White-is-zero: need to invert data.
 286                 byte[] inverted = new byte[bytesPerRow];
 287                 for(int row = 0; row < h; row++) {
 288                     if (abortRequested())
 289                         break;
 290                     for(int col = 0; col < bytesPerRow; col++) {
 291                         inverted[col] = (byte)(~(bdata[col+offset]));
 292                     }
 293                     stream.write(inverted, 0, bytesPerRow);
 294                     offset += lineStride;
 295                     processImageProgress(100.0F * row / h);
 296                 }
 297             }
 298         }
 299 
 300         if (abortRequested())
 301             processWriteAborted();
 302         else {
 303             processImageComplete();
 304             stream.flushBefore(stream.getStreamPosition());
 305         }
 306     }
 307 
 308     @Override
 309     public void reset() {
 310         super.reset();
 311         stream = null;
 312     }
 313 
 314     private void checkSampleModel(SampleModel sm) {
 315         int type = sm.getDataType();
 316         if (type < DataBuffer.TYPE_BYTE || type > DataBuffer.TYPE_INT
 317             || sm.getNumBands() != 1 || sm.getSampleSize(0) != 1)
 318             throw new IllegalArgumentException(I18N.getString("WBMPImageWriter2"));
 319     }
 320 }