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
*/