001    /*
002     * BMPCodec
003     *
004     * Copyright (c) 2000, 2001, 2002, 2003, 2004 Marco Schmidt.
005     * All rights reserved.
006     */
007    
008    package net.sourceforge.jiu.codecs;
009    
010    import java.io.DataInput;
011    import java.io.DataOutput;
012    import java.io.IOException;
013    import net.sourceforge.jiu.codecs.ImageCodec;
014    import net.sourceforge.jiu.codecs.InvalidFileStructureException;
015    import net.sourceforge.jiu.codecs.UnsupportedTypeException;
016    import net.sourceforge.jiu.codecs.WrongFileFormatException;
017    import net.sourceforge.jiu.data.BilevelImage;
018    import net.sourceforge.jiu.data.ByteChannelImage;
019    import net.sourceforge.jiu.data.Gray8Image;
020    import net.sourceforge.jiu.data.MemoryBilevelImage;
021    import net.sourceforge.jiu.data.MemoryPaletted8Image;
022    import net.sourceforge.jiu.data.MemoryRGB24Image;
023    import net.sourceforge.jiu.data.Paletted8Image;
024    import net.sourceforge.jiu.data.PixelImage;
025    import net.sourceforge.jiu.data.Palette;
026    import net.sourceforge.jiu.data.RGB24Image;
027    import net.sourceforge.jiu.data.RGBIndex;
028    import net.sourceforge.jiu.ops.MissingParameterException;
029    import net.sourceforge.jiu.ops.OperationFailedException;
030    import net.sourceforge.jiu.util.ArrayConverter;
031    
032    /**
033     * A codec to read and write Windows BMP image files.
034     * <p>
035     * Typical file extensions are <code>.bmp</code> and <code>.rle</code>
036     * (the latter is only used for compressed files).
037     * <h3>Bounds</h3>
038     * <p>
039     * This codec supports the bounds concept for loading and saving.
040     * </p>
041     * <h3>Supported BMP types when loading</h3>
042     * <ul>
043     * <li>Bilevel, 1 bit per pixel, uncompressed.
044     *  BMP supports palettes for bilevel images, but the content of that
045     *  palette is ignored and 0 is considered black and 1 white.
046     *  Any class implementing {@link net.sourceforge.jiu.data.BilevelImage}
047     *  can be given to the codec and it will load the image to that object
048     *  (if the image's resolution is sufficient).
049     *  If no image object is given to the codec, a new
050     *  {@link net.sourceforge.jiu.data.MemoryBilevelImage} will be created.</li>
051     * <li>Paletted, 4 bits per pixel, uncompressed or RLE4 compression.
052     *  Both types are loaded to a {@link net.sourceforge.jiu.data.Paletted8Image} object.
053     *  This requires 50 % more space than is necessary, but there is
054     *  no dedicated 4 bit image data class in JIU.</li>
055     * <li>Paletted, 8 bits per pixel, uncompressed or RLE8 compression.
056     *  Both types are loaded to a {@link net.sourceforge.jiu.data.Paletted8Image} object.</li>
057     * <li>RGB truecolor, 24 bits per pixel, uncompressed.
058     *  This is loaded to a {@link net.sourceforge.jiu.data.RGB24Image} object.</li>
059     * </ul>
060     * There is no support for 16 bpp images or BI_BITFIELDS compression (for lack of test files).
061     * <p>
062     * <h3>Supported JIU image data classes when saving to BMP</h3>
063     * <ul>
064     * <li>{@link net.sourceforge.jiu.data.BilevelImage} objects are stored as 1 bit per pixel BMP files.</li>
065     * <li>{@link net.sourceforge.jiu.data.Gray8Image} and 
066     *  {@link net.sourceforge.jiu.data.Paletted8Image} objects are stored as 
067     *  paletted 8 bits per pixel files.
068     *  It doesn't really matter how many entries the palette has, the BMP file's
069     *  palette will always have 256 entries,  filled up with zero entries if necessary.</li>
070     * <li>{@link net.sourceforge.jiu.data.RGB24Image} objects are stored as 24 bpp BMP files.</li>
071     * </ul>
072     * There is no support for compressed BMP files when saving.
073     * <p>
074     * <h3>I/O classes</h3>
075     * BMPCodec works with all input and output classes supported by ImageCodec
076     * ({@link java.io.InputStream}, {@link java.io.OutputStream}, 
077     *  {@link java.io.DataInput}, {@link java.io.DataOutput},
078     *  {@link java.io.RandomAccessFile}).
079     * <h3>Problems</h3>
080     * <p>The RLE-compressed BMP files that I could test this codec on seem to
081     *  have an end-of-line code at the end of every line instead of relying 
082     *  on the decoder to know when it has unpacked enough bytes for a line.
083     *  Whenever this codec encounters an EOL symbol and has a current column
084     *  value of <code>0</code>, the EOL is ignored.
085     * <h3>Usage examples</h3>
086     * Write an image to a BMP file.
087     * <pre>
088     * BMPCodec codec = new BMPCodec();
089     * codec.setImage(image);
090     * codec.setFile("out.bmp", CodecMode.SAVE);
091     * codec.process();
092     * codec.close();
093     * </pre>
094     * Read an image from a BMP file.
095     * <pre>
096     * BMPCodec codec = new BMPCodec();
097     * codec.setFile("image.bmp", CodecMode.LOAD);
098     * codec.process();
099     * codec.close();
100     * PixelImage image = codec.getImage();
101     * </pre>
102     * @author Marco Schmidt
103     * @since 0.7.0
104     */
105    public class BMPCodec extends ImageCodec
106    {
107            private int colorDepth;
108            private int compression;
109            private int dataOffset;
110            private int imageHeight;
111            private int imageWidth;
112            private DataInput in;
113            private DataOutput out;
114            private Palette palette;
115    
116            public String[] getFileExtensions()
117            {
118                    return new String[] {".bmp", ".rle"};
119            }
120    
121            public String getFormatName()
122            {
123                    return "Windows BMP";
124            }
125    
126            public String[] getMimeTypes()
127            {
128                    return new String[] {"image/bmp", "image/x-ms-bmp"};
129            }
130    
131            public boolean isLoadingSupported()
132            {
133                    return true;
134            }
135    
136            public boolean isSavingSupported()
137            {
138                    return true;
139            }
140    
141            private void load() throws
142                    MissingParameterException, 
143                    OperationFailedException, 
144                    UnsupportedTypeException,
145                    WrongFileFormatException
146            {
147                    in = getInputAsDataInput();
148                    if (in == null)
149                    {
150                            throw new MissingParameterException("Input stream / random access file parameter missing.");
151                    }
152                    // now write the output stream
153                    try
154                    {
155                            loadHeader();
156                            loadStream();
157                    }
158                    catch (IOException ioe)
159                    {
160                            // wrap any I/O failures in an OperationFailedException
161                            throw new OperationFailedException("I/O failure: " + ioe.toString());
162                    }
163            }
164    
165            private void loadCompressedPaletted4Stream() throws IOException
166            {
167                    Paletted8Image image = (Paletted8Image)getImage();
168                    int imageBytesPerRow = imageWidth;
169                    int bytesPerRow = imageBytesPerRow;
170                    int mod = bytesPerRow % 4;
171                    if (mod != 0)
172                    {
173                            bytesPerRow += 4 - mod;
174                    }
175                    final int COLUMNS = getBoundsWidth();
176                    final int ROWS = imageHeight - getBoundsY1();
177                    final int X1 = getBoundsX1();
178                    int processedRows = 0;
179                    byte[] row = new byte[bytesPerRow];
180                    int x = 0;
181                    int y = imageHeight - 1;
182                    boolean endOfBitmap = false;
183                    boolean delta = false;
184                    int newX = 0;
185                    int newY = 0;
186                    while (processedRows < ROWS)
187                    {
188                            int v1 = in.readUnsignedByte();
189                            int v2 = in.readUnsignedByte();
190                            if (v1 == 0)
191                            {
192                                    switch(v2)
193                                    {
194                                            case(0):
195                                            {
196                                                    // end of line
197                                                    if (x != 0)
198                                                    {
199                                                            x = bytesPerRow;
200                                                    }
201                                                    break;
202                                            }
203                                            case(1):
204                                            {
205                                                    // end of bitmap
206                                                    x = bytesPerRow;
207                                                    endOfBitmap = true;
208                                                    break;
209                                            }
210                                            case(2):
211                                            {
212                                                    // delta
213                                                    delta = true;
214                                                    newX = x + in.readUnsignedByte();
215                                                    newY = y - in.readUnsignedByte();
216                                                    x = bytesPerRow;
217                                                    break;
218                                            }
219                                            default:
220                                            {
221                                                    // copy the next v2 (3..255) samples from file to output
222                                                    // two samples are packed into one byte
223                                                    // if the number of bytes used to pack is not a multiple of 2,
224                                                    // an additional padding byte is in the stream and must be skipped
225                                                    boolean paddingByte = (((v2 + 1) / 2) % 2) != 0;
226                                                    while (v2 > 1)
227                                                    {
228                                                            int packed = in.readUnsignedByte();
229                                                            int sample1 = (packed >> 4) & 0x0f;
230                                                            int sample2 = packed & 0x0f;
231                                                            row[x++] = (byte)sample1;
232                                                            row[x++] = (byte)sample2;
233                                                            v2 -= 2;
234                                                    }
235                                                    if (v2 == 1)
236                                                    {
237                                                            int packed = in.readUnsignedByte();
238                                                            int sample = (packed >> 4) & 0x0f;
239                                                            row[x++] = (byte)sample;
240                                                    }
241                                                    if (paddingByte)
242                                                    {
243                                                            v2 = in.readUnsignedByte();
244                                                    }
245                                                    break;
246                                            }
247                                    }
248                            }
249                            else
250                            {
251                                    // rle: replicate the two samples in v2 as many times as v1 says
252                                    byte sample1 = (byte)((v2 >> 4) & 0x0f);
253                                    byte sample2 = (byte)(v2 & 0x0f);
254                                    while (v1 > 1)
255                                    {
256                                            row[x++] = sample1;
257                                            row[x++] = sample2;
258                                            v1 -= 2;
259                                    }
260                                    if (v1 == 1)
261                                    {
262                                            row[x++] = sample1;
263                                    }
264                            }
265                            // end of line?
266                            if (x == bytesPerRow)
267                            {
268                                    if (y <= getBoundsY2())
269                                    {
270                                            image.putByteSamples(0, 0, y - getBoundsY1(), COLUMNS, 1, row, X1);
271                                    }
272                                    if (delta)
273                                    {
274                                            x = newX;
275                                            y = newY;
276                                    }
277                                    else
278                                    {
279                                            x = 0;
280                                            y--;
281                                    }
282                                    if (endOfBitmap)
283                                    {
284                                            processedRows = ROWS - 1;
285                                    }
286                                    setProgress(processedRows, ROWS);
287                                    processedRows++;
288                                    delta = false;
289                            }
290                    }
291            }
292    
293            private void loadCompressedPaletted8Stream() throws IOException
294            {
295                    Paletted8Image image = (Paletted8Image)getImage();
296                    int imageBytesPerRow = imageWidth;
297                    int bytesPerRow = imageBytesPerRow;
298                    int mod = bytesPerRow % 4;
299                    if (mod != 0)
300                    {
301                            bytesPerRow += 4 - mod;
302                    }
303                    final int COLUMNS = getBoundsWidth();
304                    final int ROWS = imageHeight - getBoundsY1();
305                    final int X1 = getBoundsX1();
306                    int processedRows = 0;
307                    byte[] row = new byte[bytesPerRow];
308                    int x = 0;
309                    int y = imageHeight - 1;
310                    boolean endOfBitmap = false;
311                    boolean delta = false;
312                    int newX = 0;
313                    int newY = 0;
314                    while (processedRows < ROWS)
315                    {
316                            int v1 = in.readUnsignedByte();
317                            int v2 = in.readUnsignedByte();
318                            if (v1 == 0)
319                            {
320                                    switch(v2)
321                                    {
322                                            case(0):
323                                            {
324                                                    // end of line
325                                                    if (x != 0)
326                                                    {
327                                                            x = bytesPerRow;
328                                                    }
329                                                    break;
330                                            }
331                                            case(1):
332                                            {
333                                                    // end of bitmap
334                                                    x = bytesPerRow;
335                                                    endOfBitmap = true;
336                                                    break;
337                                            }
338                                            case(2):
339                                            {
340                                                    // delta
341                                                    delta = true;
342                                                    newX = x + in.readUnsignedByte();
343                                                    newY = y - in.readUnsignedByte();
344                                                    x = bytesPerRow;
345                                                    break;
346                                            }
347                                            default:
348                                            {
349                                                    // copy the next v2 (3..255) bytes from file to output
350                                                    boolean paddingByte = (v2 % 2) != 0;
351                                                    while (v2-- > 0)
352                                                    {
353                                                            row[x++] = (byte)in.readUnsignedByte();
354                                                    }
355                                                    if (paddingByte)
356                                                    {
357                                                            v2 = in.readUnsignedByte();
358                                                    }
359                                                    break;
360                                            }
361                                    }
362                            }
363                            else
364                            {
365                                    // rle: replicate v2 as many times as v1 says
366                                    byte value = (byte)v2;
367                                    while (v1-- > 0)
368                                    {
369                                            row[x++] = value;
370                                    }
371                            }
372                            // end of line?
373                            if (x == bytesPerRow)
374                            {
375                                    if (y <= getBoundsY2())
376                                    {
377                                            image.putByteSamples(0, 0, y - getBoundsY1(), COLUMNS, 1, row, X1);
378                                    }
379                                    if (delta)
380                                    {
381                                            x = newX;
382                                            y = newY;
383                                    }
384                                    else
385                                    {
386                                            x = 0;
387                                            y--;
388                                    }
389                                    if (endOfBitmap)
390                                    {
391                                            processedRows = ROWS - 1;
392                                    }
393                                    setProgress(processedRows, ROWS);
394                                    processedRows++;
395                                    delta = false;
396                            }
397                    }
398            }
399    
400            private void loadHeader() throws
401                    IOException,
402                    MissingParameterException, 
403                    OperationFailedException, 
404                    UnsupportedTypeException,
405                    WrongFileFormatException
406            {
407                    byte[] header = new byte[54];
408                    in.readFully(header);
409                    if (header[0] != 'B' || header[1] != 'M')
410                    {
411                            throw new WrongFileFormatException("Not a BMP file (first two bytes are not 0x42 0x4d).");
412                    }
413                    dataOffset = ArrayConverter.getIntLE(header, 0x0a);
414                    if (dataOffset < 54)
415                    {
416                            throw new InvalidFileStructureException("BMP data expected to be 54dec or larger, got " + dataOffset);
417                    }
418                    imageWidth = ArrayConverter.getIntLE(header, 0x12);
419                    imageHeight = ArrayConverter.getIntLE(header, 0x16);
420                    if (imageWidth < 1 || imageHeight < 1)
421                    {
422                            throw new InvalidFileStructureException("BMP image width and height must be larger than 0, got " + imageWidth + " x " + imageHeight);
423                    }
424                    int planes = ArrayConverter.getShortLE(header, 0x1a);
425                    if (planes != 1)
426                    {
427                            throw new InvalidFileStructureException("Can only handle BMP number of planes = 1, got " + planes);
428                    }
429                    colorDepth = ArrayConverter.getShortLE(header, 0x1c);
430                    if (colorDepth != 1 && colorDepth != 4 && colorDepth != 8 && colorDepth != 24)
431                    {
432                            // TO DO: add support for 16 bpp BMP reading
433                            throw new InvalidFileStructureException("Unsupported BMP color depth: " + colorDepth);
434                    }
435                    compression = ArrayConverter.getIntLE(header, 0x1e);
436                    if (compression != 0 && !(compression == 1 && colorDepth == 8) && !(compression == 2 && colorDepth == 4))
437                    {
438                            throw new InvalidFileStructureException("Unsupported BMP compression type / color depth combination: " + 
439                                    compression + " / " + colorDepth);
440                    }
441                    float dpiXValue = ArrayConverter.getIntLE(header, 0x26) / (100.0f / 2.54f);
442                    float dpiYValue = ArrayConverter.getIntLE(header, 0x2a) / (100.0f / 2.54f);
443                    setDpi((int)dpiXValue, (int)dpiYValue);
444            }
445    
446            private void loadStream() throws
447                    IOException,
448                    MissingParameterException, 
449                    OperationFailedException, 
450                    UnsupportedTypeException
451            {
452                    // 1. check bounds, initialize them if necessary
453                    setBoundsIfNecessary(imageWidth, imageHeight);
454                    checkBounds(imageWidth, imageHeight);
455                    // 2. read palette if the image isn't truecolor (even monochrome BMPs have a palette)
456                    int bytesToSkip;
457                    if (colorDepth <= 8)
458                    {
459                            int numPaletteEntries = 1 << colorDepth;
460                            int expectedPaletteSize = 4 * numPaletteEntries;
461                            int headerSpaceLeft = dataOffset - 54;
462                            bytesToSkip = headerSpaceLeft - expectedPaletteSize;
463                            if (bytesToSkip < 0)
464                            {
465                                    throw new InvalidFileStructureException("Not enough space in header for palette with " + 
466                                            numPaletteEntries + "entries.");
467                            }
468                            palette = new Palette(numPaletteEntries);
469                            for (int index = 0; index < numPaletteEntries; index++)
470                            {
471                                    int blue = in.readUnsignedByte();
472                                    int green = in.readUnsignedByte();
473                                    int red = in.readUnsignedByte();
474                                    int filler = in.readUnsignedByte();
475                                    palette.put(index, red, green, blue);
476                            }
477                    }
478                    else
479                    {
480                            bytesToSkip = dataOffset - 54;
481                    }
482                    // 3. seek to beginning of image data
483                    while (bytesToSkip > 0)
484                    {
485                            int skipped = in.skipBytes(bytesToSkip);
486                            if (skipped > 0)
487                            {
488                                    bytesToSkip -= skipped;
489                            }
490                    }
491                    // 4. check if we have an image object that we are supposed to reuse
492                    //    if there is one, check if it has the correct type
493                    //    if there is none, create a new one
494                    PixelImage image = getImage();
495                    if (image == null)
496                    {
497                            switch(colorDepth)
498                            {
499                                    case(1):
500                                    {
501                                            setImage(new MemoryBilevelImage(getBoundsWidth(), getBoundsHeight()));
502                                            break;
503                                    }
504                                    case(4):
505                                    case(8):
506                                    {
507                                            setImage(new MemoryPaletted8Image(getBoundsWidth(), getBoundsHeight(), palette));
508                                            break;
509                                    }
510                                    case(24):
511                                    {
512                                            setImage(new MemoryRGB24Image(getBoundsWidth(), getBoundsHeight()));
513                                            break;
514                                    }
515                                    // loadHeader would have thrown an exception for any other color depths
516                            }
517                    }
518                    else
519                    {
520                            // TODO: check if image is of correct type
521                    }
522                    // now read actual image data
523                    if (compression == 0)
524                    {
525                            loadUncompressedStream();
526                    }
527                    else
528                    if (compression == 1)
529                    {
530                            loadCompressedPaletted8Stream();
531                    }
532                    else
533                    if (compression == 2)
534                    {
535                            loadCompressedPaletted4Stream();
536                    }
537            }
538    
539            private void loadUncompressedBilevelStream() throws 
540                    IOException, 
541                    OperationFailedException
542            {
543                    if ((getBoundsX1() % 8) != 0)
544                    {
545                            throw new OperationFailedException("When loading bilevel images, horizontal X1 bounds must be a multiple of 8; got " + getBoundsX1());
546                    }
547                    BilevelImage image = (BilevelImage)getImage();
548                    int imageBytesPerRow = (imageWidth + 7) / 8;
549                    int bytesPerRow = imageBytesPerRow;
550                    int mod = bytesPerRow % 4;
551                    if (mod != 0)
552                    {
553                            bytesPerRow += 4 - mod;
554                    }
555                    int bottomRowsToSkip = imageHeight - 1 - getBoundsY2();
556                    int bytesToSkip = bottomRowsToSkip * bytesPerRow;
557                    while (bytesToSkip > 0)
558                    {
559                            int skipped = in.skipBytes(bytesToSkip);
560                            if (skipped > 0)
561                            {
562                                    bytesToSkip -= skipped;
563                            }
564                    }
565                    final int COLUMNS = getBoundsWidth();
566                    final int ROWS = getBoundsHeight();
567                    final int SRC_OFFSET = getBoundsX1() / 8;
568                    final int SRC_BIT_OFFSET = getBoundsX1() % 8;
569                    int y = image.getHeight() - 1;
570                    int processedRows = 0;
571                    byte[] row = new byte[bytesPerRow];
572                    while (processedRows < ROWS)
573                    {
574                            in.readFully(row);
575                            image.putPackedBytes(0, y, COLUMNS, row, SRC_OFFSET, SRC_BIT_OFFSET);
576                            y--;
577                            setProgress(processedRows, ROWS);
578                            processedRows++;
579                    }
580            }
581    
582            private void loadUncompressedPaletted4Stream() throws
583                    IOException
584            {
585                    Paletted8Image image = (Paletted8Image)getImage();
586                    int imageBytesPerRow = (imageWidth + 1) / 2;
587                    int bytesPerRow = imageBytesPerRow;
588                    int mod = bytesPerRow % 4;
589                    if (mod != 0)
590                    {
591                            bytesPerRow += 4 - mod;
592                    }
593                    int bottomRowsToSkip = imageHeight - 1 - getBoundsY2();
594                    int bytesToSkip = bottomRowsToSkip * bytesPerRow;
595                    while (bytesToSkip > 0)
596                    {
597                            int skipped = in.skipBytes(bytesToSkip);
598                            if (skipped > 0)
599                            {
600                                    bytesToSkip -= skipped;
601                            }
602                    }
603                    final int COLUMNS = getBoundsWidth();
604                    final int ROWS = getBoundsHeight();
605                    final int X1 = getBoundsX1();
606                    int y = image.getHeight() - 1;
607                    int processedRows = 0;
608                    byte[] row = new byte[bytesPerRow];
609                    byte[] samples = new byte[bytesPerRow * 2];
610                    while (processedRows < ROWS)
611                    {
612                            in.readFully(row);
613                            ArrayConverter.decodePacked4Bit(row, 0, samples, 0, row.length);
614                            image.putByteSamples(0, 0, y, COLUMNS, 1, samples, X1);
615                            y--;
616                            setProgress(processedRows, ROWS);
617                            processedRows++;
618                    }
619            }
620    
621            private void loadUncompressedPaletted8Stream() throws IOException
622            {
623                    Paletted8Image image = (Paletted8Image)getImage();
624                    int imageBytesPerRow = imageWidth;
625                    int bytesPerRow = imageBytesPerRow;
626                    int mod = bytesPerRow % 4;
627                    if (mod != 0)
628                    {
629                            bytesPerRow += 4 - mod;
630                    }
631                    int bottomRowsToSkip = imageHeight - 1 - getBoundsY2();
632                    int bytesToSkip = bottomRowsToSkip * bytesPerRow;
633                    while (bytesToSkip > 0)
634                    {
635                            int skipped = in.skipBytes(bytesToSkip);
636                            if (skipped > 0)
637                            {
638                                    bytesToSkip -= skipped;
639                            }
640                    }
641                    final int COLUMNS = getBoundsWidth();
642                    final int ROWS = getBoundsHeight();
643                    final int X1 = getBoundsX1();
644                    int y = image.getHeight() - 1;
645                    int processedRows = 0;
646                    byte[] row = new byte[bytesPerRow];
647                    while (processedRows < ROWS)
648                    {
649                            in.readFully(row);
650                            image.putByteSamples(0, 0, y, COLUMNS, 1, row, X1);
651                            y--;
652                            setProgress(processedRows, ROWS);
653                            processedRows++;
654                    }
655            }
656    
657            private void loadUncompressedRgb24Stream() throws IOException
658            {
659                    RGB24Image image = (RGB24Image)getImage();
660                    int imageBytesPerRow = imageWidth * 3;
661                    int bytesPerRow = imageBytesPerRow;
662                    int mod = bytesPerRow % 4;
663                    if (mod != 0)
664                    {
665                            bytesPerRow += 4 - mod;
666                    }
667                    int bottomRowsToSkip = imageHeight - 1 - getBoundsY2();
668                    int bytesToSkip = bottomRowsToSkip * bytesPerRow;
669                    while (bytesToSkip > 0)
670                    {
671                            int skipped = in.skipBytes(bytesToSkip);
672                            if (skipped > 0)
673                            {
674                                    bytesToSkip -= skipped;
675                            }
676                    }
677                    final int COLUMNS = getBoundsWidth();
678                    final int ROWS = getBoundsHeight();
679                    final int X1 = getBoundsX1();
680                    int y = image.getHeight() - 1;
681                    int processedRows = 0;
682                    byte[] row = new byte[bytesPerRow];
683                    byte[] samples = new byte[COLUMNS];
684                    while (processedRows < ROWS)
685                    {
686                            in.readFully(row);
687                            // copy red samples to array samples and store those samples
688                            for (int x = X1 * 3 + 2, i = 0; i < COLUMNS; x += 3, i++)
689                            {
690                                    samples[i] = row[x];
691                            }
692                            image.putByteSamples(RGBIndex.INDEX_RED, 0, y, COLUMNS, 1, samples, 0);
693                            // copy green samples to array samples and store those samples
694                            for (int x = X1 * 3 + 1, i = 0; i < COLUMNS; x += 3, i++)
695                            {
696                                    samples[i] = row[x];
697                            }
698                            image.putByteSamples(RGBIndex.INDEX_GREEN, 0, y, COLUMNS, 1, samples, 0);
699                            // copy blue samples to array samples and store those samples
700                            for (int x = X1 * 3, i = 0; i < COLUMNS; x += 3, i++)
701                            {
702                                    samples[i] = row[x];
703                            }
704                            image.putByteSamples(RGBIndex.INDEX_BLUE, 0, y, COLUMNS, 1, samples, 0);
705                            y--;
706                            setProgress(processedRows, ROWS);
707                            processedRows++;
708                    }
709            }
710    
711            private void loadUncompressedStream() throws
712                    IOException,
713                    OperationFailedException
714            {
715                    switch(colorDepth)
716                    {
717                            case(1):
718                            {
719                                    loadUncompressedBilevelStream();
720                                    break;
721                            }
722                            case(4):
723                            {
724                                    loadUncompressedPaletted4Stream();
725                                    break;
726                            }
727                            case(8):
728                            {
729                                    loadUncompressedPaletted8Stream();
730                                    break;
731                            }
732                            case(24):
733                            {
734                                    loadUncompressedRgb24Stream();
735                                    break;
736                            }
737                    }
738            }
739    
740            public void process() throws 
741                    MissingParameterException, 
742                    OperationFailedException
743            {
744                    initModeFromIOObjects();
745                    if (getMode() == CodecMode.LOAD)
746                    {
747                            load();
748                    }
749                    else
750                    {
751                            save();
752                    }
753            }
754    
755            private void save() throws
756                    MissingParameterException, 
757                    OperationFailedException, 
758                    UnsupportedTypeException
759            {
760                    // check parameters of this operation
761                    // 1 image to be saved
762                    // 1.1 is it available?
763                    PixelImage image = getImage();
764                    if (image == null)
765                    {
766                            throw new MissingParameterException("No image available.");
767                    }
768                    // 1.2 is it supported?
769                    if (!(image instanceof Paletted8Image ||
770                          image instanceof Gray8Image ||
771                          image instanceof BilevelImage ||
772                          image instanceof RGB24Image))
773                    {
774                            throw new UnsupportedTypeException("Unsupported image type: " + image.getClass().getName());
775                    }
776                    // 2 is output stream available?
777                    out = getOutputAsDataOutput();
778                    if (out == null)
779                    {
780                            throw new MissingParameterException("Output stream / random access file parameter missing.");
781                    }
782                    // now write the output stream
783                    try
784                    {
785                            writeStream();
786                    }
787                    catch (IOException ioe)
788                    {
789                            throw new OperationFailedException("I/O failure: " + ioe.toString());
790                    }
791            }
792    
793            public String suggestFileExtension(PixelImage image)
794            {
795                    return ".bmp";
796            }
797    
798            private void writeHeader(PixelImage image, int filesize, int offset, int numBits) throws IOException
799            {
800                    out.write(0x42); // 'B'
801                    out.write(0x4d); // 'M'
802                    writeInt(filesize);
803                    writeShort(0);
804                    writeShort(0);
805                    writeInt(offset);
806    
807                    writeInt(40); // BITMAP_INFO header length
808                    writeInt(getBoundsWidth());
809                    writeInt(getBoundsHeight());
810                    writeShort(1); // # of planes
811                    writeShort(numBits);
812                    writeInt(0); // compression (0 = none)
813                    writeInt(filesize - offset); // size of image data in bytes
814                    writeInt((int)(getDpiX() * (100f / 2.54f))); // horizontal resolution in dpi
815                    writeInt((int)(getDpiY() * (100f / 2.54f))); // vertical resolution in dpi
816                    writeInt(0); // # of used colors
817                    writeInt(0); // # of important colors
818            }
819    
820            // we can't use out.writeInt because we need little endian byte order
821            private void writeInt(int value) throws IOException
822            {
823                    out.write(value & 0xff);
824                    out.write((value >> 8) & 0xff);
825                    out.write((value >> 16) & 0xff);
826                    out.write((value >> 24) & 0xff);
827            }
828    
829            /**
830             * Write the palette associated with the image getImage().
831             * Required not only for image objects that implement PalettedImage
832             * but also for BilevelImage and Grayscale8Image.
833             * For the latter two the palette values must be explicitly written into the file.
834             */
835            private void writePalette() throws IOException
836            {
837                    PixelImage pi = getImage();
838                    if (pi == null)
839                    {
840                            return;
841                    }
842                    if (pi instanceof Paletted8Image)
843                    {
844                            // always write 256 entries; if there aren't enough
845                            // in the palette, fill it up to 256 with (0, 0, 0, 0)
846                            Palette palette = ((Paletted8Image)pi).getPalette();
847                            for (int i = 0; i < 256; i++)
848                            {
849                                    if (i < palette.getNumEntries())
850                                    {
851                                            out.write(palette.getSample(RGBIndex.INDEX_BLUE, i));
852                                            out.write(palette.getSample(RGBIndex.INDEX_GREEN, i));
853                                            out.write(palette.getSample(RGBIndex.INDEX_RED, i));
854                                            out.write(0);
855                                    }
856                                    else
857                                    {
858                                            out.writeInt(0); // writes four 0 bytes
859                                    }
860                            }
861                    }
862                    if (pi instanceof Gray8Image)
863                    {
864                            for (int i = 0; i < 256; i++)
865                            {
866                                    out.write(i);
867                                    out.write(i);
868                                    out.write(i);
869                                    out.write(0);
870                            }
871                    }
872                    if (pi instanceof BilevelImage)
873                    {
874                            for (int i = 0; i < 2; i++)
875                            {
876                                    out.write(i * 255);
877                                    out.write(i * 255);
878                                    out.write(i * 255);
879                                    out.write(0);
880                            }
881                    }
882            }
883    
884            // we can't use out.writeShort because we need little endian byte order
885            private void writeShort(int value) throws IOException
886            {
887                    out.write(value & 0xff);
888                    out.write((value >> 8) & 0xff);
889            }
890    
891            private void writeStream() throws IOException
892            {
893                    PixelImage image = getImage();
894                    setBoundsIfNecessary(image.getWidth(), image.getHeight());
895                    int width = getBoundsWidth();
896                    int height = getBoundsHeight();
897                    ByteChannelImage bcimg = null;
898                    BilevelImage bilevelImage = null;
899                    RGB24Image rgbimg = null;
900                    int bytesPerRow = 0;
901                    int offset = 54;
902                    int numBits = 0;
903                    int numPackedBytes = 0;
904                    if (image instanceof Paletted8Image ||
905                        image instanceof Gray8Image)
906                    {
907                            bcimg = (ByteChannelImage)image;
908                            bytesPerRow = width;
909                            offset += 1024;
910                            numBits = 8;
911                    }
912                    else
913                    if (image instanceof BilevelImage)
914                    {
915                            bilevelImage = (BilevelImage)image;
916                            numPackedBytes = (width + 7) / 8;
917                            bytesPerRow = numPackedBytes;
918                            offset += 8;
919                            numBits = 1;
920                    }
921                    else
922                    if (image instanceof RGB24Image)
923                    {
924                            rgbimg = (RGB24Image)image;
925                            bytesPerRow = width * 3;
926                            numBits = 24;
927                    }
928                    if ((bytesPerRow % 4) != 0)
929                    {
930                            bytesPerRow = ((bytesPerRow + 3) / 4) * 4;
931                    }
932                    int filesize = offset + bytesPerRow * height;
933                    writeHeader(image, filesize, offset, numBits);
934                    writePalette();
935                    byte[] row = new byte[bytesPerRow];
936                    final int X1 = getBoundsX1();
937                    for (int y = getBoundsY2(), processed = 0; processed < height; y--, processed++)
938                    {
939                            if (bilevelImage != null)
940                            {
941                                    bilevelImage.getPackedBytes(X1, y, width, row, 0, 0);
942                            }
943                            else
944                            if (bcimg != null)
945                            {
946                                    bcimg.getByteSamples(0, 0, y, width, 1, row, 0);
947                            }
948                            else
949                            if (rgbimg != null)
950                            {
951                                    int offs = 0;
952                                    for (int x = X1; x < X1 + width; x++)
953                                    {
954                                            row[offs++] = rgbimg.getByteSample(RGBIndex.INDEX_BLUE, x, y);
955                                            row[offs++] = rgbimg.getByteSample(RGBIndex.INDEX_GREEN, x, y);
956                                            row[offs++] = rgbimg.getByteSample(RGBIndex.INDEX_RED, x, y);
957                                    }
958                            }
959                            else
960                            {
961                                    // error
962                            }
963                            out.write(row);
964                            setProgress(processed, height);
965                            if (getAbort())
966                            {
967                                    break;
968                            }
969                    }
970                    close();
971            }
972    }