KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > com > sun > imageio > plugins > bmp > BMPImageWriter


1 /*
2  * @(#)BMPImageWriter.java 1.8 03/09/22 13:03:28
3  *
4  * Copyright 2004 Sun Microsystems, Inc. All rights reserved.
5  * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
6  */

7
8 package com.sun.imageio.plugins.bmp;
9
10 import java.awt.Point JavaDoc;
11 import java.awt.Rectangle JavaDoc;
12 import java.awt.image.ColorModel JavaDoc;
13 import java.awt.image.ComponentSampleModel JavaDoc;
14 import java.awt.image.DataBuffer JavaDoc;
15 import java.awt.image.DataBufferByte JavaDoc;
16 import java.awt.image.DataBufferInt JavaDoc;
17 import java.awt.image.DataBufferShort JavaDoc;
18 import java.awt.image.DataBufferUShort JavaDoc;
19 import java.awt.image.IndexColorModel JavaDoc;
20 import java.awt.image.MultiPixelPackedSampleModel JavaDoc;
21 import java.awt.image.BandedSampleModel JavaDoc;
22 import java.awt.image.Raster JavaDoc;
23 import java.awt.image.RenderedImage JavaDoc;
24 import java.awt.image.SampleModel JavaDoc;
25 import java.awt.image.SinglePixelPackedSampleModel JavaDoc;
26 import java.awt.image.WritableRaster JavaDoc;
27 import java.awt.image.BufferedImage JavaDoc;
28
29 import java.io.IOException JavaDoc;
30 import java.io.ByteArrayOutputStream JavaDoc;
31 import java.nio.ByteOrder JavaDoc;
32 import java.util.Iterator JavaDoc;
33
34 import javax.imageio.IIOImage JavaDoc;
35 import javax.imageio.IIOException JavaDoc;
36 import javax.imageio.ImageIO JavaDoc;
37 import javax.imageio.ImageTypeSpecifier JavaDoc;
38 import javax.imageio.ImageWriteParam JavaDoc;
39 import javax.imageio.ImageWriter JavaDoc;
40 import javax.imageio.metadata.IIOMetadata JavaDoc;
41 import javax.imageio.metadata.IIOMetadataNode JavaDoc;
42 import javax.imageio.metadata.IIOMetadataFormatImpl JavaDoc;
43 import javax.imageio.metadata.IIOInvalidTreeException JavaDoc;
44 import javax.imageio.spi.ImageWriterSpi JavaDoc;
45 import javax.imageio.stream.ImageOutputStream JavaDoc;
46 import javax.imageio.event.IIOWriteProgressListener JavaDoc;
47 import javax.imageio.event.IIOWriteWarningListener JavaDoc;
48
49 import org.w3c.dom.Node JavaDoc;
50 import org.w3c.dom.NodeList JavaDoc;
51
52 import javax.imageio.plugins.bmp.BMPImageWriteParam JavaDoc;
53 import com.sun.imageio.plugins.common.ImageUtil;
54 import com.sun.imageio.plugins.common.I18N;
55
56 /**
57  * The Java Image IO plugin writer for encoding a binary RenderedImage into
58  * a BMP format.
59  *
60  * The encoding process may clip, subsample using the parameters
61  * specified in the <code>ImageWriteParam</code>.
62  *
63  * @see javax.imageio.plugins.bmp.BMPImageWriteParam
64  */

