/* * ImageFile.java * (FScape) * * Copyright (c) 2001-2016 Hanns Holger Rutz. All rights reserved. * * This software is published under the GNU General Public License v3+ * * * For further information, please contact Hanns Holger Rutz at * contact@sciss.de */ package de.sciss.fscape.io; import java.io.EOFException; import java.io.File; import java.io.IOException; import java.io.UnsupportedEncodingException; public class ImageFile extends GenericFile { // -------- public variables -------- public static final String ERR_PARALLEL = "Colour planes not interleaved"; public static final String ERR_COMPRESSED = "Unsupported compression"; // -------- private variables -------- private boolean suckyPCdata = false; // true for PC-style least-signifcant-byte first shit private boolean invert = false; // TIFF-grayscale: highest value is black private long stripOffsetOffs = 0L; private int stripOffsetSize; // private long stripCountOffs = 0L; // private int stripCountSize; private int rowsPerStrip; private int numStrips; private int bytesPerRow; private int stripNum = -1; // which is the current strip # private int stripLeft = 0; // number of bytes left in the strip private byte[] buf = null; private int bufSize; private int bufOffset; protected ImageStream stream = null; // TIFF essential Tags protected static final int NEWSUBTYPE_TAG = 0x00FE; protected static final int SUBTYPE_TAG = 0x00FF; protected static final int WIDTH_TAG = 0x0100; protected static final int HEIGHT_TAG = 0x0101; protected static final int BITSPERSMP_TAG = 0x0102; // bits per sample protected static final int CMPTYPE_TAG = 0x0103; // compression protected static final int PHOTOMETR_TAG = 0x0106; // whether grayscale or rgb protected static final int DESCR_TAG = 0x010E; // where we can keep track of the spect origin protected static final int STRIPOFFS_TAG = 0x0111; // where the image data is stored protected static final int SMPPERPIXEL_TAG = 0x0115; // color depth protected static final int ROWSPERSTRIP_TAG = 0x0116; protected static final int STRIPCOUNT_TAG = 0x0117; protected static final int XRES_TAG = 0x011A; protected static final int YRES_TAG = 0x011B; protected static final int PLANE_TAG = 0x011C; // rgb or seperate planes protected static final int RESUNIT_TAG = 0x0128; // inches, cm etc. protected static final int CMPTYPE_NONE = 0x0001; // no compression protected static final int CMPTYPE_LZW = 0x0005; protected static final int NEWSUBTYPE_THUMB = 0x0001; // reduced version protected static final int NEWSUBTYPE_TRANSP= 0x0004; // transparent mask protected static final int SUBTYPE_THUMB = 0x0002; // reduced version (old tag) protected static final int PHOTOMETR_GRAYINV= 0x0000; // grayscale inverted protected static final int PHOTOMETR_GRAY = 0x0001; // normal grayscale protected static final int PHOTOMETR_RGB = 0x0002; protected static final int PLANE_SERIAL = 0x0001; // r1g1b2, r2g2b2 etc. protected static final int PLANE_PARALLEL = 0x0002; // r, g, b got separate planes protected static final int RESUNIT_NONE = ImageStream.RES_NONE; // 0x0001; protected static final int RESUNIT_INCH = ImageStream.RES_INCH; // 0x0002; protected static final int RESUNIT_CM = ImageStream.RES_CM; // 0x0003; // -------- public methods -------- /** * Datei, die Imagedaten enthaelt bzw. enthalten soll, oeffnen * * @param imageF entsprechende Datei * @param mode MODE_INPUT zum Lesen, MODE_OUTPUT zum Schreiben */ public ImageFile( File imageF, int mode ) throws IOException { super( imageF, (mode & ~MODE_TYPEMASK) | MODE_TIFF ); } public ImageFile( String fname, int mode ) throws IOException { this( new File( fname ), mode ); } /** * erzeugt einen ImageStream, in den der Header der Datei * uebertragen wird * KEINE AENDERUNGEN AM SOUNDSTREAM VORNEHMEN! */ public ImageStream initReader() throws IOException { if( stream == null ) { stream = new ImageStream(); readHeader(); bufSize = 131072; // bufSize = getDefaultBufSize(); bufSize -= bufSize % bytesPerRow; buf = new byte[ Math.max( bytesPerRow, bufSize )]; } stripNum = -1; stripLeft = 0; bufOffset = bufSize; // "empty" return stream; } /** * Meldet einen ImageStream zum Schreiben in das File an; */ public void initWriter( ImageStream strm ) throws IOException { stream = strm; bytesPerRow = strm.width * strm.smpPerPixel * ((strm.bitsPerSmp + 7) >> 3); suckyPCdata = false; invert = false; rowsPerStrip = strm.height; numStrips = 1; bufSize = 131072; // bufSize = getDefaultBufSize(); bufSize -= bufSize % bytesPerRow; buf = new byte[ Math.max( bytesPerRow, bufSize )]; bufOffset = 0; // "empty" writeHeader(); } /** * Erzeugt ein fuer diese Objekt zum Lesen/Schreiben geeignetes Array von Bytes */ public byte[] allocRow() { return new byte[ stream.width * stream.smpPerPixel * ((stream.bitsPerSmp + 7) >> 3) ]; } protected void seekNewStrip() throws IOException { stripNum++; if( stripNum >= numStrips ) throw new EOFException(); if( stripOffsetSize == TIFFentry.LONG ) { seek( stripOffsetOffs + (stripNum << 2) ); seek( readUniversalInt() ); } else { seek( stripOffsetOffs + stripNum << 1 ); seek( readUniversalUShort() ); } stripLeft = bytesPerRow * rowsPerStrip; } /** * Liest einen Zeile aus der Datei ein * * @param data sollte mit allocRow() beschafft worden sein! */ public void readRow( byte[] data ) throws IOException { int i, num; byte b; // fill buffer if( bufOffset >= bufSize ) { // buffer empty? physically load new rows if( stripLeft == 0 ) { seekNewStrip(); } num = Math.min( bufSize, stripLeft ); readFully( buf, bufSize - num, num ); bufOffset = bufSize - num; stripLeft -= num; if( suckyPCdata && (stream.bitsPerSmp == 16) ) { for( i = 0; i < bufSize; i += 2 ) { b = data[ i ]; data[ i ] = data[ i + 1 ]; data[ i+1 ] = b; } } } // copy data System.arraycopy( buf, bufOffset, data, 0, data.length ); bufOffset += data.length; stream.rowsRead++; if( invert ) { for( i = 0; i < data.length; i++ ) { data[ i ] = (byte) ~data[ i ]; } } } /** * Schreibt einen Zeile in die Datei * * @param data sollte mit allocRow() beschafft worden sein! */ public void writeRow( byte[] data ) throws IOException { // flush buffer if( bufOffset >= bufSize ) { // buffer full? physically save rows write( buf ); bufOffset = 0; } // copy data System.arraycopy( data, 0, buf, bufOffset, data.length ); bufOffset += data.length; stream.rowsWritten++; } /** * Datei schliessen */ public void close() throws IOException { stream = null; int bufOffTmp = bufOffset; bufOffset = 0; byte bufTmp[] = buf; buf = null; if( (mode & MODE_FILEMASK) == MODE_OUTPUT ) { if( (bufTmp != null) && (bufOffTmp > 0) ) { // ...then flush buffer write( bufTmp, 0, bufOffTmp ); } } super.close(); } /** * Format string besorgen */ public String getFormat() throws IOException { ImageStream tmpStream; tmpStream = stream; if( tmpStream == null ) { tmpStream = initReader(); } return( ImageStream.getFormat( tmpStream )); } // -------- private methods -------- /* * Datei Header einlesen; bei nicht unterstuetzen Formaten wird eine IOException * vom Typ UnsupportedEncodingException ausgeloest */ private void readHeader() throws IOException { int magic; long oldPos; int essentials; int offset; int i; // TIFF: int entries; boolean gray; TIFFentry entry = new TIFFentry(); seek( 0L ); magic = readInt(); switch( magic ) { case TIFF_MAC_MAGIC: suckyPCdata = false; break; case TIFF_IBM_MAGIC: suckyPCdata = true; break; default: throw new UnsupportedEncodingException( ERR_ILLEGALFILE ); } mode = (mode & ~MODE_TYPEMASK) | MODE_TIFF; // only supported one so far do { // go through all the IFD's offset = readUniversalInt(); if( offset == 0 ) { throw new UnsupportedEncodingException( ERR_MISSINGDATA ); } seek( offset ); // stripCountOffs = 0L; // optional invert = false; gray = true; stream.hRes = 1.0f / 72.0f; stream.hRes = 1.0f / 72.0f; stream.resUnit = ImageStream.RES_INCH; // TIFF-default stream.bitsPerSmp = 1; // TIFF-default, will be rejected! stream.smpPerPixel = 1; // TIFF-default entries = readUniversalUShort(); essentials = 5; IFD: for( i = 0; i < entries; i++ ) { readTIFFentry( entry ); switch( entry.tag ) { case NEWSUBTYPE_TAG: if( (entry.value == NEWSUBTYPE_THUMB) || (entry.value == NEWSUBTYPE_TRANSP) ) { break IFD; // can't use this image } break; case SUBTYPE_TAG: if( entry.value == SUBTYPE_THUMB ) { break IFD; // can't use this image } break; case CMPTYPE_TAG: if( entry.value != CMPTYPE_NONE ) { throw new UnsupportedEncodingException( ERR_COMPRESSED ); } break; case WIDTH_TAG: essentials--; stream.width = entry.value; break; case HEIGHT_TAG: essentials--; stream.height = entry.value; break; case XRES_TAG: stream.hRes = Float.intBitsToFloat( entry.value ); break; case YRES_TAG: stream.vRes = Float.intBitsToFloat( entry.value ); break; case RESUNIT_TAG: stream.resUnit = entry.value & 0x03; break; case BITSPERSMP_TAG: if( entry.count == 1 ) { stream.bitsPerSmp = entry.value; } else { oldPos = getFilePointer(); seek( entry.value ); for( int j = 0, k = 0; j < entry.count; j++, k = stream.bitsPerSmp ) { stream.bitsPerSmp = readUniversalUShort(); if( (j > 0) & (k != stream.bitsPerSmp) ) { // cannot mix different depths throw new UnsupportedEncodingException( ERR_UNSUPPORTED ); } } seek( oldPos ); } // only 8-Bit and 16-Bit if( (stream.bitsPerSmp != 8) && (stream.bitsPerSmp != 16) ) { throw new UnsupportedEncodingException( ERR_UNSUPPORTED ); } break; case SMPPERPIXEL_TAG: if( (entry.value < 1) || (gray && (entry.value != 1)) ) { // grayscale must be 1 smp throw new UnsupportedEncodingException( ERR_CORRUPTED ); } stream.smpPerPixel = entry.value; break; case PHOTOMETR_TAG: essentials--; switch( entry.value ) { case PHOTOMETR_GRAY: gray = true; invert = false; break; case PHOTOMETR_GRAYINV: gray = true; invert = true; break; case PHOTOMETR_RGB: gray = false; invert = false; break; default: throw new UnsupportedEncodingException( ERR_UNSUPPORTED ); } break; case DESCR_TAG: stream.descr = readTIFFstring( entry ); break; case STRIPOFFS_TAG: essentials--; if( entry.count == 1 ) { stripOffsetOffs = getFilePointer() - 4; stripOffsetSize = TIFFentry.LONG; } else { stripOffsetOffs = entry.value; stripOffsetSize = entry.type; } break; case ROWSPERSTRIP_TAG: essentials--; rowsPerStrip = entry.value; numStrips = (stream.height + rowsPerStrip - 1) / rowsPerStrip; bytesPerRow = stream.width * stream.smpPerPixel * ((stream.bitsPerSmp + 7) >> 3); break; case STRIPCOUNT_TAG: if( entry.count == 1 ) { // stripCountOffs = getFilePointer() - 4; // stripCountSize = TIFFentry.LONG; } else { // stripCountOffs = entry.value; // stripCountSize = entry.type; } break; case PLANE_TAG: if( entry.value == PLANE_PARALLEL ) { throw new UnsupportedEncodingException( ERR_PARALLEL ); } else if( entry.value != PLANE_SERIAL ) { throw new UnsupportedEncodingException( ERR_UNSUPPORTED ); } break; default: break; } } // for( IFD-entries ) if( (i == entries) && (essentials > 0) ) { throw new UnsupportedEncodingException( ERR_MISSINGDATA ); } if( i < entries ) { // means this subfile was useless, so go to the next one seek( getFilePointer() + 12 * (entries - i) ); } } while( essentials > 0 ); // until we found a completely useable subfile } /* * Unsigned Short lesen, ggf. PC-Codierung anwenden */ protected int readUniversalUShort() throws IOException { if( !suckyPCdata ) { return readUnsignedShort(); } else { int i = readUnsignedShort(); return( (i & 0x00FF) << 8 | ((i & 0xFF00) >> 8) ); } } /* * 4-Byte Integer lesen, ggf. PC-Codierung anwenden */ protected int readUniversalInt() throws IOException { if( !suckyPCdata ) { return readInt(); } else { int i = readInt(); return( (int) ((((long) i & 0xFF000000) >> 24) | ((i & 0x00FF0000) >> 8) | ((i & 0x0000FF00) << 8) | ((i & 0x000000FF) << 24)) ); } } /* * TIFF-Entry einlesen; Typen-Konvertierung erfolgt automatisch * * einzelne Bytes und Shorts werden rechtsbuendig formattiert * einzelne "Rational"-Typen werden in ein Float konvertiert (zurueck ueber Float.intBitsToFloat()) */ protected void readTIFFentry( TIFFentry entry ) throws IOException { entry.tag = readUniversalUShort(); entry.type = readUniversalUShort(); entry.count = readUniversalInt(); entry.value = readUniversalInt(); switch( entry.type ) { case TIFFentry.BYTE: if( entry.count == 1 ) { entry.value = (entry.value >> 24) & 0x000000FF; } break; case TIFFentry.SHORT: if( entry.count == 1 ) { entry.value = (entry.value >> 16) & 0x0000FFFF; } break; case TIFFentry.RATIONAL: if( entry.count == 1 ) { long oldPos = getFilePointer(); seek( entry.value ); long val = readLong(); seek( oldPos ); entry.value = Float.floatToIntBits( (float) (val >> 32) / (float) (val & 0xFFFFFFFF) ); } break; default: break; } } /* * TIFF-Entry schreiben */ protected void writeTIFFentry( int tag, int type, int count, int val ) throws IOException { writeShort( tag ); writeShort( type ); writeInt( count ); if( count == 1 ) { if( type == TIFFentry.SHORT ) { val <<= 16; } else if( type == TIFFentry.BYTE ) { val <<= 24; } } writeInt( val ); } /* * ASCII-String, der durch TIFFentry markiert ist lesen */ protected String readTIFFstring( TIFFentry entry ) throws IOException { byte ascii[] = new byte[ entry.count - 1 ]; long oldPos = getFilePointer(); seek( entry.value ); readFully( ascii ); seek( oldPos ); return new String( ascii ); } /* * Datei Header schreiben */ private void writeHeader() throws IOException { int entries = (stream.smpPerPixel == 1) ? 13 : 14; // rgb has photometric interpret. String descr = (stream.descr != null) ? stream.descr : ""; int offset = 24 + 6 + (descr.length() + 2) & ~1; seek( 0L ); writeInt( TIFF_MAC_MAGIC ); writeInt( offset ); // offset writeInt( (int) (stream.hRes * 10000f) ); writeInt( 10000 ); writeInt( (int) (stream.vRes * 10000f) ); writeInt( 10000 ); writeShort( stream.bitsPerSmp ); // auch grayscale, um gleichen offset zu haben writeShort( stream.bitsPerSmp ); writeShort( stream.bitsPerSmp ); if( descr.length() > 0 ) { writeBytes( descr ); } if( (getFilePointer() & 1) == 0 ) { writeShort( 0 ); } else { writeByte( 0 ); } writeShort( entries ); for( int tag = NEWSUBTYPE_TAG; tag <= RESUNIT_TAG; tag++ ) { switch( tag ) { case NEWSUBTYPE_TAG: writeTIFFentry( tag, TIFFentry.LONG, 1, 0 ); break; case WIDTH_TAG: writeTIFFentry( tag, TIFFentry.LONG, 1, stream.width ); break; case HEIGHT_TAG: case ROWSPERSTRIP_TAG: writeTIFFentry( tag, TIFFentry.LONG, 1, stream.height ); break; case STRIPOFFS_TAG: writeTIFFentry( tag, TIFFentry.LONG, 1, offset + entries * 12 + 4 ); break; case STRIPCOUNT_TAG: writeTIFFentry( tag, TIFFentry.LONG, 1, bytesPerRow * stream.height ); break; case XRES_TAG: writeTIFFentry( tag, TIFFentry.RATIONAL, 1, 8 ); break; case YRES_TAG: writeTIFFentry( tag, TIFFentry.RATIONAL, 1, 16 ); break; case RESUNIT_TAG: writeTIFFentry( tag, TIFFentry.SHORT, 1, stream.resUnit ); break; case SMPPERPIXEL_TAG: writeTIFFentry( tag, TIFFentry.SHORT, 1, stream.smpPerPixel ); break; case BITSPERSMP_TAG: writeTIFFentry( tag, TIFFentry.SHORT, stream.smpPerPixel, (stream.smpPerPixel == 1) ? stream.bitsPerSmp : 24 ); // ggf. offset break; case CMPTYPE_TAG: writeTIFFentry( tag, TIFFentry.SHORT, 1, CMPTYPE_NONE ); break; case PHOTOMETR_TAG: writeTIFFentry( tag, TIFFentry.SHORT, 1, (stream.smpPerPixel == 1) ? PHOTOMETR_GRAY : PHOTOMETR_RGB ); break; case PLANE_TAG: if( stream.smpPerPixel == 3 ) { writeTIFFentry( tag, TIFFentry.SHORT, 1, PLANE_SERIAL ); } break; case DESCR_TAG: if( descr.length() > 0 ) { writeTIFFentry( tag, TIFFentry.ASCII, descr.length() + 1, 30 ); } break; default: break; } } writeShort( 0 ); // no more sub images } } // class ImageFile class TIFFentry { int tag; int type; int count; int value; static final int BYTE = 1; static final int ASCII = 2; static final int SHORT = 3; static final int LONG = 4; static final int RATIONAL = 5; } // class TIFFentry