from io import BytesIO
import twain
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import logging
import PIL.ImageTk
import PIL.Image
import datetime
scanned_image = None
current_settings = {
'scan_mode': 'Color',
'resolution': 300,
'document_size': 'A4',
'document_type': 'Normal',
'auto_crop': False,
'brightness': 0,
'contrast': 0,
'destination': 'File',
'file_format': 'JPEG',
'file_path': ''
}
def check_adf_support(src):
"""Check if the scanner supports ADF and return ADF status"""
try:
# Check if ADF is supported
if src.get_capability(twain.CAP_FEEDERENABLED):
print("ADF is supported by this scanner")
# Check if ADF is loaded with documents
if src.get_capability(twain.CAP_FEEDERLOADED):
print("ADF has documents loaded")
return True
else:
print("ADF is empty")
return False
else:
print("ADF is not supported")
return False
except twain.excTWCC_CAPUNSUPPORTED:
print("ADF capability not supported")
return False
def apply_settings_to_scanner(src):
"""Apply the current settings to the scanner source"""
try:
# Set basic scan parameters
if current_settings['scan_mode'] == 'Color':
src.set_capability(twain.ICAP_PIXELTYPE, twain.TWPT_RGB)
elif current_settings['scan_mode'] == 'Grayscale':
src.set_capability(twain.ICAP_PIXELTYPE, twain.TWPT_GRAY)
else: # Black & White
src.set_capability(twain.ICAP_PIXELTYPE, twain.TWPT_BW)
src.set_capability(twain.ICAP_XRESOLUTION, float(current_settings['resolution']))
src.set_capability(twain.ICAP_YRESOLUTION, float(current_settings['resolution']))
# Set document size (simplified)
if current_settings['document_size'] == 'A4':
src.set_capability(twain.ICAP_SUPPORTEDSIZES, twain.TWSS_A4)
# Set brightness and contrast if supported
src.set_capability(twain.ICAP_BRIGHTNESS, float(current_settings['brightness']))
src.set_capability(twain.ICAP_CONTRAST, float(current_settings['contrast']))
# Set auto crop if supported
if current_settings['auto_crop']:
src.set_capability(twain.ICAP_AUTOMATICBORDERDETECTION, True)
except twain.excTWCC_CAPUNSUPPORTED:
print("Some capabilities are not supported by this scanner")
def process_scanned_image(img):
"""Handle the scanned image (save or display)"""
global scanned_image
# Save to file if destination is set to file
if current_settings['destination'] == 'File' and current_settings['file_path']:
file_ext = current_settings['file_format'].lower()
if file_ext == 'jpeg':
file_ext = 'jpg'
# Add timestamp to filename for ADF scans
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S_%f")
img.save(f"{current_settings['file_path']}_{timestamp}.{file_ext}")
# Display in UI (only the last image for ADF)
width, height = img.size
factor = 600.0 / width
scanned_image = PIL.ImageTk.PhotoImage(img.resize(size=(int(width * factor), int(height * factor))))
image_frame.destroy()
ttk.Label(root, image=scanned_image).pack(side="left", fill="both", expand=1)
def scan():
global scanned_image
with twain.SourceManager(root) as sm:
src = sm.open_source()
if src:
try:
# Check ADF support
adf_supported = check_adf_support(src)
# Apply settings before scanning
apply_settings_to_scanner(src)
if adf_supported:
# Enable ADF mode
src.set_capability(twain.CAP_FEEDERENABLED, True)
src.set_capability(twain.CAP_AUTOFEED, True)
print("Scanning using ADF mode...")
else:
print("Scanning in flatbed mode...")
# Scan loop for ADF (will scan once if flatbed)
while True:
src.request_acquire(show_ui=False, modal_ui=False)
(handle, remaining_count) = src.xfer_image_natively()
if handle is None:
break
bmp_bytes = twain.dib_to_bm_file(handle)
img = PIL.Image.open(BytesIO(bmp_bytes), formats=["bmp"])
process_scanned_image(img)
# Break if no more documents in ADF
if remaining_count == 0:
break
except Exception as e:
messagebox.showerror("Scan Error", f"Error during scanning: {e}")
finally:
src.destroy()
else:
messagebox.showwarning("Warning", "No scanner selected")
def test_adf_support():
"""Test if ADF is supported and show result in messagebox"""
with twain.SourceManager(root) as sm:
src = sm.open_source()
if src:
try:
# Check basic ADF support
try:
has_adf = src.get_capability(twain.CAP_FEEDER)
except:
has_adf = False
# Check more detailed ADF capabilities
capabilities = {
'CAP_FEEDER': has_adf,
'CAP_FEEDERENABLED': False,
'CAP_FEEDERLOADED': False,
'CAP_AUTOFEED': False,
'CAP_FEEDERPREP': False
}
for cap in capabilities.keys():
try:
capabilities[cap] = src.get_capability(getattr(twain, cap))
except:
pass
# Build results message
result_msg = "ADF Test Results:\n\n"
result_msg += f"Basic ADF Support: {'Yes' if capabilities['CAP_FEEDER'] else 'No'}\n"
result_msg += f"ADF Enabled: {'Yes' if capabilities['CAP_FEEDERENABLED'] else 'No'}\n"
result_msg += f"Documents Loaded: {'Yes' if capabilities['CAP_FEEDERLOADED'] else 'No'}\n"
result_msg += f"Auto-feed Available: {'Yes' if capabilities['CAP_AUTOFEED'] else 'No'}\n"
result_msg += f"Needs Preparation: {'Yes' if capabilities['CAP_FEEDERPREP'] else 'No'}\n"
messagebox.showinfo("ADF Test", result_msg)
except Exception as e:
messagebox.showerror("Error", f"Error testing ADF: {e}")
finally:
src.destroy()
else:
messagebox.showwarning("Warning", "No scanner selected")
def browse_file():
filename = filedialog.asksaveasfilename(
defaultextension=f".{current_settings['file_format'].lower()}",
filetypes=[(f"{current_settings['file_format']} files", f"*.{current_settings['file_format'].lower()}")]
)
if filename:
current_settings['file_path'] = filename
file_path_var.set(filename)
def update_setting(setting_name, value):
current_settings[setting_name] = value
if setting_name == 'file_format' and current_settings['file_path']:
# Update file extension if file path exists
base_path = current_settings['file_path'].rsplit('.', 1)[0]
current_settings['file_path'] = base_path
file_path_var.set(base_path)
def create_settings_panel(parent):
# Scan Mode
ttk.Label(parent, text="Scan Mode:").grid(row=0, column=0, sticky='w')
scan_mode = ttk.Combobox(parent, values=['Color', 'Grayscale', 'Black & White'], state='readonly')
scan_mode.set(current_settings['scan_mode'])
scan_mode.grid(row=0, column=1, sticky='ew')
scan_mode.bind('<<ComboboxSelected>>', lambda e: update_setting('scan_mode', scan_mode.get()))
# Resolution
ttk.Label(parent, text="Resolution (DPI):").grid(row=1, column=0, sticky='w')
resolution = ttk.Combobox(parent, values=[75, 150, 300, 600, 1200], state='readonly')
resolution.set(current_settings['resolution'])
resolution.grid(row=1, column=1, sticky='ew')
resolution.bind('<<ComboboxSelected>>', lambda e: update_setting('resolution', int(resolution.get())))
# Document Size
ttk.Label(parent, text="Document Size:").grid(row=2, column=0, sticky='w')
doc_size = ttk.Combobox(parent, values=['A4', 'Letter', 'Legal', 'Auto'], state='readonly')
doc_size.set(current_settings['document_size'])
doc_size.grid(row=2, column=1, sticky='ew')
doc_size.bind('<<ComboboxSelected>>', lambda e: update_setting('document_size', doc_size.get()))
# Document Type
ttk.Label(parent, text="Document Type:").grid(row=3, column=0, sticky='w')
doc_type = ttk.Combobox(parent, values=['Normal', 'Text', 'Photo', 'Magazine'], state='readonly')
doc_type.set(current_settings['document_type'])
doc_type.grid(row=3, column=1, sticky='ew')
doc_type.bind('<<ComboboxSelected>>', lambda e: update_setting('document_type', doc_type.get()))
# Auto Crop
auto_crop = tk.BooleanVar(value=current_settings['auto_crop'])
ttk.Checkbutton(parent, text="Auto Crop", variable=auto_crop,
command=lambda: update_setting('auto_crop', auto_crop.get())).grid(row=4, column=0, columnspan=2, sticky='w')
# Brightness
ttk.Label(parent, text="Brightness:").grid(row=5, column=0, sticky='w')
brightness = ttk.Scale(parent, from_=-100, to=100, value=current_settings['brightness'])
brightness.grid(row=5, column=1, sticky='ew')
brightness.bind('<ButtonRelease-1>', lambda e: update_setting('brightness', brightness.get()))
# Contrast
ttk.Label(parent, text="Contrast:").grid(row=6, column=0, sticky='w')
contrast = ttk.Scale(parent, from_=-100, to=100, value=current_settings['contrast'])
contrast.grid(row=6, column=1, sticky='ew')
contrast.bind('<ButtonRelease-1>', lambda e: update_setting('contrast', contrast.get()))
# Destination
ttk.Label(parent, text="Destination:").grid(row=7, column=0, sticky='w')
dest_frame = ttk.Frame(parent)
dest_frame.grid(row=7, column=1, sticky='ew')
destination = tk.StringVar(value=current_settings['destination'])
ttk.Radiobutton(dest_frame, text="Screen", variable=destination, value="Screen",
command=lambda: update_setting('destination', destination.get())).pack(side='left')
ttk.Radiobutton(dest_frame, text="File", variable=destination, value="File",
command=lambda: update_setting('destination', destination.get())).pack(side='left')
# File Format
ttk.Label(parent, text="File Format:").grid(row=8, column=0, sticky='w')
file_format = ttk.Combobox(parent, values=['JPEG', 'PNG', 'BMP', 'TIFF'], state='readonly')
file_format.set(current_settings['file_format'])
file_format.grid(row=8, column=1, sticky='ew')
file_format.bind('<<ComboboxSelected>>', lambda e: update_setting('file_format', file_format.get()))
# File Path
ttk.Label(parent, text="File Path:").grid(row=9, column=0, sticky='w')
global file_path_var
file_path_var = tk.StringVar(value=current_settings['file_path'])
path_frame = ttk.Frame(parent)
path_frame.grid(row=9, column=1, sticky='ew')
ttk.Entry(path_frame, textvariable=file_path_var).pack(side='left', fill='x', expand=True)
ttk.Button(path_frame, text="Browse...", command=browse_file).pack(side='left')
# Scan Button
ttk.Button(parent, text="Scan", command=scan).grid(row=10, column=0, columnspan=2, pady=10)
# ADF Test Button
ttk.Button(parent, text="Test ADF Support", command=test_adf_support).grid(row=11, column=0, columnspan=2, pady=5)
# Main application setup
logging.basicConfig(level=logging.DEBUG)
root = tk.Tk()
root.title("Scanner Application with ADF Test")
# Main frame
main_frame = ttk.Frame(root, padding=10)
main_frame.pack(fill='both', expand=True)
# Settings panel on the left
settings_frame = ttk.LabelFrame(main_frame, text="Scanner Settings", padding=10)
settings_frame.pack(side='left', fill='y')
# Image display area on the right
image_frame = ttk.Frame(main_frame)
image_frame.pack(side='right', fill='both', expand=True)
create_settings_panel(settings_frame)
root.mainloop()
I have this for only flat bet mode.
But I want ADF mode using python.
Anybody experieced?