001    /*
002     * RASCodec
003     * 
004     * Copyright (c) 2000, 2001, 2002, 2003 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.Gray8Image;
019    import net.sourceforge.jiu.data.IntegerImage;
020    import net.sourceforge.jiu.data.MemoryPaletted8Image;
021    import net.sourceforge.jiu.data.MemoryRGB24Image;
022    import net.sourceforge.jiu.data.Palette;
023    import net.sourceforge.jiu.data.Paletted8Image;
024    import net.sourceforge.jiu.data.PixelImage;
025    import net.sourceforge.jiu.data.RGB24Image;
026    import net.sourceforge.jiu.data.RGBIndex;
027    import net.sourceforge.jiu.ops.OperationFailedException;
028    import net.sourceforge.jiu.ops.WrongParameterException;
029    import net.sourceforge.jiu.util.ArrayConverter;
030    
031    /**
032     * A codec to read and write Sun Raster (RAS) image files.
033     * The typical file extension for this format is <code>.ras</code>.
034     * <h3>Usage example</h3>
035     * This code snippet demonstrate how to read a RAS file.
036     * <pre>
037     * RASCodec codec = new RASCodec();
038     * codec.setFile("image.ras", CodecMode.LOAD);
039     * codec.process();
040     * PixelImage loadedImage = codec.getImage();
041     * </pre>
042     * <h3>Supported file types when reading</h3>
043     * Only uncompressed RAS files are read.
044     * Only 8 bit (gray and paletted) and 24 bit are supported when reading.
045     * <h3>Supported image types when writing</h3>
046     * Only {@link net.sourceforge.jiu.data.Paletted8Image} / uncompressed is supported when writing.
047     * <h3>Bounds</h3>
048     * The bounds concept of ImageCodec is supported so that you can load or save only part of an image.
049     * <h3>File format documentation</h3>
050     * This file format is documented as a man page <code>rasterfile(5)</code> on Sun Unix systems.
051     * That documentation can also be found online, e.g. at
052     * <a target="_top" href="http://www.doc.ic.ac.uk/~mac/manuals/sunos-manual-pages/sunos4/usr/man/man5/rasterfile.5.html">http://www.doc.ic.ac.uk/~mac/manuals/sunos-manual-pages/sunos4/usr/man/man5/rasterfile.5.html</a>.
053     * A <a target="_top" href="http://www.google.com/search?q=rasterfile%285%29&sourceid=opera&num=0">web search for rasterfile(5)</a>
054     * brings up other places as well.
055     *
056     * @author Marco Schmidt
057     */
058    public class RASCodec extends ImageCodec
059    {
060            private static final int RAS_MAGIC = 0x59a66a95;
061            private static final int COMPRESSION_NONE = 0x00000001;
062            private static final int RAS_HEADER_SIZE = 32;
063            private int width;
064            private int height;
065            private int depth;
066            private int length;
067            private int type;
068            private int mapType;
069            private int mapLength;
070            private int bytesPerRow;
071            private int paddingBytes;
072            private int numColors;
073            private DataInput in;
074            private DataOutput out;
075            private Palette palette;
076    
077            public String getFormatName()
078            {
079                    return "Sun Raster (RAS)";
080            }
081    
082            public String[] getMimeTypes()
083            {
084                    return new String[] {"image/x-ras"};
085            }
086    
087            public boolean isLoadingSupported()
088            {
089                    return true;
090            }
091    
092            public boolean isSavingSupported()
093            {
094                    return true;
095            }
096    
097            /**
098             * Loads an image from an RAS input stream.
099             * It is assumed that a stream was given to this codec using {@link #setInputStream(InputStream)}.
100             *
101             * @return the image as an instance of a class that implements {@link IntegerImage}
102             * @throws InvalidFileStructureException if the input stream is corrupt
103             * @throws java.io.IOException if there were problems reading from the input stream
104             * @throws UnsupportedTypeException if an unsupported flavor of the RAS format is encountered
105             * @throws WrongFileFormatException if this is not a valid RAS stream
106             */
107            private void load() throws 
108                    IOException,
109                    OperationFailedException
110            {
111                    in = getInputAsDataInput();
112                    readHeader();
113                    readImage();
114            }
115    
116            public void process() throws OperationFailedException
117            {
118                    try
119                    {
120                            initModeFromIOObjects();
121                            if (getMode() == CodecMode.LOAD)
122                            {
123                                    load();
124                            }
125                            else
126                            if (getMode() == CodecMode.SAVE)
127                            {
128                                    save();
129                            }
130                            else
131                            {
132                                    throw new WrongParameterException("Could find neither objects for loading nor for saving.");
133                            }
134                    }
135                    catch (IOException ioe)
136                    {
137                            throw new OperationFailedException("I/O error in RAS codec: " + ioe.toString());
138                    }
139            }
140    
141            private void readHeader() throws 
142                    InvalidFileStructureException,
143                    UnsupportedTypeException,
144                    WrongFileFormatException,
145                    WrongParameterException,
146                    java.io.IOException
147            {
148                    byte[] header = new byte[RAS_HEADER_SIZE];
149                    in.readFully(header);
150                    int magic = ArrayConverter.getIntBE(header, 0);
151                    if (magic != RAS_MAGIC)
152                    {
153                            throw new WrongFileFormatException("This stream is not a valid " +
154                                    "Sun RAS stream (bad magic: " + Integer.toHexString(magic) +
155                                    " instead of " + Integer.toHexString(RAS_MAGIC));
156                    }
157                    width = ArrayConverter.getIntBE(header, 4);
158                    height = ArrayConverter.getIntBE(header, 8);
159                    if (width < 1 || height < 1)
160                    {
161                            throw new InvalidFileStructureException("Width and height must both " +
162                                    "be larger than zero; found width=" + width + ", height=" + 
163                                    height + ".");
164                    }
165                    setBoundsIfNecessary(width, height);
166                    checkBounds(width, height);
167                    depth = ArrayConverter.getIntBE(header, 12);
168                    switch (depth)
169                    {
170                            case(1):
171                            {
172                                    bytesPerRow = (width + 7) / 8;
173                                    break;
174                            }
175                            case(8):
176                            {
177                                    bytesPerRow = width;
178                                    break;
179                            }
180                            case(24):
181                            {
182                                    bytesPerRow = width * 3;
183                                    break;
184                            }
185                            default:
186                            {
187                                    throw new UnsupportedTypeException("Depths other than 1, 8 and 24 " +
188                                            "unsupported when reading RAS stream; found " + depth);
189                            }
190                    }
191                    paddingBytes = (bytesPerRow % 2);
192                    numColors = 1 << depth;
193                    //length = ArrayConverter.getIntBE(header, 16);
194                    type = ArrayConverter.getIntBE(header, 20);
195                    if (type != COMPRESSION_NONE)
196                    {
197                            throw new UnsupportedTypeException("Only uncompressed " +
198                                    "RAS streams are read; found " + type);
199                    }
200                    mapType = ArrayConverter.getIntBE(header, 24);
201                    mapLength = ArrayConverter.getIntBE(header, 28);
202                    if (mapLength != 0)
203                    {
204                            if (depth != 8)
205                            {
206                                    throw new UnsupportedTypeException("Cannot handle Sun RAS " +
207                                            "input streams with color maps and a depth other than " +
208                                            "8 (found " + depth + ").");
209                            }
210                            if (mapLength != 768)
211                            {
212                                    throw new UnsupportedTypeException("Cannot handle Sun RAS " +
213                                            "input streams with color maps of a length different " +
214                                            "than 768; found " + mapLength);
215                            }
216                            if (mapType != 1)
217                            {
218                                    throw new UnsupportedTypeException("Cannot handle Sun RAS " +
219                                            "input streams with color maps of a type other than " +
220                                            "1; found " + mapType);
221                            }
222                            palette = readPalette();
223                    }
224                    else
225                    {
226                            palette = null;
227                    }
228            }
229    
230            private IntegerImage readImage() throws 
231                    InvalidFileStructureException,
232                    java.io.IOException
233            {
234                    RGB24Image rgb24Image = null;
235                    Paletted8Image paletted8Image = null;
236                    IntegerImage result = null;
237                    int numChannels = 1;
238                    int bytesPerRow = 0;
239                    switch(depth)
240                    {
241                            case(8):
242                            {
243                                    paletted8Image = new MemoryPaletted8Image(width, height, palette);
244                                    result = paletted8Image;
245                                    numChannels = 1;
246                                    bytesPerRow = width;
247                                    break;
248                            }
249                            case(24):
250                            {
251                                    rgb24Image = new MemoryRGB24Image(width, height);
252                                    result = rgb24Image;
253                                    numChannels = 3;
254                                    bytesPerRow = width;
255                                    break;
256                            }
257                    }
258                    setImage(result);
259                    byte[][] buffer = new byte[numChannels][];
260                    for (int i = 0; i < numChannels; i++)
261                    {
262                            buffer[i] = new byte[bytesPerRow];
263                    }
264                    for (int y = 0, destY = -getBoundsY1(); destY <= getBoundsY2(); y++, destY++)
265                    {
266                            if (rgb24Image != null)
267                            {
268                                    for (int x = 0; x < width; x++)
269                                    {
270                                            buffer[RGBIndex.INDEX_BLUE][x] = in.readByte();
271                                            buffer[RGBIndex.INDEX_GREEN][x] = in.readByte();
272                                            buffer[RGBIndex.INDEX_RED][x] = in.readByte();
273                                    }
274                                    rgb24Image.putByteSamples(RGBIndex.INDEX_RED, 0, destY, getBoundsWidth(), 1, buffer[0], getBoundsX1());
275                                    rgb24Image.putByteSamples(RGBIndex.INDEX_GREEN, 0, destY, getBoundsWidth(), 1, buffer[1], getBoundsX1());
276                                    rgb24Image.putByteSamples(RGBIndex.INDEX_BLUE, 0, destY, getBoundsWidth(), 1, buffer[2], getBoundsX1());
277                            }
278                            else
279                            if (paletted8Image != null)
280                            {
281                                    in.readFully(buffer[0], 0, width);
282                                    paletted8Image.putByteSamples(0, 0, destY, getBoundsWidth(), 1, buffer[0], getBoundsX1());
283                            }
284                            if (in.skipBytes(paddingBytes) != paddingBytes)
285                            {
286                                    throw new InvalidFileStructureException("Could not skip " +
287                                            "byte after row " + y + ".");
288                            }
289                            setProgress(y, getBoundsY2() + 1);
290                    }
291                    return result;
292            }
293    
294            private Palette readPalette() throws 
295                    InvalidFileStructureException,
296                    java.io.IOException
297            {
298                    Palette result = new Palette(256, 255);
299                    for (int channel = 0; channel < 3; channel++)
300                    {
301                            int channelIndex = -1;
302                            switch(channel)
303                            {
304                                    case(0):
305                                    {
306                                            channelIndex = Palette.INDEX_RED;
307                                            break;
308                                    }
309                                    case(1):
310                                    {
311                                            channelIndex = Palette.INDEX_GREEN;
312                                            break;
313                                    }
314                                    case(2):
315                                    {
316                                            channelIndex = Palette.INDEX_BLUE;
317                                            break;
318                                    }
319                            }
320                            for (int i = 0; i < numColors; i++)
321                            {
322                                    int value = in.readUnsignedByte();
323                                    if (value == -1)
324                                    {
325                                            throw new InvalidFileStructureException("Unexpected end " +
326                                                    "of file when reading Sun RAS palette.");
327                                    }
328                                    result.putSample(channelIndex, i, value);
329                            }
330                    }
331                    return result;
332            }
333    
334            private void save() throws 
335                    IOException,
336                    UnsupportedTypeException,
337                    WrongParameterException
338            {
339                    PixelImage image = getImage();
340                    if (image == null || (!(image instanceof Paletted8Image)))
341                    {
342                            throw new UnsupportedTypeException("Must have non-null image that is a Paletted8Image.");
343                    }
344                    saveHeader(image);
345                    if (image instanceof Paletted8Image)
346                    {
347                            saveData((Paletted8Image)image);
348                    }
349            }
350    
351            private void saveData(Paletted8Image image) throws IOException
352            {
353                    byte[] row = new byte[getBoundsWidth()];
354                    for (int y1 = 0, y2 = getBoundsY1(); y1 < getBoundsHeight(); y1++, y2++)
355                    {
356                            image.getByteSamples(0, getBoundsX1(), y2, row.length, 1, row, 0);
357                            out.write(row);
358                            int num = paddingBytes;
359                            while (num-- > 0)
360                            {
361                                    out.write(0);
362                            }
363                            setProgress(y1, getBoundsHeight());
364                    }
365            }
366    
367            private void saveHeader(PixelImage image) throws 
368                    IOException,
369                    UnsupportedTypeException,
370                    WrongParameterException
371                    
372            {
373                    setBoundsIfNecessary(width, height);
374                    checkBounds(width, height);
375                    out.writeInt(RAS_MAGIC);
376                    int width = getBoundsWidth();
377                    out.writeInt(width);
378                    int height = getBoundsHeight();
379                    out.writeInt(height);
380                    if (image instanceof BilevelImage)
381                    {
382                            depth = 1;
383                            bytesPerRow = (width + 7) / 8;
384                    }
385                    else
386                    if (image instanceof Gray8Image ||
387                        image instanceof Paletted8Image)
388                    {
389                            depth = 8;
390                            bytesPerRow = width;
391                    }
392                    else
393                    if (image instanceof RGB24Image)
394                    {
395                            bytesPerRow = width * 3;
396                            depth = 24;
397                    }
398                    else
399                    {
400                            throw new UnsupportedTypeException("Cannot store image types " +
401                                    "other than bilevel, gray8, paletted8 and RGB24.");
402                    }
403                    out.writeInt(depth);
404                    paddingBytes = (bytesPerRow % 2);
405                    numColors = 1 << depth;
406                    length = bytesPerRow * getBoundsHeight();
407                    out.writeInt(length); // length
408                    out.writeInt(COMPRESSION_NONE); // type
409                    mapType = 1;
410                    mapLength = 0;
411                    if (image instanceof Paletted8Image)
412                    {
413                            mapLength = 768;
414                    }
415                    out.writeInt(mapType); 
416                    out.writeInt(mapLength);
417                    if (image instanceof Paletted8Image)
418                    {
419                            Paletted8Image pal = (Paletted8Image)image;
420                            savePalette(pal.getPalette());
421                    }
422            }
423    
424            private void savePalette(Palette palette) throws java.io.IOException
425            {
426                    int numEntries = palette.getNumEntries();
427                    for (int channel = 0; channel < 3; channel++)
428                    {
429                            int channelIndex = -1;
430                            switch(channel)
431                            {
432                                    case(0):
433                                    {
434                                            channelIndex = Palette.INDEX_RED;
435                                            break;
436                                    }
437                                    case(1):
438                                    {
439                                            channelIndex = Palette.INDEX_GREEN;
440                                            break;
441                                    }
442                                    case(2):
443                                    {
444                                            channelIndex = Palette.INDEX_BLUE;
445                                            break;
446                                    }
447                            }
448                            for (int i = 0; i < 256; i++)
449                            {
450                                    int value = 0;
451                                    if (i < numEntries)
452                                    {
453                                            value = palette.getSample(channelIndex, i);
454                                    }
455                                    out.write(value);
456                            }
457                    }
458            }
459    
460            public String suggestFileExtension(PixelImage image)
461            {
462                    return ".ras";
463            }
464    }