package uk.co.mmscomputing.device.sane; import java.io.*; import java.awt.image.*; import java.awt.*; import java.awt.event.*; import javax.swing.*; import javax.swing.border.*; import uk.co.mmscomputing.util.JarImageIcon; import uk.co.mmscomputing.concurrent.Semaphore; import uk.co.mmscomputing.device.scanner.ScannerDevice; import uk.co.mmscomputing.device.scanner.ScannerIOException; import uk.co.mmscomputing.device.sane.gui.SaneAcquirePanel; import uk.co.mmscomputing.device.sane.option.*; public class SaneDevice implements SaneConstants,ScannerDevice{ static final private int buffersize = 1 << 15; private String name; private int saneHandle; private boolean cancelled,userCancelled,adfmode; private int state,adfcount; private boolean showUI; // show user interface private boolean showPB; // show user progress bar private JProgressBar pbar; // progressbar to be shown private Semaphore blocker; // sane function are non reentrant; make sure we call them one after another SaneDevice(String device){ super(); saneHandle = 0; name = device; cancelled = false; userCancelled = false; adfmode = false; adfcount = -1; // -1: no limit; 0: don't scan; >0 scan count pages state = SANE_STATE_EXIT; showUI = true; showPB = true; pbar = null; blocker = new Semaphore(1,true); } public String getName(){ return name;} public int getState(){ return state;} void setState(int s){ state=s;jsane.signalStateChange(this);} synchronized boolean isState(int state){ return this.state==state;} public boolean isBusy(){ return !isState(SANE_STATE_EXIT);} public void checkBusy()throws SaneIOException{ if(isBusy()){throw new SaneIOException(getClass(),"checkBusy","SaneDevice.ex.isbusy",name);} } public boolean getADFMode(){ return adfmode;} public void setADFMode(boolean mode){ adfmode=mode;} public int getADFCount(){ return adfcount;} public void setADFCount(int count){ adfcount=count;} public boolean isOpen(){ return (state==SANE_STATE_OPEN)||(state==SANE_STATE_READY); } public void setShowUserInterface(boolean enable){showUI=enable;} public void setShowProgressBar(boolean enable){showPB=enable;} public void setProgressBar(JProgressBar pbar){this.pbar=pbar;} private void setProgressBar(String str){if(pbar!=null){pbar.setString(str);}} private void resetProgressBar(int max){if(pbar!=null){pbar.setValue(0);pbar.setMaximum(max);}} private void setProgressBar(int progress){ if(pbar!=null){ pbar.setValue(progress); if(progress>10*1024*1024){ progress>>=20; pbar.setString(""+progress+" MB"); }else if(progress>10*1024){ progress>>=10; pbar.setString(""+progress+" kB"); } } } // option descriptors public int getNumberOfOptions()throws SaneIOException{ return jsane.getNumberOfOptions(saneHandle); } public OptionDescriptor getOptionDescriptor(int option){ return jsane.getOptionDescriptor(saneHandle,option); } public OptionDescriptor[] getOptionDescriptors()throws SaneIOException{ OptionDescriptor[] list=new OptionDescriptor[getNumberOfOptions()]; for(int i=0;i<list.length;i++){ try{ list[i]=getOptionDescriptor(i); }catch(Exception e){ // The SANE 'test' scanner returns a lot of 'Invalid arguments' here. // Strange, but I guess that is how it is supposed to be. list[i]=null; }catch(Error e){ } } return list; } // option negotiation public void setOption(String option,String value)throws ScannerIOException{ int n=getNumberOfOptions(); for(int i=0;i<n;i++){ Descriptor od=(Descriptor)getOptionDescriptor(i); String name=od.getName(); if(option.equals(name)){ if(od instanceof StringDesc){ String res=od.setStringValue(value); if(res.equals(value)){return;} }else if(od instanceof FixedDesc){ double val=Double.parseDouble(value); // check it is number double res=Double.parseDouble(od.setStringValue(0,value)); if(res==val){return;} }else if(od instanceof IntDesc){ double val=Double.parseDouble(value); // check it is number double res=Double.parseDouble(od.setStringValue(0,value)); if(res==val){return;} } String[] args={option,value}; throw new SaneIOException(getClass(),"setOption","SaneDevice.ex.couldnotsetoption",args); } } throw new ScannerIOException(getClass().getName()+".setOption:\n\tCould not find option '"+option); } public void setOption(String option,double value)throws ScannerIOException{ int n=getNumberOfOptions(); for(int i=0;i<n;i++){ Descriptor od=(Descriptor)getOptionDescriptor(i); String name=od.getName(); if(option.equals(name)){ if(od instanceof FixedDesc){ double res=Double.parseDouble(od.setStringValue(0,""+value)); if(res==value){return;} }else if(od instanceof IntDesc){ double res=Double.parseDouble(od.setStringValue(0,""+((int)value))); if(res==value){return;} } String[] args={option,""+value}; throw new SaneIOException(getClass(),"setOption","SaneDevice.ex.couldnotsetoption",args); } } throw new SaneIOException(getClass(),"setOption","SaneDevice.ex.couldnotfindoption",option); } public Descriptor getOption(String option)throws ScannerIOException{ int n=getNumberOfOptions(); for(int i=0;i<n;i++){ Descriptor od=(Descriptor)getOptionDescriptor(i); String name=od.getName(); if(option.equals(name)){ return od; } } throw new SaneIOException(getClass(),"getOption","SaneDevice.ex.couldnotfindoption",option); } public void setResolution(double dpi)throws ScannerIOException{ // Unit : DPI // Type : INT or FIXED setOption("resolution",dpi); } public void setRegionOfInterest(int x, int y, int width, int height)throws ScannerIOException{ // Unit : PIXEL Descriptor tlx = getOption("tl-x"); // convert into MM if necessary if(tlx.getUnit()!=SANE_UNIT_PIXEL){ // only PIXE: and MM allowed int res = getOption("resolution").getWordValue(0); x = (int)tlx.convertPixels2Units(res,x); y = (int)tlx.convertPixels2Units(res,x); width = (int)tlx.convertPixels2Units(res,width); height = (int)tlx.convertPixels2Units(res,height); } setOption("tl-x",x); setOption("tl-y",y); setOption("br-x",x+width); setOption("br-y",y+height); } public void setRegionOfInterest(double x, double y, double width, double height)throws ScannerIOException{ // Unit : MM // to do: convert into Pixel if necessary setOption("tl-x",x); setOption("tl-y",y); setOption("br-x",x+width); setOption("br-y",y+height); } public void select(String name)throws ScannerIOException{ select(name,true); } public void select(String name,boolean onlyLocal)throws ScannerIOException{ if(state!=SANE_STATE_INITIALIZE){ throw new SaneIOException(getClass(),"select","SaneDevice.ex.calledinwrongstate","SANE_STATE_INITIALIZE"); } String[] devices=jsane.getDevices(onlyLocal); for(int i=0;i<devices.length;i++){ if(devices[i].equals(name)){ name=devices[i];return; } } throw new SaneIOException(getClass(),"select","SaneDevice.ex.cannotfinddevice",name); } // sane functions; the jsane.functions are not reentrant! private void init()throws SaneIOException{ try{ blocker.acquire(); jsane.init(); setState(SANE_STATE_INITIALIZE); }catch(InterruptedException ie){ }finally{ blocker.release(); } } private void exit()throws SaneIOException{ try{ blocker.acquire(); jsane.exit(); setState(SANE_STATE_EXIT); }catch(InterruptedException ie){ }finally{ blocker.release(); } } private void start()throws SaneIOException{ if(cancelled){return;} try{ blocker.acquire(); if(cancelled){return;} jsane.start(saneHandle); setState(SANE_STATE_READY); }catch(InterruptedException ie){ }finally{ blocker.release(); } } private void close()throws SaneIOException{ try{ blocker.acquire(); jsane.close(saneHandle); setState(SANE_STATE_CLOSED); }catch(InterruptedException ie){ }finally{ blocker.release(); } } private void getParameters(Parameters p)throws SaneIOException{ p.format=-1; p.lastFrame=SANE_TRUE; if(cancelled){return;} try{ blocker.acquire(); if(cancelled){return;} jsane.getParameters(saneHandle,p); }catch(InterruptedException ie){ }finally{ blocker.release(); } } private int read(byte[] buf,int off,int len)throws SaneIOException{ if(cancelled){return -1;} try{ blocker.acquire(); if(cancelled){return -1;} return jsane.read(saneHandle,buf,off,len); }catch(InterruptedException ie){ }finally{ blocker.release(); } return -1; } private void cancel(){ if(!isOpen()||cancelled){return;} try{ blocker.acquire(); if(!isOpen()||cancelled){return;} jsane.cancel(saneHandle); // System.err.println("cancel sane handle 0x"+Integer.toHexString(saneHandle)); cancelled=true; setState(SANE_STATE_CANCELLED); }catch(InterruptedException ie){ }finally{ blocker.release(); } } public void setCancel(boolean c){ if(c){cancel();} userCancelled=c; } public boolean getCancel(){return userCancelled;} // image acquisition private BufferedImage createGrayImage(Parameters p, InputStream in)throws IOException{ try{ in=new BufferedInputStream(in,buffersize); if(p.depth==1){ // P4 black&white int width =p.bytesPerLine; // width in bytes int height =p.lines; resetProgressBar(height); setProgressBar(0); // System.err.println(getClass().getName()+".createGrayImage[BW]:\n\tp.bytesPerLine="+width); BufferedImage image=new BufferedImage(width<<3,height,BufferedImage.TYPE_BYTE_BINARY); Raster raster = image.getRaster(); DataBufferByte db = (DataBufferByte)raster.getDataBuffer(); byte[] data = db.getData(); int k=0; for(int y=0;y<height;y++){ for(int x=0;x<width;x++){ data[k++]=(byte)(~in.read()); } if((y%100)==0){setProgressBar(y);} } setProgressBar(height); return image; }else if(p.depth==8){ int width =p.bytesPerLine; int height =p.lines; resetProgressBar(height); setProgressBar(0); // System.err.println(getClass().getName()+".createGrayImage[GRAY8]:\n\tp.bytesPerLine="+width); BufferedImage image=new BufferedImage(width,height,BufferedImage.TYPE_BYTE_GRAY); Raster raster = image.getRaster(); DataBufferByte db = (DataBufferByte)raster.getDataBuffer(); byte[] data = db.getData(); int k=0; for(int y=0;y<height;y++){ for(int x=0;x<width;x++){ data[k++]=(byte)(in.read()); } if((y%100)==0){setProgressBar(y);} } setProgressBar(height); return image; }else if(p.depth==16){ int width =p.bytesPerLine>>1; int height =p.lines; resetProgressBar(height); setProgressBar(0); // System.err.println(getClass().getName()+".createGrayImage[GRAY16]:\n\tp.bytesPerLine="+width); BufferedImage image=new BufferedImage(width,height,BufferedImage.TYPE_USHORT_GRAY); Raster raster = image.getRaster(); DataBufferUShort db = (DataBufferUShort)raster.getDataBuffer(); short[] data = db.getData(); int k=0; if(isLitteEndian){ for(int y=0;y<height;y++){ for(int x=0;x<width;x++){ data[k++]=(short)((in.read()&0x00FF)|((in.read()&0x00FF)<<8)); } if((y%100)==0){setProgressBar(y);} } }else{ for(int y=0;y<height;y++){ for(int x=0;x<width;x++){ data[k++]=(short)(((in.read()&0x00FF)<<8)|(in.read()&0x00FF)); } if((y%100)==0){setProgressBar(y);} } } setProgressBar(height); return image; }else{ throw new SaneIOException(getClass(),"createGrayImage","SaneDevice.ex.grayscaledepthnotsupported",""+p.depth); } }catch(Exception e){ // couldn't start device jsane.signalException(e); }finally{ in.close(); } return null; } private BufferedImage createRGBImage(Parameters p, InputStream in)throws IOException{ if(p.depth==1){ // sane "test" scanner allows colour/1bit throw new SaneIOException(getClass(),"createRGBImage","SaneDevice.ex.rgbdepthnotsupported",""+p.depth); } try{ int width =p.bytesPerLine/((p.depth>>3)*3); int height =p.lines; // System.err.println(getClass().getName()+".createRGBImage:\n\tp.bytesPerLine="+p.bytesPerLine); BufferedImage image = new BufferedImage(width,height,BufferedImage.TYPE_INT_RGB); Raster raster = image.getRaster(); DataBufferInt db = (DataBufferInt)raster.getDataBuffer(); int[] data = db.getData(); in=new BufferedInputStream(in,buffersize); resetProgressBar(height); setProgressBar(0); int r,g,b,k=0,pixel; if(p.depth==8){ // P6 colour for(int y=0;y<height;y++){ for(int x=0;x<width;x++){ r=in.read()&0x00FF; g=in.read()&0x00FF; b=in.read()&0x00FF; data[k++]=0xFF000000|(r<<16)|(g<<8)|b; } if((y%100)==0){setProgressBar(y);} } }else if(p.depth==16){ if(isLitteEndian){ for(int y=0;y<height;y++){ for(int x=0;x<width;x++){ in.read();r=in.read()&0x00FF; // scale to 0..255 range; use most significant byte; in.read();g=in.read()&0x00FF; in.read();b=in.read()&0x00FF; data[k++]=0xFF000000|(r<<16)|(g<<8)|b; } if((y%100)==0){setProgressBar(y);} } }else{ for(int y=0;y<height;y++){ for(int x=0;x<width;x++){ r=in.read()&0x00FF;in.read(); // scale to 0..255 range; use most significant byte; g=in.read()&0x00FF;in.read(); b=in.read()&0x00FF;in.read(); data[k++]=0xFF000000|(r<<16)|(g<<8)|b; } if((y%100)==0){setProgressBar(y);} } } }else{ throw new SaneIOException(getClass(),"createRGBImage","SaneDevice.ex.rgbdepthnotsupported",""+p.depth); } setProgressBar(height); return image; }finally{ in.close(); } } private BufferedImage create3FRGBImage(Parameters p,InputStream red,InputStream green,InputStream blue)throws IOException{ try{ if((red==null)||(green==null)||(blue==null)){ throw new SaneIOException(getClass(),"create3FRGBImage","SaneDevice.ex.rgbframesmissing"); } // System.err.println(getClass().getName()+".create3FRGBImage: "+p.bytesPerLine); int width =p.bytesPerLine/(p.depth>>3); int height =p.lines; BufferedImage image=new BufferedImage(width,height,BufferedImage.TYPE_INT_RGB); Raster raster = image.getRaster(); DataBufferInt db = (DataBufferInt)raster.getDataBuffer(); int[] data = db.getData(); int r,g,b,k=0,pixel,sample; red =new BufferedInputStream(red,buffersize); green=new BufferedInputStream(green,buffersize); blue =new BufferedInputStream(blue,buffersize); resetProgressBar(height); setProgressBar(0); if(p.depth==8){ for(int y=0;y<height;y++){ for(int x=0;x<width;x++){ r=red.read(); g=green.read(); b=blue.read(); data[k++]=0xFF000000|(r<<16)|(g<<8)|b; } if((y%100)==0){setProgressBar(y);} } }else if(p.depth==16){ if(isLitteEndian){ for(int y=0;y<height;y++){ for(int x=0;x<width;x++){ red.read(); r=red.read(); green.read(); g=green.read(); blue.read(); b=blue.read(); data[k++]=0xFF000000|(r<<16)|(g<<8)|b; } if((y%100)==0){setProgressBar(y);} } }else{ for(int y=0;y<height;y++){ for(int x=0;x<width;x++){ r=red.read(); red.read(); g=green.read(); green.read(); b=blue.read(); blue.read(); data[k++]=0xFF000000|(r<<16)|(g<<8)|b; } if((y%100)==0){setProgressBar(y);} } } }else{ throw new SaneIOException(getClass(),"create3FRGBImage","SaneDevice.ex.rgbdepthnotsupported",""+p.depth); } setProgressBar(height); return image; }finally{ red.close();green.close();blue.close(); } } private File readImageData(Parameters p)throws IOException{ File file=File.createTempFile("mmsc_sane_",null); OutputStream out=new BufferedOutputStream(new FileOutputStream(file),buffersize); int len; byte[] buf=new byte[buffersize]; try{ start(); // when scanning 3 frame images need to call start otherwise getParameter will return wrong values getParameters(p); // p.lines might be -1 resetProgressBar(p.bytesPerLine*p.lines); int size=0; try{ while((len=read(buf,0,buf.length))!=-1){ out.write(buf,0,len); size+=len; setProgressBar(size); } }catch(SaneIOException e){ jsane.signalException(e); } if(p.lines==-1){p.lines=size/p.bytesPerLine;} // getParameters(p); // gives wrong values here }finally{ out.close(); } return file; } public BufferedImage getImage(boolean onlyOne)throws IOException{ // also called from SanePreviewPanel BufferedImage img = getImage(); cancel(); return img; } public BufferedImage getImage()throws IOException{ // also called from SanePreviewPanel File[] files=new File[3]; Parameters p =new Parameters(); cancelled = false; try{ int frames; for(frames=0;(frames<files.length)&&(p.lastFrame==SANE_FALSE);frames++){ File file=readImageData(p); if(cancelled){break;} switch(p.format){ case SANE_FRAME_GRAY: files[0]=file; break; case SANE_FRAME_RGB: files[0]=file; break; case SANE_FRAME_RED: files[0]=file; break; case SANE_FRAME_GREEN:files[1]=file; break; case SANE_FRAME_BLUE: files[2]=file; break; default: p.lastFrame=SANE_TRUE;frames=-1;break; } } if(!cancelled){ if(frames==3){ return create3FRGBImage(p,new FileInputStream(files[0]),new FileInputStream(files[1]),new FileInputStream(files[2])); }else if(frames==1){ if(p.format==SANE_FRAME_RGB){ return createRGBImage(p,new FileInputStream(files[0])); }else if(p.format==SANE_FRAME_GRAY){ return createGrayImage(p,new FileInputStream(files[0])); } } } }finally{ for(int i=0;i<files.length;i++){ if(files[i]!=null){files[i].delete();} } } return null; } void acquire(){ // String busy=System.getProperty(getClass().getName()+".busy","false"); try{ // if(busy.equals("true")){ // applets // throw new SaneIOException(getClass().getName()+".init:\n\tjsane is already used by another applet in this JVM."); // } // System.setProperty(getClass().getName()+".busy","true"); setProgressBar((JProgressBar)null); init(); try{ adfcount=-1; userCancelled=false; saneHandle=jsane.open(name); setState(SANE_STATE_OPEN); jsane.negotiateOptions(this); if(showUI){ Semaphore s=new Semaphore(0,true); new SaneAcquirePanel(this,s).display(); s.acquire(); } ProgressMonitor pm=(showPB)?new ProgressMonitor(name):null; setProgressBar(pm); setProgressBar("preparing "+name); try{ if(!userCancelled){ if(pm!=null){pm.open();} try{ do{ jsane.signalImage(getImage()); adfcount--; }while(adfmode&&(adfcount!=0)); // if not in adf-mode we scan only once. }catch(SaneNoDocumentsException snde){ // this is the normal end of document condition for adf scanners !!! if(!adfmode){throw snde;} }finally{ cancel(); } } }finally{ close(); if(pm!=null){pm.close();} } }catch(Exception e){ jsane.signalException(e); }finally{ exit(); } }catch(Exception e){ jsane.signalException(e); } // System.setProperty(getClass().getName()+".busy","false"); // applets } static private boolean isLitteEndian; static{ isLitteEndian=true; String endian=System.getProperty("sun.cpu.endian"); if(endian!=null){isLitteEndian=endian.equals("little");} } private class ProgressMonitor extends JProgressBar{ JFrame dialog; ProgressMonitor(String device){ super(0,100); setStringPainted(true); JPanel panel=new JPanel(); JPanel q=new JPanel(); q.setLayout(new BorderLayout()); Border border = q.getBorder(); Border margin = new EmptyBorder(10,10,5,10); q.setBorder(new CompoundBorder(border, margin)); q.add(new JLabel("Scanner:"),BorderLayout.NORTH); JLabel img=new JLabel(new JarImageIcon(getClass(),"32x32/scanner.png")); img.setBorder(new EmptyBorder(10,10,10,10)); q.add(img,BorderLayout.WEST); JLabel msg=new JLabel("<html>"+device+"</html>"); msg.setBorder(new EmptyBorder(10,10,10,10)); q.add(msg,BorderLayout.CENTER); JPanel p=new JPanel(); p.setLayout(new BorderLayout()); setBorder(new EmptyBorder(5,0,5,0)); p.add(this,BorderLayout.NORTH); JPanel buttons=new JPanel(); buttons.setLayout(new GridLayout(0,3)); buttons.add(new JPanel()); JButton button=new JButton( new AbstractAction("cancel"){public void actionPerformed(ActionEvent ev){setCancel(true);}} ); buttons.add(button); buttons.add(new JPanel()); p.add(buttons,BorderLayout.CENTER); q.add(p,BorderLayout.SOUTH); panel.setLayout(new BorderLayout()); panel.setOpaque(true); panel.add(q,BorderLayout.NORTH); dialog = new JFrame(jsane.getResource("SaneDevice.ProgressMonitor.title")); // dialog.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); dialog.setContentPane(panel); dialog.pack(); } void open(){ dialog.setLocationRelativeTo(null); dialog.toFront(); dialog.setVisible(true); } void close(){ dialog.setVisible(false); // dialog.dispose(); // hangs when jvm shuts down and dialog is still up } } } /* [1] SANE Standard Version 1.03 (Scanner Access Now Easy) 2002-10-10 http://www.mostang.com/sane */