package uk.co.mmscomputing.imageio.tiff; import java.io.*; import java.awt.image.*; import javax.imageio.stream.*; import uk.co.mmscomputing.io.BitSwapInputStream; import uk.co.mmscomputing.io.LZWInputStream; import uk.co.mmscomputing.io.PackBitsInputStream; import uk.co.mmscomputing.io.ModHuffmanInputStream; import uk.co.mmscomputing.io.RLEBitInputStream; import uk.co.mmscomputing.io.InvertedInputStream; import uk.co.mmscomputing.io.IntFilterInputStream; import uk.co.mmscomputing.io.RGBInputStream; import uk.co.mmscomputing.imageio.jpeg.*; // [1] Adobe TIFF6.pdf /* Read: Black & White : base line : no compression, modified huffman, packbits + lzw [1] p.57ff Gray 4 & 8 bit: base line : no compression, packbits, jpeg(7)(8bit grayscale) + lzw [1] p.57ff Palette Color 4 & 8 bit: base line : no compression, packbits + lzw [1] p.57ff RGB : 888 rgb images : no compression, packbits, lzw, jpeg(7) YCbCr : 888 YCbCr images : no compression, packbits, lzw */ public class TIFFBaselineFactory implements TIFFConstants{ static final private String cn="uk.co.mmscomputing.imageio.tiff.TIFFBaselineFactory"; static final private String email="\nPlease send this tiff file to mm@mms-computing.co.uk."; static final private IndexColorModel bwwhiteiszero; static final private IndexColorModel bwblackiszero; static final private IndexColorModel graywhiteiszero; static final private IndexColorModel grayblackiszero; static public BufferedImage readImage(ImageInputStream in,IFD ifd)throws IOException{ int phmi=ifd.getPhotometricInterpretation(); int spp =ifd.getSamplesPerPixel(); int bps =ifd.getBitsPerSample(0); int pc =ifd.getPlanarConfiguration(); if(pc!=1){ System.out.println("9\b"+cn+".readImage:\n\tDo only support Planar Configuration 1."+email); return null; } if(ifd.getSampleFormat()!=1){ System.out.println("9\b"+cn+".readImage:\n\tDo not support sample format other than unsigned integer."+email); return null; } // todo: check for tile width & length => don't support tiles // System.err.println(" phmi = "+phmi+" bps = "+bps); switch(phmi){ case WhiteIsZero: if(spp==1){ switch(bps){ case 1: return readBWImage(in,ifd,bwwhiteiszero); case 4: return read4bitImage(in,ifd,graywhiteiszero); case 8: return readGray8bitImage(in,ifd,true); } } break; case BlackIsZero: if(spp==1){ switch(bps){ case 1: return readBWImage(in,ifd,bwblackiszero); case 4: return read4bitImage(in,ifd,grayblackiszero); case 8: return readGray8bitImage(in,ifd,false); } } break; case RGB: if(spp>=3){ try{ for(int i=0;i<spp;i++){ if(ifd.getBitsPerSample(i)!=8){ System.out.println("9\b"+cn+".readImage:\n\tDo only support 8 bits samples."); break; } } }catch(ArrayIndexOutOfBoundsException aioobe){ System.out.println("9\b"+cn+".readImage:\n\tBitsPerSample["+aioobe.getMessage()+"] array to short SamplesPerPixel = "+spp+" assume 8 bits."); } // todo: ReferenceBlackWhite return readRGBImage(in,ifd,spp); }else{ System.out.println(cn+".readImage:\n\tInvalid tiff file, less than three samples per pixel in rgb file."); } break; case PaletteColor: if(spp==1){ // spp must be one [1] p.37 switch(bps){ case 4: return read4bitImage(in,ifd,ifd.getColorModel()); case 8: return read8bitImage(in,ifd); } } break; case TransparencyMask: if((spp==1)&&(ifd.getBitsPerSample(0)!=1)){ // spp and bps must be one [1] p.37 // phmi must have been set System.out.println("9\b"+cn+".readImage:\n\tDo not support transparency mask."+email); } break; case CMYK: if(spp==4){ for(int i=0;i<spp;i++){ if(ifd.getBitsPerSample(i)!=8){ System.out.println(cn+".readImage:\n\tUnsupported tiff file. Support only 8 bit samples in CMYK file."); break; } } return readCMYKImage(in,ifd); }else{ System.out.println(cn+".readImage:\n\tUnsupported tiff file, Support only four samples per pixel CMYK."); } break; case YCbCr: if(spp==3){ for(int i=0;i<spp;i++){ if(ifd.getBitsPerSample(i)!=8){ System.out.println(cn+".readImage:\n\tInvalid TIFF file: TIFF does only support 8 bit samples in YCbCr file."); break; } } return readYCbCrImage(in,ifd); }else{ System.out.println(cn+".readImage:\n\tInvalid TIFF file: a TIFF YCbCr file needs to have three samples per pixel."); } break; case CIELab: break; } System.out.println("9\b"+cn+".readImage:\n\tNot a Baseline TIFF File. Invalid or unsupported parameters."+email); return null; } static private BufferedImage readBWImage(ImageInputStream in,IFD ifd,IndexColorModel cm)throws IOException{ BufferedImage image=null; try{ int width =ifd.getWidth(); int height =ifd.getHeight(); int cmp =ifd.getCompression(); image=new BufferedImage(width,height,BufferedImage.TYPE_BYTE_BINARY,cm); WritableRaster raster=image.getRaster(); DataBufferByte buffer=(DataBufferByte)raster.getDataBuffer(); byte[] imgdata=(byte[])buffer.getData(); long[] offsets =ifd.getStripOffsets(); long[] counts =ifd.getStripByteCounts(); int rps =ifd.getRowsPerStrip(); int predictor=ifd.getPredictor(); int offset=0,mbps=((width+7)>>3)*rps,max=((width+7)>>3)*height; for(int i=0;i<offsets.length;i++){ in.seek(offsets[i]); byte[] data=new byte[(int)counts[i]]; in.read(data); // read codes InputStream is=new ByteArrayInputStream(data); if(cmp==CCITTGROUP3MODHUFFMAN){ // 2, baseline if(ifd.getFillOrder()==LowColHighBit){is=new BitSwapInputStream(is);} offset=readMH(imgdata,offset,is,width); }else{ if(ifd.getFillOrder()!=LowColHighBit){is=new BitSwapInputStream(is);} switch(cmp){ case NOCOMPRESSION: break; // 1, case LZW: is=new LZWInputStream(is,8,false); break; // 5, non base line case PACKBITS: is=new PackBitsInputStream(is); break; // 32773, default: System.out.println("9\b"+cn+".readBWImage:\n\tDo not support compression scheme "+cmp+"."); return image; } mbps=((max-offset)<mbps)?max-offset:mbps; offset+=is.read(imgdata,offset,mbps); // read/decode max. rps image lines } } }catch(Exception e){ System.out.println("9\b"+cn+".readImage:\n\t"+e.getMessage()); e.printStackTrace(); } return image; } static private int readMH(byte[] imgdata,int off,InputStream is,int width)throws IOException{ ModHuffmanInputStream mhis=new ModHuffmanInputStream(is); RLEBitInputStream rlis=new RLEBitInputStream(mhis); if((width&0x0007)==0){ byte[] buf=new byte[width>>3];int len=0; while(true){ rlis.resetToStartCodeWord(); // start next line with white try{ len=rlis.read(buf); // read one image line if(len==-1){break;} // end of page System.arraycopy(buf,0,imgdata,off,len); // copy line to image buffer mhis.skipPadding(8); // skip bits up until next byte boundary }catch(ModHuffmanInputStream.ModHuffmanCodingException mhce){ System.out.println(cn+".copyin:\n\t"+mhce); } off+=len; } }else{ byte[] buf=new byte[(width+7)>>3];int len=0,ecw=8-(width&0x0007),bits; while(true){ rlis.resetToStartCodeWord(); // start next line with white try{ len=rlis.read(buf,0,buf.length-1); // read one image line if(len==-1){break;} // end of page bits=rlis.readBits(7,ecw); buf[len]=(byte)bits; System.arraycopy(buf,0,imgdata,off,len+1); // copy line to image buffer mhis.skipPadding(8); // skip bits up until next byte boundary }catch(ModHuffmanInputStream.ModHuffmanCodingException mhce){ System.out.println(cn+".copyin:\n\t"+mhce); } off+=len+1; } } return off; } static private BufferedImage read4bitImage(ImageInputStream in,IFD ifd,IndexColorModel cm)throws IOException{ BufferedImage image=null; try{ int width =ifd.getWidth(); int height =ifd.getHeight(); int cmp =ifd.getCompression(); image=new BufferedImage(width,height,BufferedImage.TYPE_BYTE_BINARY,cm); WritableRaster raster=image.getRaster(); DataBufferByte buffer=(DataBufferByte)raster.getDataBuffer(); byte[] imgdata=(byte[])buffer.getData(); long[] offsets =ifd.getStripOffsets(); long[] counts =ifd.getStripByteCounts(); int rps =ifd.getRowsPerStrip(); int predictor=ifd.getPredictor(); if(predictor!=1){ // does any program use this ? System.out.println("9\b"+cn+".read4bitImage:\n\tDo not support 'Differencing Predictor' yet."); return image; } int offset=0,mbps=((width+1)>>1)*rps,max=((width+1)>>1)*height; // two 4-bit pixel per byte for(int i=0;i<offsets.length;i++){ in.seek(offsets[i]); byte[] data=new byte[(int)counts[i]]; in.read(data); // read codes InputStream is=new ByteArrayInputStream(data); if(ifd.getFillOrder()!=LowColHighBit){is=new BitSwapInputStream(is);} switch(cmp){ case NOCOMPRESSION: break; // 1, case LZW: is=new LZWInputStream(is,8,false); break; // 5, non base line case PACKBITS: is=new PackBitsInputStream(is); break; // 32773, default: System.out.println("9\b"+cn+".read4bitImage:\n\tDo not support compression scheme "+cmp+"."); return image; } mbps=((max-offset)<mbps)?max-offset:mbps; offset+=is.read(imgdata,offset,mbps); // read/decode max. rps image lines } }catch(Exception e){ System.out.println("9\b"+cn+".read4bitImage:\n\t"+e.getMessage()); e.printStackTrace(); } return image; } static private BufferedImage readGray8bitImage(ImageInputStream in,IFD ifd,boolean invert)throws IOException{ BufferedImage image=null; try{ int width =ifd.getWidth(); int height =ifd.getHeight(); int cmp =ifd.getCompression(); image=new BufferedImage(width,height,BufferedImage.TYPE_BYTE_GRAY); WritableRaster raster=image.getRaster(); DataBufferByte buffer=(DataBufferByte)raster.getDataBuffer(); byte[] imgdata=(byte[])buffer.getData(); long[] offsets =ifd.getStripOffsets(); long[] counts =ifd.getStripByteCounts(); int rps =ifd.getRowsPerStrip(); int predictor=ifd.getPredictor(); JPEGInputStream tables; try{ tables = new JPEGInputStream(new ByteArrayInputStream(ifd.getJPEGTables())); }catch(IllegalArgumentException iae){ tables=null; } int offset=0,mbps=width*rps,max=width*height; // pixel per byte for(int i=0;i<offsets.length;i++){ in.seek(offsets[i]); byte[] data=new byte[(int)counts[i]]; in.read(data); // read codes InputStream is=new ByteArrayInputStream(data); if(ifd.getFillOrder()!=LowColHighBit){is=new BitSwapInputStream(is);} switch(cmp){ case NOCOMPRESSION: break; // 1, case LZW: // 5, non base line is=new LZWInputStream(is,8,false); is=getPredictorInputStream(is,predictor,width,1); break; case JPEG: if(tables!=null){ is=new JPEGInputStream(is,tables.getQTs(),tables.getDCIns(),tables.getACIns()); }else{ is=new JPEGInputStream(is);} break; case PACKBITS: is=new PackBitsInputStream(is); break; // 32773, default: System.out.println("9\b"+cn+".readGray8bitImage:\n\tDo not support compression scheme "+cmp+"."); return image; } if(invert){is=new InvertedInputStream(is);} mbps=((max-offset)<mbps)?max-offset:mbps; offset+=is.read(imgdata,offset,mbps); // read/decode max. rps image lines } }catch(Exception e){ System.out.println("9\b"+cn+".readGray8bitImage:\n\t"+e.getMessage()); e.printStackTrace(); } return image; } static private BufferedImage read8bitImage(ImageInputStream in,IFD ifd)throws IOException{ BufferedImage image=null; try{ int width =ifd.getWidth(); int height =ifd.getHeight(); int cmp =ifd.getCompression(); IndexColorModel cm=ifd.getColorModel(); image=new BufferedImage(width,height,BufferedImage.TYPE_BYTE_INDEXED,cm); WritableRaster raster=image.getRaster(); DataBufferByte buffer=(DataBufferByte)raster.getDataBuffer(); byte[] imgdata=(byte[])buffer.getData(); long[] offsets =ifd.getStripOffsets(); long[] counts =ifd.getStripByteCounts(); int rps =ifd.getRowsPerStrip(); int predictor=ifd.getPredictor(); int offset=0,mbps=width*rps,max=width*height; // pixel per byte for(int i=0;i<offsets.length;i++){ in.seek(offsets[i]); byte[] data=new byte[(int)counts[i]]; in.read(data); // read codes InputStream is=new ByteArrayInputStream(data); if(ifd.getFillOrder()!=LowColHighBit){is=new BitSwapInputStream(is);} switch(cmp){ case NOCOMPRESSION: break; // 1, case LZW: // 5, non base line is=new LZWInputStream(is,8,false); is=getPredictorInputStream(is,predictor,width,1); break; case PACKBITS: is=new PackBitsInputStream(is); break; // 32773, default: System.out.println("9\b"+cn+".read8bitImage:\n\tDo not support compression scheme "+cmp+"."); return image; } mbps=((max-offset)<mbps)?max-offset:mbps; offset+=is.read(imgdata,offset,mbps); // read/decode max. rps image lines } }catch(Exception e){ System.out.println("9\b"+cn+".read8bitImage:\n\t"+e.getMessage()); e.printStackTrace(); } return image; } static private BufferedImage readRGBImage(ImageInputStream in,IFD ifd,int spp)throws IOException{ BufferedImage image=null; try{ int width =ifd.getWidth(); int height =ifd.getHeight(); int cmp =ifd.getCompression(); int alpha=0; int esl=ifd.getExtraSamplesLength(); // do we have extra samples ? if(esl>0){ if((spp-3)==esl){ // if last sample is alpha data alpha=ifd.getExtraSample(esl-1); // [1] p.77 : 1 = Associated alpha data (with pre-multiplied color) }else{ System.out.println("9\b"+cn+".readRGBImage:\n\tInvalid TIFF file. 'Samples per Pixel' != ('Extra Samples' + 3)."); } } switch(alpha){ case 1: image=new BufferedImage(width,height,BufferedImage.TYPE_INT_ARGB_PRE); case 2: image=new BufferedImage(width,height,BufferedImage.TYPE_INT_ARGB); default: image=new BufferedImage(width,height,BufferedImage.TYPE_INT_RGB); } WritableRaster raster=image.getRaster(); DataBufferInt buffer=(DataBufferInt)raster.getDataBuffer(); int[] imgdata=(int[])buffer.getData(); long[] offsets =ifd.getStripOffsets(); long[] counts =ifd.getStripByteCounts(); // int rps = ifd.getRowsPerStrip(); int rps; // work around: required RowsPerStrip field missing try{ // tiff 6.0 p.24 rps = ifd.getRowsPerStrip(); }catch(IllegalArgumentException iae){ // assume only one if(counts[0]!=width*height*spp){ throw iae;} System.out.println("9\b"+cn+".readRGBImage:\n\tInvalid TIFF file: Missing 'RowsPerStrip' field. Assume one strip. Set rps = height"); rps = height; } int predictor=ifd.getPredictor(); JPEGInputStream tables; try{ tables = new JPEGInputStream(new ByteArrayInputStream(ifd.getJPEGTables())); }catch(IllegalArgumentException iae){ tables=null; } IntFilterInputStream intis; int offset=0,mbps=width*rps,max=width*height; for(int i=0;i<offsets.length;i++){ in.seek(offsets[i]); byte[] data=new byte[(int)counts[i]]; in.read(data); // read codes InputStream is=new ByteArrayInputStream(data); if(ifd.getFillOrder()!=LowColHighBit){is=new BitSwapInputStream(is);} mbps=((max-offset)<mbps)?max-offset:mbps; switch(cmp){ case NOCOMPRESSION: // 1 intis=new RGBInputStream(is,spp,alpha!=0); break; case LZW: // 5, non base line is=new LZWInputStream(is,8,false); is=getPredictorInputStream(is,predictor,width,spp); intis=new RGBInputStream(is,spp,alpha!=0); break; case JPEG: if(tables!=null){ intis=new JPEGInputStream(is,tables.getQTs(),tables.getDCIns(),tables.getACIns()); }else{ intis=new JPEGInputStream(is);} break; case PACKBITS: // 32773 is=new PackBitsInputStream(is); intis=new RGBInputStream(is,spp,alpha!=0); break; default: System.out.println("9\b"+cn+".readRGBImage:\n\tDo not support compression scheme "+cmp+"."); return image; } offset+=intis.read(imgdata,offset,mbps); // read/decode max. rps image lines } }catch(Exception e){ System.out.println("9\b"+cn+".readRGBImage:\n\t"+e.getMessage()); e.printStackTrace(); } return image; } static private BufferedImage readCMYKImage(ImageInputStream in,IFD ifd)throws IOException{ BufferedImage image=null; try{ int width =ifd.getWidth(); int height =ifd.getHeight(); int cmp =ifd.getCompression(); image=new BufferedImage(width,height,BufferedImage.TYPE_INT_RGB); WritableRaster raster=image.getRaster(); DataBufferInt buffer=(DataBufferInt)raster.getDataBuffer(); int[] imgdata=(int[])buffer.getData(); long[] offsets =ifd.getStripOffsets(); long[] counts =ifd.getStripByteCounts(); int rps =ifd.getRowsPerStrip(); int offset=0,mbps=width*rps,max=width*height; for(int i=0;i<offsets.length;i++){ in.seek(offsets[i]); byte[] data=new byte[(int)counts[i]]; in.read(data); // read codes InputStream is=new ByteArrayInputStream(data); if(ifd.getFillOrder()!=LowColHighBit){is=new BitSwapInputStream(is);} switch(cmp){ case NOCOMPRESSION: break; // 1, case LZW: is=new LZWInputStream(is,8,false); break; // 5, non base line case PACKBITS: is=new PackBitsInputStream(is); break; // 32773, default: System.out.println("9\b"+cn+".readCMYKImage:\n\tDo not support compression scheme "+cmp+"."); return image; } mbps=((max-offset)<mbps)?max-offset:mbps; TIFFCMYKInputStream cmykis=new TIFFCMYKInputStream(is); offset+=cmykis.read(imgdata,offset,mbps); // read/decode max. rps image lines } }catch(Exception e){ System.out.println("9\b"+cn+".readCMYKImage:\n\t"+e.getMessage()); e.printStackTrace(); } return image; } static private BufferedImage readYCbCrImage(ImageInputStream in,IFD ifd)throws IOException{ BufferedImage image=null; try{ int width =ifd.getWidth(); int height =ifd.getHeight(); int cmp =ifd.getCompression(); image=new BufferedImage(width,height,BufferedImage.TYPE_INT_RGB); WritableRaster raster=image.getRaster(); DataBufferInt buffer=(DataBufferInt)raster.getDataBuffer(); int[] imgdata=(int[])buffer.getData(); long[] offsets =ifd.getStripOffsets(); long[] counts =ifd.getStripByteCounts(); int rps =ifd.getRowsPerStrip(); int predictor=ifd.getPredictor(); if(predictor!=1){ System.out.println("9\b"+cn+".readYCbCrImage:\n\tDo not support 'Differencing Predictor' yet."+email); return image; } JPEGInputStream tables; try{ tables = new JPEGInputStream(new ByteArrayInputStream(ifd.getJPEGTables())); }catch(IllegalArgumentException iae){ tables=null; } double[] coeff = ifd.getYCbCrCoefficients(); long[] sampling = ifd.getYCbCrSubSampling(); int positioning = ifd.getYCbCrPositioning(); double[] rbw = ifd.getReferenceBlackWhite(); IntFilterInputStream intis; int offset=0,mbps=width*rps,max=width*height; for(int i=0;i<offsets.length;i++){ in.seek(offsets[i]); byte[] data=new byte[(int)counts[i]]; in.read(data); // read codes InputStream is=new ByteArrayInputStream(data); if(ifd.getFillOrder()!=LowColHighBit){is=new BitSwapInputStream(is);} switch(cmp){ case NOCOMPRESSION: // 1, intis=new TIFFSubSamplingInputStream(is,width,(int)sampling[1],(int)sampling[0],positioning); break; case LZW: // 5, non base line is=new LZWInputStream(is,8,false); intis=new TIFFSubSamplingInputStream(is,width,(int)sampling[1],(int)sampling[0],positioning); break; case JPEG: // 7 if(tables!=null){ intis=new JPEGInputStream(is,tables.getQTs(),tables.getDCIns(),tables.getACIns()); }else{ intis=new JPEGInputStream(is);} break; case PACKBITS: // 32773, is=new PackBitsInputStream(is); intis=new TIFFSubSamplingInputStream(is,width,(int)sampling[1],(int)sampling[0],positioning); break; default: System.out.println("9\b"+cn+".readYCbCrImage:\n\tDo not support compression scheme "+cmp+"."); return image; } mbps=((max-offset)<mbps)?max-offset:mbps; TIFFYCbCrInputStream ycbcris=new TIFFYCbCrInputStream(intis); ycbcris.setColourCoefficients(coeff[0],coeff[1],coeff[2]); ycbcris.setRfBWY (rbw[0],rbw[1]); ycbcris.setRfBWCb(rbw[2],rbw[3]); ycbcris.setRfBWCr(rbw[4],rbw[5]); offset+=ycbcris.read(imgdata,offset,mbps); // read/decode max. rps image lines } }catch(Exception e){ System.out.println("9\b"+cn+".readYCbCrImage:\n\t"+e.getMessage()); e.printStackTrace(); } return image; } static private InputStream getPredictorInputStream(InputStream in, int predictor, int width, int spp){ switch(predictor){ case 1: return in; // none case 2: return new TIFFHorizontalDifferenceInputStream(in,width,spp); // horizontal difference default: // afaik nothing else is specified System.out.println("9\b"+cn+".getPredictorInputStream:\n\tDo not support 'Predictor' "+predictor+" yet."+email); break; } return in; } static{ // byte[] bwiz={ 0,-1}; // byte[] bbiz={-1, 0}; byte[] bwiz={-1, 0}; // 2009-05-25 byte[] bbiz={ 0,-1}; bwwhiteiszero=new IndexColorModel(1,2,bwiz,bwiz,bwiz); bwblackiszero=new IndexColorModel(1,2,bbiz,bbiz,bbiz); byte[] gwiz={-1,-18,-35,-52,-69,-86,-103,-120, 119, 102, 85, 68, 51, 34, 17, 0}; byte[] gbiz={ 0, 17, 34, 51, 68, 85, 102, 119,-120,-103,-86,-69,-52,-35,-18,-1}; graywhiteiszero=new IndexColorModel(4,16,gwiz,gwiz,gwiz); grayblackiszero=new IndexColorModel(4,16,gbiz,gbiz,gbiz); } public static void main(String[] args){ try{ for(int i=0;i<256;i+=17){ byte b=(byte)i; System.out.print(""+b+","); } System.out.println(" "); for(int i=255;i>=0;i-=17){ byte b=(byte)i; System.out.print(""+b+","); } }catch(Exception e){ System.out.println(e); } } }