65 public class BMPImageWriter extends ImageWriter JavaDoc implements BMPConstants {
66     /** The output stream to write into */
67     private ImageOutputStream JavaDoc stream = null;
68     private ByteArrayOutputStream JavaDoc embedded_stream = null;
69     private int version;
70     private int compressionType;
71     private boolean isTopDown;
72     private int w, h;
73     private int compImageSize = 0;
74     private int[] bitPos;
75     private byte[] bpixels;
76     private short[] spixels;
77     private int[] ipixels;
78
79     /** Constructs <code>BMPImageWriter</code> based on the provided
80      * <code>ImageWriterSpi</code>.
81      */

82     public BMPImageWriter(ImageWriterSpi JavaDoc originator) {
83         super(originator);
84     }
85
86     public void setOutput(Object JavaDoc output) {
87         super.setOutput(output); // validates output
88
if (output != null) {
89             if (!(output instanceof ImageOutputStream JavaDoc))
90                 throw new IllegalArgumentException JavaDoc(I18N.getString("BMPImageWriter0"));
91             this.stream = (ImageOutputStream JavaDoc)output;
92             stream.setByteOrder(ByteOrder.LITTLE_ENDIAN);
93         } else
94             this.stream = null;
95     }
96
97     public ImageWriteParam JavaDoc getDefaultWriteParam() {
98         return new BMPImageWriteParam JavaDoc();
99     }
100
101     public IIOMetadata JavaDoc getDefaultStreamMetadata(ImageWriteParam JavaDoc param) {
102         return null;
103     }
104
105     public IIOMetadata JavaDoc getDefaultImageMetadata(ImageTypeSpecifier JavaDoc imageType,
106                                                ImageWriteParam JavaDoc param) {
107         BMPMetadata meta = new BMPMetadata();
108         meta.bmpVersion = VERSION_3;
109         meta.compression = getPreferredCompressionType(imageType);
110         if (param != null
111             && param.getCompressionMode() == ImageWriteParam.MODE_EXPLICIT) {
112             meta.compression = getCompressionType(param.getCompressionType());
113         }
114         meta.bitsPerPixel = (short)imageType.getColorModel().getPixelSize();
115         return meta;
116     }
117
118     public IIOMetadata JavaDoc convertStreamMetadata(IIOMetadata JavaDoc inData,
119                                              ImageWriteParam JavaDoc param) {
120         return null;
121     }
122
123     public IIOMetadata JavaDoc convertImageMetadata(IIOMetadata JavaDoc metadata,
124                                             ImageTypeSpecifier JavaDoc type,
125                                             ImageWriteParam JavaDoc param) {
126         return null;
127     }
128
129     public boolean canWriteRasters() {
130         return true;
131     }
132
133     public void write(IIOMetadata JavaDoc streamMetadata,
134                       IIOImage JavaDoc image,
135                       ImageWriteParam JavaDoc param) throws IOException JavaDoc {
136
137         if (stream == null) {
138             throw new IllegalStateException JavaDoc(I18N.getString("BMPImageWriter7"));
139         }
140
141         if (image == null) {
142             throw new IllegalArgumentException JavaDoc(I18N.getString("BMPImageWriter8"));
143         }
144
145         clearAbortRequest();
146         processImageStarted(0);
147         if (param == null)
148             param = getDefaultWriteParam();
149
150         BMPImageWriteParam JavaDoc bmpParam = (BMPImageWriteParam JavaDoc)param;
151
152         // Default is using 24 bits per pixel.
153
int bitsPerPixel = 24;
154         boolean isPalette = false;
155         int paletteEntries = 0;
156         IndexColorModel JavaDoc icm = null;
157
158         RenderedImage JavaDoc input = null;
159         Raster JavaDoc inputRaster = null;
160         boolean writeRaster = image.hasRaster();
161         Rectangle JavaDoc sourceRegion = param.getSourceRegion();
162         SampleModel JavaDoc sampleModel = null;
163         ColorModel JavaDoc colorModel = null;
164
165     compImageSize = 0;
166
167         if (writeRaster) {
168             inputRaster = image.getRaster();
169             sampleModel = inputRaster.getSampleModel();
170             colorModel = ImageUtil.createColorModel(null, sampleModel);
171             if (sourceRegion == null)
172                 sourceRegion = inputRaster.getBounds();
173             else
174                 sourceRegion = sourceRegion.intersection(inputRaster.getBounds());
175         } else {
176             input = image.getRenderedImage();
177             sampleModel = input.getSampleModel();
178             colorModel = input.getColorModel();
179             Rectangle JavaDoc rect = new Rectangle JavaDoc(input.getMinX(), input.getMinY(),
180                                            input.getWidth(), input.getHeight());
181             if (sourceRegion == null)
182                 sourceRegion = rect;
183             else
184                 sourceRegion = sourceRegion.intersection(rect);
185         }
186
187         IIOMetadata JavaDoc imageMetadata = image.getMetadata();
188         BMPMetadata bmpImageMetadata = null;
189         if (imageMetadata != null
190             && imageMetadata instanceof BMPMetadata)
191         {
192             bmpImageMetadata = (BMPMetadata)imageMetadata;
193         } else {
194             ImageTypeSpecifier JavaDoc imageType =
195                 new ImageTypeSpecifier JavaDoc(colorModel, sampleModel);
196             
197             bmpImageMetadata = (BMPMetadata)getDefaultImageMetadata(imageType,
198                                                                     param);
199         }
200
201         if (sourceRegion.isEmpty())
202             throw new RuntimeException JavaDoc(I18N.getString("BMPImageWrite0"));
203
204         int scaleX = param.getSourceXSubsampling();
205         int scaleY = param.getSourceYSubsampling();
206         int xOffset = param.getSubsamplingXOffset();
207         int yOffset = param.getSubsamplingYOffset();
208
209         // cache the data type;
210
int dataType = sampleModel.getDataType();
211
212         sourceRegion.translate(xOffset, yOffset);
213         sourceRegion.width -= xOffset;
214         sourceRegion.height -= yOffset;
215
216         int minX = sourceRegion.x / scaleX;
217         int minY = sourceRegion.y / scaleY;
218         w = (sourceRegion.width + scaleX - 1) / scaleX;
219         h = (sourceRegion.height + scaleY - 1) / scaleY;
220         xOffset = sourceRegion.x % scaleX;
221         yOffset = sourceRegion.y % scaleY;
222
223         Rectangle JavaDoc destinationRegion = new Rectangle JavaDoc(minX, minY, w, h);
224         boolean noTransform = destinationRegion.equals(sourceRegion);
225
226         // Raw data can only handle bytes, everything greater must be ASCII.
227
int[] sourceBands = param.getSourceBands();
228         boolean noSubband = true;
229         int numBands = sampleModel.getNumBands();
230
231         if (sourceBands != null) {
232             sampleModel = sampleModel.createSubsetSampleModel(sourceBands);
233             colorModel = null;
234             noSubband = false;
235             numBands = sampleModel.getNumBands();
236         } else {
237             sourceBands = new int[numBands];
238             for (int i = 0; i < numBands; i++)
239                 sourceBands[i] = i;
240         }
241
242         int[] bandOffsets = null;
243         boolean bgrOrder = true;
244
245         if (sampleModel instanceof ComponentSampleModel JavaDoc) {
246             bandOffsets = ((ComponentSampleModel JavaDoc)sampleModel).getBandOffsets();
247             if (sampleModel instanceof BandedSampleModel JavaDoc) {
248                 // for images with BandedSampleModel we can not work
249
// with raster directly and must use writePixels()
250
bgrOrder = false;
251             } else {
252                 // we can work with raster directly only in case of
253
// RGB component order.
254
// In any other case we must use writePixels()
255
for (int i = 0; i < bandOffsets.length; i++)
256                     bgrOrder &= bandOffsets[i] == bandOffsets.length - i -1;
257             }
258         } else {
259             bandOffsets = new int[numBands];
260             for (int i = 0; i < numBands; i++)
261                 bandOffsets[i] = i;
262         }
263         
264         // BugId 4892214: we can not work with raster directly
265
// if image have different color order than RGB.
266
// We should use writePixels() for such images.
267
if (bgrOrder
268             && sampleModel instanceof SinglePixelPackedSampleModel JavaDoc) {
269             int[] bitOffsets = ((SinglePixelPackedSampleModel JavaDoc)sampleModel).getBitOffsets();
270             for (int i=0; i<bitOffsets.length-1; i++) {
271                 bgrOrder &= bitOffsets[i] > bitOffsets[i+1];
272             }
273         }
274
275         noTransform &= bgrOrder;
276
277         int sampleSize[] = sampleModel.getSampleSize();
278
279         //XXX: check more
280

281         // Number of bytes that a scanline for the image written out will have.
282
int destScanlineBytes = w * numBands;
283
284         switch(bmpParam.getCompressionMode()) {
285         case ImageWriteParam.MODE_EXPLICIT:
286             compressionType = getCompressionType(bmpParam.getCompressionType());
287             break;
288         case ImageWriteParam.MODE_COPY_FROM_METADATA:
289             compressionType = bmpImageMetadata.compression;
290             break;
291         case ImageWriteParam.MODE_DEFAULT:
292             compressionType = getPreferredCompressionType(colorModel, sampleModel);
293             break;
294         default:
295             // ImageWriteParam.MODE_DISABLED:
296
compressionType = BI_RGB;
297         }
298         
299         if (!canEncodeImage(compressionType, colorModel, sampleModel)) {
300             throw new IOException JavaDoc("Image can not be encoded with compression type "
301                                   + compressionTypeNames[compressionType]);
302         }
303  
304         byte r[] = null, g[] = null, b[] = null, a[] = null;
305
306         if (colorModel instanceof IndexColorModel JavaDoc) {
307             isPalette = true;
308             icm = (IndexColorModel JavaDoc)colorModel;
309             paletteEntries = icm.getMapSize();
310
311             if (paletteEntries <= 2) {
312                 bitsPerPixel = 1;
313                 destScanlineBytes = w + 7 >> 3;
314             } else if (paletteEntries <= 16) {
315                 bitsPerPixel = 4;
316                 destScanlineBytes = w + 1 >> 1;
317             } else if (paletteEntries <= 256) {
318                 bitsPerPixel = 8;
319             } else {
320                 // Cannot be written as a Palette image. So write out as
321
// 24 bit image.
322
bitsPerPixel = 24;
323                 isPalette = false;
324                 paletteEntries = 0;
325                 destScanlineBytes = w * 3;
326             }
327
328             if (isPalette == true) {
329                 r = new byte[paletteEntries];
330                 g = new byte[paletteEntries];
331                 b = new byte[paletteEntries];
332                 a = new byte[paletteEntries];
333
334                 icm.getAlphas(a);
335                 icm.getReds(r);
336                 icm.getGreens(g);
337                 icm.getBlues(b);
338             }
339
340         } else {
341             // Grey scale images
342
if (numBands == 1) {
343
344                 isPalette = true;
345                 paletteEntries = 256;
346                 bitsPerPixel = sampleSize[0];
347
348                 destScanlineBytes = (w * bitsPerPixel + 7 >> 3);
349                 
350                 r = new byte[256];
351                 g = new byte[256];
352                 b = new byte[256];
353                 a = new byte[256];
354
355                 for (int i = 0; i < 256; i++) {
356                     r[i] = (byte)i;
357                     g[i] = (byte)i;
358                     b[i] = (byte)i;
359                     a[i] = (byte)255;
360                 }
361            
362             } else {
363                 if (sampleModel instanceof SinglePixelPackedSampleModel JavaDoc &&
364                     noSubband) {
365                     bitsPerPixel =
366                         DataBuffer.getDataTypeSize(sampleModel.getDataType());
367                     destScanlineBytes = w * bitsPerPixel + 7 >> 3;
368
369                     if (compressionType == BMPConstants.BI_BITFIELDS) {
370                         isPalette = true;
371                         paletteEntries = 3;
372                         r = new byte[paletteEntries];
373                         g = new byte[paletteEntries];
374                         b = new byte[paletteEntries];
375                         a = new byte[paletteEntries];
376                         if (bitsPerPixel == 16) {
377                             b[0]=(byte)0x00; g[0]=(byte)0x00; r[0]=(byte)0xF8; a[0]=(byte)0x00; // red mask 0x00000F800
378
b[1]=(byte)0x00; g[1]=(byte)0x00; r[1]=(byte)0x07; a[1]=(byte)0xE0; // green mask 0x0000007E0
379
b[2]=(byte)0x00; g[2]=(byte)0x00; r[2]=(byte)0x00; a[2]=(byte)0x1F; // blue mask 0x00000001F
380
} else if (bitsPerPixel == 32) {
381                             b[0]=(byte)0x00; g[0]=(byte)0xFF; r[0]=(byte)0x00; a[0]=(byte)0x00; // red mask 0x00FF0000
382
b[1]=(byte)0x00; g[1]=(byte)0x00; r[1]=(byte)0xFF; a[1]=(byte)0x00; // green mask 0x0000FF00
383
b[2]=(byte)0x00; g[2]=(byte)0x00; r[2]=(byte)0x00; a[2]=(byte)0xFF; // blue mask 0x000000FF
384
} else {
385                             throw new RuntimeException JavaDoc(I18N.getString("BMPImageWrite6"));
386                         }
387                     }
388                 }
389             }
390         }
391
392         // actual writing of image data
393
int fileSize = 0;
394         int offset = 0;
395         int headerSize = 0;
396         int imageSize = 0;
397         int xPelsPerMeter = 0;
398         int yPelsPerMeter = 0;
399         int colorsUsed = 0;
400         int colorsImportant = paletteEntries;
401
402         // Calculate padding for each scanline
403
int padding = destScanlineBytes % 4;
404         if (padding != 0) {
405             padding = 4 - padding;
406         }
407
408         if (sampleModel instanceof SinglePixelPackedSampleModel JavaDoc && noSubband) {
409             destScanlineBytes = w;
410             bitPos =
411                 ((SinglePixelPackedSampleModel JavaDoc)sampleModel).getBitMasks();
412             for (int i = 0; i < bitPos.length; i++)
413                 bitPos[i] = firstLowBit(bitPos[i]);
414         }
415
416     // FileHeader is 14 bytes, BitmapHeader is 40 bytes,
417
// add palette size and that is where the data will begin
418
offset = 54 + paletteEntries * 4;
419     
420     imageSize = (destScanlineBytes + padding) * h;
421     fileSize = imageSize + offset;
422     headerSize = 40;
423
424         long headPos = stream.getStreamPosition();
425
426         writeFileHeader(fileSize, offset);
427
428         writeInfoHeader(headerSize, bitsPerPixel);
429
430         // compression
431
stream.writeInt(compressionType);
432
433         // imageSize
434
stream.writeInt(imageSize);
435
436         // xPelsPerMeter
437
stream.writeInt(xPelsPerMeter);
438
439         // yPelsPerMeter
440
stream.writeInt(yPelsPerMeter);
441
442         // Colors Used
443
stream.writeInt(colorsUsed);
444
445         // Colors Important
446
stream.writeInt(colorsImportant);
447
448         // palette
449
if (isPalette == true) {
450
451             // write palette
452
if (compressionType == BMPConstants.BI_BITFIELDS) {
453         // write masks for red, green and blue components.
454
for (int i=0; i<3; i++) {
455             int mask = (a[i]&0xFF) + ((r[i]&0xFF)*0x100) + ((g[i]&0xFF)*0x10000) + ((b[i]&0xFF)*0x1000000);
456             stream.writeInt(mask);
457         }
458         } else {
459         for (int i=0; i<paletteEntries; i++) {
460             stream.writeByte(b[i]);
461             stream.writeByte(g[i]);
462             stream.writeByte(r[i]);
463             stream.writeByte(a[i]);
464         }
465         }
466     }
467         
468         // Writing of actual image data
469
int scanlineBytes = w * numBands;
470
471         // Buffer for up to 8 rows of pixels
472
int[] pixels = new int[scanlineBytes * scaleX];
473
474         // Also create a buffer to hold one line of the data
475
// to be written to the file, so we can use array writes.
476
bpixels = new byte[destScanlineBytes];
477
478         int l;
479     
480         if (compressionType == BMPConstants.BI_JPEG ||
481             compressionType == BMPConstants.BI_PNG) {
482             
483             // prepare embedded buffer
484
embedded_stream = new ByteArrayOutputStream JavaDoc();
485             writeEmbedded(image, bmpParam);
486             // update the file/image Size
487
embedded_stream.flush();
488             imageSize = embedded_stream.size();
489             
490             long endPos = stream.getStreamPosition();
491             fileSize = (int)(offset + imageSize);
492             stream.seek(headPos);
493             writeSize(fileSize, 2);
494             stream.seek(headPos);
495             writeSize(imageSize, 34);
496             stream.seek(endPos);
497             stream.write(embedded_stream.toByteArray());
498             embedded_stream = null;
499
500             if (abortRequested()) {
501                 processWriteAborted();
502             } else {
503                 processImageComplete();
504                 stream.flushBefore(stream.getStreamPosition());
505             }
506
507             return;
508         }
509
510         isTopDown = bmpParam.isTopDown();
511         
512         int maxBandOffset = bandOffsets[0];
513         for (int i = 1; i < bandOffsets.length; i++)
514             if (bandOffsets[i] > maxBandOffset)
515                 maxBandOffset = bandOffsets[i];
516
517         int[] pixel = new int[maxBandOffset + 1];
518
519         for (int i = 0; i < h; i++) {
520             if (abortRequested()) {
521                 break;
522             }
523
524             int row = minY + i;
525
526             if (!isTopDown)
527                 row = minY + h - i -1;
528
529             // Get the pixels
530
Raster JavaDoc src = inputRaster;
531
532             Rectangle JavaDoc srcRect =
533                 new Rectangle JavaDoc(minX * scaleX + xOffset,
534                               row * scaleY + yOffset,
535                               (w - 1)* scaleX + 1,
536                               1);
537             if (!writeRaster)
538                 src = input.getData(srcRect);
539
540             if (noTransform && noSubband) {
541                 SampleModel JavaDoc sm = src.getSampleModel();
542                 int pos = 0;
543                 int startX = srcRect.x - src.getSampleModelTranslateX();
544                 int startY = srcRect.y - src.getSampleModelTranslateY();
545                 if (sm instanceof ComponentSampleModel JavaDoc) {
546                     ComponentSampleModel JavaDoc csm = (ComponentSampleModel JavaDoc)sm;
547                     pos = csm.getOffset(startX, startY, 0);
548                     for(int nb=1; nb < csm.getNumBands(); nb++) {
549                         if (pos > csm.getOffset(startX, startY, nb)) {
550                             pos = csm.getOffset(startX, startY, nb);
551                         }
552                     }
553                 } else if (sm instanceof MultiPixelPackedSampleModel JavaDoc) {
554                     MultiPixelPackedSampleModel JavaDoc mppsm =
555                         (MultiPixelPackedSampleModel JavaDoc)sm;
556                     pos = mppsm.getOffset(startX, startY);
557                 } else if (sm instanceof SinglePixelPackedSampleModel JavaDoc) {
558                     SinglePixelPackedSampleModel JavaDoc sppsm =
559                         (SinglePixelPackedSampleModel JavaDoc)sm;
560                     pos = sppsm.getOffset(startX, startY);
561                 }
562
563                 if (compressionType == BMPConstants.BI_RGB || compressionType == BMPConstants.BI_BITFIELDS){
564                     switch(dataType) {
565                     case DataBuffer.TYPE_BYTE:
566                         byte[] bdata =
567                             ((DataBufferByte JavaDoc)src.getDataBuffer()).getData();
568                         stream.write(bdata, pos, destScanlineBytes);
569                         break;
570
571                     case DataBuffer.TYPE_SHORT:
572                         short[] sdata =
573                             ((DataBufferShort JavaDoc)src.getDataBuffer()).getData();
574                         stream.writeShorts(sdata, pos, destScanlineBytes);
575                         break;
576
577                     case DataBuffer.TYPE_USHORT:
578                         short[] usdata =
579                             ((DataBufferUShort JavaDoc)src.getDataBuffer()).getData();
580                         stream.writeShorts(usdata, pos, destScanlineBytes);
581                         break;
582
583                     case DataBuffer.TYPE_INT:
584                         int[] idata =
585                             ((DataBufferInt JavaDoc)src.getDataBuffer()).getData();
586                         stream.writeInts(idata, pos, destScanlineBytes);
587                         break;
588                     }
589
590                     for(int k=0; k<padding; k++) {
591                         stream.writeByte(0);
592                     }
593                 } else if (compressionType == BMPConstants.BI_RLE4) {
594                     if (bpixels == null || bpixels.length < scanlineBytes)
595                         bpixels = new byte[scanlineBytes];
596                     src.getPixels(srcRect.x, srcRect.y,
597                                   srcRect.width, srcRect.height, pixels);
598                     for (int h=0; h<scanlineBytes; h++) {
599                         bpixels[h] = (byte)pixels[h];
600                     }
601                     encodeRLE4(bpixels, scanlineBytes);
602                 } else if (compressionType == BMPConstants.BI_RLE8) {
603                     //byte[] bdata =
604
// ((DataBufferByte)src.getDataBuffer()).getData();
605
//System.out.println("bdata.length="+bdata.length);
606
//System.arraycopy(bdata, pos, bpixels, 0, scanlineBytes);
607
if (bpixels == null || bpixels.length < scanlineBytes)
608                         bpixels = new byte[scanlineBytes];
609                     src.getPixels(srcRect.x, srcRect.y,
610                                   srcRect.width, srcRect.height, pixels);
611                     for (int h=0; h<scanlineBytes; h++) {
612                         bpixels[h] = (byte)pixels[h];
613                     }
614                     
615                     encodeRLE8(bpixels, scanlineBytes);
616                 }
617             } else {
618                 src.getPixels(srcRect.x, srcRect.y,
619                               srcRect.width, srcRect.height, pixels);
620
621
622                 if (scaleX != 1 || maxBandOffset != numBands -1 ||
623                     bgrOrder)
624                     for (int j = 0, k = 0, n=0; j < w;
625                          j++, k += scaleX * numBands, n += numBands) {
626                         System.arraycopy(pixels, k, pixel, 0, pixel.length);
627                         for (int m = 0; m < numBands; m++)
628                             pixels[n + numBands - m - 1] =
629                                 pixel[bandOffsets[sourceBands[m]]];
630                     }
631
632                 writePixels(0, scanlineBytes, bitsPerPixel, pixels,
633                             padding, numBands, icm);
634             }
635
636             processImageProgress(100.0f * (((float)i) / ((float)h)));
637         }
638
639         if (compressionType == BMPConstants.BI_RLE4 ||
640             compressionType == BMPConstants.BI_RLE8) {
641             // Write the RLE EOF marker and
642
stream.writeByte(0);
643             stream.writeByte(1);
644             incCompImageSize(2);
645             // update the file/image Size
646
imageSize = compImageSize;
647             fileSize = compImageSize + offset;
648             long endPos = stream.getStreamPosition();
649             stream.seek(headPos);
650             writeSize(fileSize, 2);
651             stream.seek(headPos);
652             writeSize(imageSize, 34);
653             stream.seek(endPos);
654         }
655
656         if (abortRequested()) {
657             processWriteAborted();
658         } else {
659             processImageComplete();
660             stream.flushBefore(stream.getStreamPosition());
661         }
662     }
663
664     private void writePixels(int l, int scanlineBytes, int bitsPerPixel,
665                              int pixels[],
666                              int padding, int numBands,
667                              IndexColorModel JavaDoc icm) throws IOException JavaDoc {
668         int pixel = 0;
669         int k = 0;
670         switch (bitsPerPixel) {
671
672         case 1:
673
674             for (int j=0; j<scanlineBytes/8; j++) {
675                 bpixels[k++] = (byte)((pixels[l++] << 7) |
676                                       (pixels[l++] << 6) |
677                                       (pixels[l++] << 5) |
678                                       (pixels[l++] << 4) |
679                                       (pixels[l++] << 3) |
680                                       (pixels[l++] << 2) |
681                                       (pixels[l++] << 1) |
682                                       pixels[l++]);
683             }
684
685             // Partially filled last byte, if any
686
if (scanlineBytes%8 > 0) {
687                 pixel = 0;
688                 for (int j=0; j<scanlineBytes%8; j++) {
689                     pixel |= (pixels[l++] << (7 - j));
690                 }
691                 bpixels[k++] = (byte)pixel;
692             }
693             stream.write(bpixels, 0, (scanlineBytes+7)/8);
694
695             break;
696
697         case 4:
698             if (compressionType == BMPConstants.BI_RLE4){
699                 byte[] bipixels = new byte[scanlineBytes];
700                 for (int h=0; h<scanlineBytes; h++) {
701                     bipixels[h] = (byte)pixels[l++];
702                 }
703                 encodeRLE4(bipixels, scanlineBytes);
704             }else {
705                 for (int j=0; j<scanlineBytes/2; j++) {
706                     pixel = (pixels[l++] << 4) | pixels[l++];
707                     bpixels[k++] = (byte)pixel;
708                 }
709                 // Put the last pixel of odd-length lines in the 4 MSBs
710
if ((scanlineBytes%2) == 1) {
711                     pixel = pixels[l] << 4;
712                     bpixels[k++] = (byte)pixel;
713                 }
714                 stream.write(bpixels, 0, (scanlineBytes+1)/2);
715             }
716             break;
717
718         case 8:
719             if(compressionType == BMPConstants.BI_RLE8) {
720                 for (int h=0; h<scanlineBytes; h++) {
721                     bpixels[h] = (byte)pixels[l++];
722                 }
723                 encodeRLE8(bpixels, scanlineBytes);
724             }else {
725                 for (int j=0; j<scanlineBytes; j++) {
726                     bpixels[j] = (byte)pixels[l++];
727                 }
728                 stream.write(bpixels, 0, scanlineBytes);
729             }
730             break;
731
732         case 16:
733             if (spixels == null)
734                 spixels = new short[scanlineBytes / numBands];
735             for (int j = 0, m = 0; j < scanlineBytes; m++) {
736                 spixels[m] = 0;
737                 for(int i = numBands -1 ; i >= 0; i--, j++)
738                     spixels[m] |= pixels[j] << bitPos[i];
739             }
740             stream.writeShorts(spixels, 0, spixels.length);
741             break;
742
743         case 24:
744             if (numBands == 3) {
745                 for (int j=0; j<scanlineBytes; j+=3) {
746                     // Since BMP needs BGR format
747
bpixels[k++] = (byte)(pixels[l+2]);
748                     bpixels[k++] = (byte)(pixels[l+1]);
749                     bpixels[k++] = (byte)(pixels[l]);
750                     l+=3;
751                 }
752                 stream.write(bpixels, 0, scanlineBytes);
753             } else {
754                 // Case where IndexColorModel had > 256 colors.
755
int entries = icm.getMapSize();
756
757                 byte r[] = new byte[entries];
758                 byte g[] = new byte[entries];
759                 byte b[] = new byte[entries];
760
761                 icm.getReds(r);
762                 icm.getGreens(g);
763                 icm.getBlues(b);
764                 int index;
765
766                 for (int j=0; j<scanlineBytes; j++) {
767                     index = pixels[l];
768                     bpixels[k++] = b[index];
769                     bpixels[k++] = g[index];
770                     bpixels[k++] = b[index];
771                     l++;
772                 }
773                 stream.write(bpixels, 0, scanlineBytes*3);
774             }
775             break;
776
777         case 32:
778             if (ipixels == null)
779                 ipixels = new int[scanlineBytes / numBands];
780             for (int j = 0, m = 0; j < scanlineBytes; m++) {
781                 ipixels[m] = 0;
782                 for(int i = numBands -1 ; i >= 0; i--, j++)
783                     ipixels[m] |= pixels[j] << bitPos[i];
784             }
785             stream.writeInts(ipixels, 0, ipixels.length);
786             break;
787         }
788
789         // Write out the padding
790
if (compressionType == BMPConstants.BI_RGB){
791             for(k=0; k<padding; k++) {
792                 stream.writeByte(0);
793             }
794         }
795     }
796
797     private void encodeRLE8(byte[] bpixels, int scanlineBytes)
798       throws IOException JavaDoc{
799
800         int runCount = 1, absVal = -1, j = -1;
801         byte runVal = 0, nextVal =0 ;
802
803         runVal = bpixels[++j];
804         byte[] absBuf = new byte[256];
805
806         while (j < scanlineBytes-1) {
807             nextVal = bpixels[++j];
808             if (nextVal == runVal ){
809                 if(absVal >= 3 ){
810                     /// Check if there was an existing Absolute Run
811
stream.writeByte(0);
812                     stream.writeByte(absVal);
813                     incCompImageSize(2);
814                     for(int a=0; a<absVal;a++){
815                         stream.writeByte(absBuf[a]);
816                         incCompImageSize(1);
817                     }
818                     if (!isEven(absVal)){
819                         //Padding
820
stream.writeByte(0);
821                         incCompImageSize(1);
822                     }
823                 }
824                 else if(absVal > -1){
825                     /// Absolute Encoding for less than 3
826
/// treated as regular encoding
827
/// Do not include the last element since it will
828
/// be inclued in the next encoding/run
829
for (int b=0;b<absVal;b++){
830                         stream.writeByte(1);
831                         stream.writeByte(absBuf[b]);
832                         incCompImageSize(2);
833                     }
834                 }
835                 absVal = -1;
836                 runCount++;
837                 if (runCount == 256){
838                     /// Only 255 values permitted
839
stream.writeByte(runCount-1);
840                     stream.writeByte(runVal);
841                     incCompImageSize(2);
842                     runCount = 1;
843                 }
844             }
845             else {
846                 if (runCount > 1){
847                     /// If there was an existing run
848
stream.writeByte(runCount);
849                     stream.writeByte(runVal);
850                     incCompImageSize(2);
851                 } else if (absVal < 0){
852                     // First time..
853
absBuf[++absVal] = runVal;
854                     absBuf[++absVal] = nextVal;
855                 } else if (absVal < 254){
856                     // 0-254 only
857
absBuf[++absVal] = nextVal;
858                 } else {
859                     stream.writeByte(0);
860                     stream.writeByte(absVal+1);
861                     incCompImageSize(2);
862                     for(int a=0; a<=absVal;a++){
863                         stream.writeByte(absBuf[a]);
864                         incCompImageSize(1);
865                     }
866                     // padding since 255 elts is not even
867
stream.writeByte(0);
868                     incCompImageSize(1);
869                     absVal = -1;
870                 }
871                 runVal = nextVal;
872                 runCount = 1;
873             }
874
875             if (j == scanlineBytes-1){ // EOF scanline
876
// Write the run
877
if (absVal == -1){
878                     stream.writeByte(runCount);
879                     stream.writeByte(runVal);
880                     incCompImageSize(2);
881                     runCount = 1;
882                 }
883                 else {
884                     // write the Absolute Run
885
if(absVal >= 2){
886                         stream.writeByte(0);
887                         stream.writeByte(absVal+1);
888                         incCompImageSize(2);
889                         for(int a=0; a<=absVal;a++){
890                             stream.writeByte(absBuf[a]);
891                             incCompImageSize(1);
892                         }
893                         if (!isEven(absVal+1)){
894                             //Padding
895
stream.writeByte(0);
896                             incCompImageSize(1);
897                         }
898
899                     }
900                     else if(absVal > -1){
901                         for (int b=0;b<=absVal;b++){
902                             stream.writeByte(1);
903                             stream.writeByte(absBuf[b]);
904                             incCompImageSize(2);
905                         }
906                     }
907                 }
908                 /// EOF scanline
909

910                 stream.writeByte(0);
911                 stream.writeByte(0);
912                 incCompImageSize(2);
913             }
914         }
915     }
916
917     private void encodeRLE4(byte[] bipixels, int scanlineBytes)
918       throws IOException JavaDoc {
919
920         int runCount=2, absVal=-1, j=-1, pixel=0, q=0;
921         byte runVal1=0, runVal2=0, nextVal1=0, nextVal2=0;
922         byte[] absBuf = new byte[256];
923
924
925         runVal1 = bipixels[++j];
926         runVal2 = bipixels[++j];
927
928         while (j < scanlineBytes-2){
929             nextVal1 = bipixels[++j];
930             nextVal2 = bipixels[++j];
931
932             if (nextVal1 == runVal1 ) {
933
934                 //Check if there was an existing Absolute Run
935
if(absVal >= 4){
936                     stream.writeByte(0);
937                     stream.writeByte(absVal - 1);
938                     incCompImageSize(2);
939                     // we need to exclude last 2 elts, similarity of
940
// which caused to enter this part of the code
941
for(int a=0; a<absVal-2;a+=2){
942                         pixel = (absBuf[a] << 4) | absBuf[a+1];
943                         stream.writeByte((byte)pixel);
944                         incCompImageSize(1);
945                     }
946                     // if # of elts is odd - read the last element
947
if(!(isEven(absVal-1))){
948                         q = absBuf[absVal-2] << 4| 0;
949                         stream.writeByte(q);
950                         incCompImageSize(1);
951                     }
952                     // Padding to word align absolute encoding
953
if ( !isEven((int)Math.ceil((absVal-1)/2)) ) {
954                         stream.writeByte(0);
955                         incCompImageSize(1);
956                     }
957                 } else if (absVal > -1){
958                     stream.writeByte(2);
959                     pixel = (absBuf[0] << 4) | absBuf[1];
960                     stream.writeByte(pixel);
961                     incCompImageSize(2);
962                 }
963                 absVal = -1;
964
965                 if (nextVal2 == runVal2){
966                     // Even runlength
967
runCount+=2;
968                     if(runCount == 256){
969                         stream.writeByte(runCount-1);
970                         pixel = ( runVal1 << 4) | runVal2;
971                         stream.writeByte(pixel);
972                         incCompImageSize(2);
973                         runCount =2;
974                         if(j< scanlineBytes - 1){
975                             runVal1 = runVal2;
976                             runVal2 = bipixels[++j];
977                         } else {
978                             stream.writeByte(01);
979                             int r = runVal2 << 4 | 0;
980                             stream.writeByte(r);
981                             incCompImageSize(2);
982                             runCount = -1;/// Only EOF required now
983
}
984                     }
985                 } else {
986                     // odd runlength and the run ends here
987
// runCount wont be > 254 since 256/255 case will
988
// be taken care of in above code.
989
runCount++;
990                     pixel = ( runVal1 << 4) | runVal2;
991                     stream.writeByte(runCount);
992                     stream.writeByte(pixel);
993                     incCompImageSize(2);
994                     runCount = 2;
995                     runVal1 = nextVal2;
996                     // If end of scanline
997
if (j < scanlineBytes -1){
998                         runVal2 = bipixels[++j];
999                     }else {
1000                        stream.writeByte(01);
1001                        int r = nextVal2 << 4 | 0;
1002                        stream.writeByte(r);
1003                        incCompImageSize(2);
1004                        runCount = -1;/// Only EOF required now
1005
}
1006
1007                }
1008            } else{
1009                // Check for existing run
1010
if (runCount > 2){
1011                    pixel = ( runVal1 << 4) | runVal2;
1012                    stream.writeByte(runCount);
1013                    stream.writeByte(pixel);
1014                    incCompImageSize(2);
1015                } else if (absVal < 0){ // first time
1016
absBuf[++absVal] = runVal1;
1017                    absBuf[++absVal] = runVal2;
1018                    absBuf[++absVal] = nextVal1;
1019                    absBuf[++absVal] = nextVal2;
1020                } else if (absVal < 253){ // only 255 elements
1021
absBuf[++absVal] = nextVal1;
1022                    absBuf[++absVal] = nextVal2;
1023                } else {
1024                    stream.writeByte(0);
1025                    stream.writeByte(absVal+1);
1026                    incCompImageSize(2);
1027                    for(int a=0; a<absVal;a+=2){
1028                        pixel = (absBuf[a] << 4) | absBuf[a+1];
1029                        stream.writeByte((byte)pixel);
1030                        incCompImageSize(1);
1031                    }
1032                    // Padding for word align
1033
// since it will fit into 127 bytes
1034
stream.writeByte(0);
1035                    incCompImageSize(1);
1036                    absVal = -1;
1037                }
1038
1039                runVal1 = nextVal1;
1040                runVal2 = nextVal2;
1041                runCount = 2;
1042            }
1043            // Handle the End of scanline for the last 2 4bits
1044
if (j >= scanlineBytes-2 ) {
1045                if (absVal == -1 && runCount >= 2){
1046                    if (j == scanlineBytes-2){
1047                        if(bipixels[++j] == runVal1){
1048                            runCount++;
1049                            pixel = ( runVal1 << 4) | runVal2;
1050                            stream.writeByte(runCount);
1051                            stream.writeByte(pixel);
1052                            incCompImageSize(2);
1053                        } else {
1054                            pixel = ( runVal1 << 4) | runVal2;
1055                            stream.writeByte(runCount);
1056                            stream.writeByte(pixel);
1057                            stream.writeByte(01);
1058                            pixel = bipixels[j]<<4 |0;
1059                            stream.writeByte(pixel);
1060                            int n = bipixels[j]<<4|0;
1061                            incCompImageSize(4);
1062                        }
1063                    } else {
1064                        stream.writeByte(runCount);
1065                        pixel =( runVal1 << 4) | runVal2 ;
1066                        stream.writeByte(pixel);
1067                        incCompImageSize(2);
1068                    }
1069                } else if(absVal > -1){
1070                    if (j == scanlineBytes-2){
1071                        absBuf[++absVal] = bipixels[++j];
1072                    }
1073                    if (absVal >=2){
1074                        stream.writeByte(0);
1075                        stream.writeByte(absVal+1);
1076                        incCompImageSize(2);
1077                        for(int a=0; a<absVal;a+=2){
1078                            pixel = (absBuf[a] << 4) | absBuf[a+1];
1079                            stream.writeByte((byte)pixel);
1080                            incCompImageSize(1);
1081                        }
1082                        if(!(isEven(absVal+1))){
1083                            q = absBuf[absVal] << 4|0;
1084                            stream.writeByte(q);
1085                            incCompImageSize(1);
1086                        }
1087
1088                        // Padding
1089
if ( !isEven((int)Math.ceil((absVal+1)/2)) ) {
1090                            stream.writeByte(0);
1091                            incCompImageSize(1);
1092                        }
1093
1094                    } else {
1095                        switch (absVal){
1096                        case 0:
1097                            stream.writeByte(1);
1098                            int n = absBuf[0]<<4 | 0;
1099                            stream.writeByte(n);
1100                            incCompImageSize(2);
1101                            break;
1102                        case 1:
1103                            stream.writeByte(2);
1104                            pixel = (absBuf[0] << 4) | absBuf[1];
1105                            stream.writeByte(pixel);
1106                            incCompImageSize(2);
1107                            break;
1108                        }
1109                    }
1110
1111                }
1112                stream.writeByte(0);
1113                stream.writeByte(0);
1114                incCompImageSize(2);
1115            }
1116        }
1117    }
1118
1119
1120    private synchronized void incCompImageSize(int value){
1121        compImageSize = compImageSize + value;
1122    }
1123
1124    private boolean isEven(int number) {
1125        return (number%2 == 0 ? true : false);
1126    }
1127
1128    private void writeFileHeader(int fileSize, int offset) throws IOException JavaDoc {
1129        // magic value
1130
stream.writeByte('B');
1131        stream.writeByte('M');
1132
1133        // File size
1134
stream.writeInt(fileSize);
1135
1136        // reserved1 and reserved2
1137
stream.writeInt(0);
1138
1139        // offset to image data
1140
stream.writeInt(offset);
1141    }
1142
1143
1144    private void writeInfoHeader(int headerSize,
1145                                 int bitsPerPixel) throws IOException JavaDoc {
1146        // size of header
1147
stream.writeInt(headerSize);
1148
1149        // width
1150
stream.writeInt(w);
1151
1152        // height
1153
stream.writeInt(h);
1154
1155        // number of planes
1156
stream.writeShort(1);
1157
1158        // Bits Per Pixel
1159
stream.writeShort(bitsPerPixel);
1160    }
1161
1162    private void writeSize(int dword, int offset) throws IOException JavaDoc {
1163        stream.skipBytes(offset);
1164        stream.writeInt(dword);
1165    }
1166
1167    public void reset() {
1168        super.reset();
1169        stream = null;
1170    }
1171
1172    private int getCompressionType(String JavaDoc typeString) {
1173        for (int i = 0; i < BMPConstants.compressionTypeNames.length; i++)
1174            if (BMPConstants.compressionTypeNames[i].equals(typeString))
1175                return i;
1176        return 0;
1177    }
1178
1179    private void writeEmbedded(IIOImage JavaDoc image,
1180                               ImageWriteParam JavaDoc bmpParam) throws IOException JavaDoc {
1181        String JavaDoc format =
1182            compressionType == BMPConstants.BI_JPEG ? "jpeg" : "png";
1183        Iterator JavaDoc iterator = ImageIO.getImageWritersByFormatName(format);
1184        ImageWriter JavaDoc writer = null;
1185        if (iterator.hasNext())
1186            writer = (ImageWriter JavaDoc)iterator.next();
1187        if (writer != null) {
1188            if (embedded_stream == null) {
1189                throw new RuntimeException JavaDoc("No stream for writing embedded image!");
1190            }
1191
1192            writer.addIIOWriteProgressListener(new IIOWriteProgressAdapter() {
1193                    public void imageProgress(ImageWriter JavaDoc source, float percentageDone) {
1194                        processImageProgress(percentageDone);
1195                    }
1196                });
1197
1198            writer.addIIOWriteWarningListener(new IIOWriteWarningListener JavaDoc() {
1199                    public void warningOccurred(ImageWriter JavaDoc source, int imageIndex, String JavaDoc warning) {
1200                        processWarningOccurred(imageIndex, warning);
1201                    }
1202                });
1203 
1204            writer.setOutput(ImageIO.createImageOutputStream(embedded_stream));
1205            ImageWriteParam JavaDoc param = writer.getDefaultWriteParam();
1206            //param.setDestinationBands(bmpParam.getDestinationBands());
1207
param.setDestinationOffset(bmpParam.getDestinationOffset());
1208            param.setSourceBands(bmpParam.getSourceBands());
1209            param.setSourceRegion(bmpParam.getSourceRegion());
1210            param.setSourceSubsampling(bmpParam.getSourceXSubsampling(),
1211                                       bmpParam.getSourceYSubsampling(),
1212                                       bmpParam.getSubsamplingXOffset(),
1213                                       bmpParam.getSubsamplingYOffset());
1214            writer.write(null, image, param);
1215        } else
1216            throw new RuntimeException JavaDoc(I18N.getString("BMPImageWrite5") + " " + format);
1217
1218    }
1219
1220    private int firstLowBit(int num) {
1221        int count = 0;
1222        while ((num & 1) == 0) {
1223            count++;
1224            num >>>= 1;
1225        }
1226        return count;
1227    }
1228
1229    private class IIOWriteProgressAdapter implements IIOWriteProgressListener JavaDoc {
1230
1231        public void imageComplete(ImageWriter JavaDoc source) {
1232        }
1233        
1234        public void imageProgress(ImageWriter JavaDoc source, float percentageDone) {
1235        }
1236
1237        public void imageStarted(ImageWriter JavaDoc source, int imageIndex) {
1238        }
1239
1240        public void thumbnailComplete(ImageWriter JavaDoc source) {
1241        }
1242
1243        public void thumbnailProgress(ImageWriter JavaDoc source, float percentageDone) {
1244        }
1245
1246        public void thumbnailStarted(ImageWriter JavaDoc source, int imageIndex, int thumbnailIndex) {
1247        }
1248
1249        public void writeAborted(ImageWriter JavaDoc source) {
1250        }
1251    }
1252
1253    /*
1254     * Returns preferred compression type for given image.
1255     * The default compression type is BI_RGB, but some image types can't be
1256     * encodeed with using default compression without cahnge color resolution.
1257     * For example, TYPE_USHORT_565_RGB may be encodeed only by using BI_BITFIELDS
1258     * compression type.
1259     *
1260     * NB: we probably need to extend this method if we encounter other image
1261     * types which can not be encoded with BI_RGB compression type.
1262     */

1263    protected int getPreferredCompressionType(ColorModel JavaDoc cm, SampleModel JavaDoc sm) {
1264        ImageTypeSpecifier JavaDoc imageType = new ImageTypeSpecifier JavaDoc(cm, sm);
1265        return getPreferredCompressionType(imageType);
1266    }
1267
1268    protected int getPreferredCompressionType(ImageTypeSpecifier JavaDoc imageType) {
1269        if (imageType.getBufferedImageType() == BufferedImage.TYPE_USHORT_565_RGB) {
1270            return BI_BITFIELDS;
1271        }
1272        return BI_RGB;
1273    }
1274
1275    /*
1276     * Check whether we can encode image of given type using compression method in question.
1277     *
1278     * For example, TYPE_USHORT_565_RGB can be encodeed with BI_BITFIELDS compression only.
1279     *
1280     * NB: method should be extended if other cases when we can not encode
1281     * with given compression will be discovered.
1282     */

1283    protected boolean canEncodeImage(int compression, ColorModel JavaDoc cm, SampleModel JavaDoc sm) {
1284        ImageTypeSpecifier JavaDoc imgType = new ImageTypeSpecifier JavaDoc(cm, sm);
1285        return canEncodeImage(compression, imgType);
1286    }
1287
1288    protected boolean canEncodeImage(int compression, ImageTypeSpecifier JavaDoc imgType) {
1289        ImageWriterSpi JavaDoc spi = this.getOriginatingProvider();
1290        if (!spi.canEncodeImage(imgType)) {
1291            return false;
1292        }
1293        int biType = imgType.getBufferedImageType();
1294        if (biType == BufferedImage.TYPE_USHORT_565_RGB
1295            && compression != BI_BITFIELDS) {
1296            return false;
1297        }
1298        
1299        int bpp = imgType.getColorModel().getPixelSize();
1300        if (compressionType == BI_RLE4 && bpp != 4) {
1301            // only 4bpp images can be encoded as BI_RLE4
1302
return false;
1303        }
1304        if (compressionType == BI_RLE8 && bpp != 8) {
1305            // only 8bpp images can be encoded as BI_RLE8
1306
return false;
1307        }
1308
1309        return true;
1310    }
1311}
1312
Popular Tags