#!/usr/bin/env python3

#   GIMP 3.0 plug-in for scanning via XSane
#   Copyright (C) 2024-2025  Lee Yingtong Li (RunasSudo)
#
#   This program is free software: you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program.  If not, see <https://www.gnu.org/licenses/>.

import gi
gi.require_version('Gimp', '3.0')
from gi.repository import Gimp
from gi.repository import GObject
from gi.repository import Gio

import os
import subprocess
import sys
import tempfile

DEVICE_NAME = None  # e.g. DEVICE_NAME = 'escl:http://10.0.0.1:80'
SCAN_MULTIPLE = True

def xsanecli_run(procedure, config, run_data, *args):
	# Get temporary directory
	tempdir = tempfile.mkdtemp('gimp-plugin-xsanecli')
	png_out = os.path.join(tempdir, 'out.png')
	
	xsane_env = dict(os.environb)
	
	# Fix if DISPLAY is set to a Wayland display
	# For some reason, in Wayland contexts, DISPLAY is clobbered with WAYLAND_DISPLAY when Python is called
	# This causes "Gtk-WARNING: cannot open display: wayland-0"
	if b'DISPLAY' in xsane_env and xsane_env[b'DISPLAY'].startswith(b'wayland-'):
		# Recover the original environment from GIMP (the parent process)
		with open('/proc/{}/environ'.format(os.getppid()), 'rb') as f:
			orig_env = {var[:var.index(b'=')]: var[var.index(b'=')+1:] for var in f.read().split(b'\0') if var}
		
		if b'DISPLAY' in orig_env:
			xsane_env[b'DISPLAY'] = orig_env[b'DISPLAY']
	
	# Open XSane
	args = ['xsane', '--save', '--no-mode-selection', '--force-filename', png_out, '--print-filenames'] + ([DEVICE_NAME] if DEVICE_NAME else [])
	proc = subprocess.Popen(args, stdout=subprocess.PIPE, encoding='utf-8', env=xsane_env)
	
	while True:
		# Wait until XSane prints the name of the scanned file, indicating scanning is finished
		# This blocks Python but that is ok because GIMP UI is not affected
		result = proc.stdout.readline().strip()
		
		if result == '':
			# XSane was closed
			break
		
		if result != 'XSANE_IMAGE_FILENAME: ' + png_out:
			Gimp.message('Unexpected XSane result')
			return Gimp.ValueArray.new_from_values([GObject.Value(Gimp.PDBStatusType, Gimp.PDBStatusType.EXECUTION_ERROR)])
		
		# Open image
		image = Gimp.file_load(Gimp.RunMode.NONINTERACTIVE, Gio.File.new_for_path(png_out))
		Gimp.Display.new(image)
		
		# Remove temporary files
		os.unlink(png_out)
		
		if not SCAN_MULTIPLE:
			proc.terminate()
			break
	
	os.rmdir(tempdir)
	
	return Gimp.ValueArray.new_from_values([GObject.Value(Gimp.PDBStatusType, Gimp.PDBStatusType.SUCCESS), GObject.Value(Gimp.Image.__gtype__, image)])

class XSaneCLI(Gimp.PlugIn):
	## GimpPlugIn virtual methods ##
	def do_set_i18n(self, procname):
		return False
	
	def do_query_procedures(self):
		return ['plug-in-xsanecli']
	
	def do_create_procedure(self, name):
		if name == 'plug-in-xsanecli':
			procedure = Gimp.Procedure.new(self, name, Gimp.PDBProcType.PLUGIN, xsanecli_run, None, None)
			procedure.set_menu_label('_XSane via CLI...')
			procedure.add_menu_path('<Image>/File/Create')
			procedure.add_enum_argument('run-mode', 'Run mode', 'The run mode', Gimp.RunMode.__gtype__, Gimp.RunMode.NONINTERACTIVE, GObject.ParamFlags.READWRITE)
			procedure.add_image_return_value('image', 'Image', 'Output image', False, GObject.ParamFlags.READWRITE)
		else:
			raise Exception('Unknown procedure')
		
		procedure.set_attribution(
			'Lee Yingtong Li (RunasSudo)',  # Author
			'Lee Yingtong Li (RunasSudo)',  # Copyright
			'Lee Yingtong Li (RunasSudo)'   # Year
		)
		return procedure

Gimp.main(XSaneCLI.__gtype__, sys.argv)
