summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Makefile.am35
-rw-r--r--src/Makefile.in661
-rw-r--r--src/book-view.c743
-rw-r--r--src/book-view.h65
-rw-r--r--src/book.c466
-rw-r--r--src/book.h60
-rw-r--r--src/page-view.c1170
-rw-r--r--src/page-view.h79
-rw-r--r--src/page.c951
-rw-r--r--src/page.h121
-rw-r--r--src/scanner.c1630
-rw-r--r--src/scanner.h134
-rw-r--r--src/simple-scan.c633
-rw-r--r--src/ui.c1648
-rw-r--r--src/ui.h71
15 files changed, 8467 insertions, 0 deletions
diff --git a/src/Makefile.am b/src/Makefile.am
new file mode 100644
index 0000000..e6654d0
--- /dev/null
+++ b/src/Makefile.am
@@ -0,0 +1,35 @@
+bin_PROGRAMS = simple-scan
+
+simple_scan_SOURCES = \
+ book.c \
+ book.h \
+ book-view.c \
+ book-view.h \
+ page.c \
+ page.h \
+ page-view.c \
+ page-view.h \
+ simple-scan.c \
+ scanner.c \
+ scanner.h \
+ ui.c \
+ ui.h
+
+simple_scan_CFLAGS = \
+ $(SIMPLE_SCAN_CFLAGS) \
+ $(WARN_CFLAGS) \
+ -DVERSION=\"$(VERSION)\" \
+ -DGETTEXT_PACKAGE=\"$(GETTEXT_PACKAGE)\" \
+ -DLOCALE_DIR=\"$(localedir)\" \
+ -DUI_DIR=\"$(datadir)/simple-scan/\" \
+ -DICON_DIR=\"$(datadir)/simple-scan/icons\" \
+ -DGCONF_DIR=\"/apps/simple-scan\" \
+ -DSIMPLE_SCAN_BINARY=\"simple-scan\"
+
+simple_scan_LDADD = \
+ $(SIMPLE_SCAN_LIBS) \
+ -lsane \
+ -lm
+
+DISTCLEANFILES = \
+ Makefile.in
diff --git a/src/Makefile.in b/src/Makefile.in
new file mode 100644
index 0000000..5665293
--- /dev/null
+++ b/src/Makefile.in
@@ -0,0 +1,661 @@
+# Makefile.in generated by automake 1.11.1 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994, 1995, 1996, 1997, 1998, 1999, 2000, 2001, 2002,
+# 2003, 2004, 2005, 2006, 2007, 2008, 2009 Free Software Foundation,
+# Inc.
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+VPATH = @srcdir@
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+bin_PROGRAMS = simple-scan$(EXEEXT)
+subdir = src
+DIST_COMMON = $(srcdir)/Makefile.am $(srcdir)/Makefile.in
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+mkinstalldirs = $(SHELL) $(top_srcdir)/mkinstalldirs
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+am__installdirs = "$(DESTDIR)$(bindir)"
+PROGRAMS = $(bin_PROGRAMS)
+am_simple_scan_OBJECTS = simple_scan-book.$(OBJEXT) \
+ simple_scan-book-view.$(OBJEXT) simple_scan-page.$(OBJEXT) \
+ simple_scan-page-view.$(OBJEXT) \
+ simple_scan-simple-scan.$(OBJEXT) \
+ simple_scan-scanner.$(OBJEXT) simple_scan-ui.$(OBJEXT)
+simple_scan_OBJECTS = $(am_simple_scan_OBJECTS)
+am__DEPENDENCIES_1 =
+simple_scan_DEPENDENCIES = $(am__DEPENDENCIES_1)
+simple_scan_LINK = $(CCLD) $(simple_scan_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+DEFAULT_INCLUDES = -I.@am__isrc@
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__depfiles_maybe = depfiles
+am__mv = mv -f
+AM_V_lt = $(am__v_lt_$(V))
+am__v_lt_ = $(am__v_lt_$(AM_DEFAULT_VERBOSITY))
+am__v_lt_0 = --silent
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_$(V))
+am__v_CC_ = $(am__v_CC_$(AM_DEFAULT_VERBOSITY))
+am__v_CC_0 = @echo " CC " $@;
+AM_V_at = $(am__v_at_$(V))
+am__v_at_ = $(am__v_at_$(AM_DEFAULT_VERBOSITY))
+am__v_at_0 = @
+CCLD = $(CC)
+LINK = $(CCLD) $(AM_CFLAGS) $(CFLAGS) $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_$(V))
+am__v_CCLD_ = $(am__v_CCLD_$(AM_DEFAULT_VERBOSITY))
+am__v_CCLD_0 = @echo " CCLD " $@;
+AM_V_GEN = $(am__v_GEN_$(V))
+am__v_GEN_ = $(am__v_GEN_$(AM_DEFAULT_VERBOSITY))
+am__v_GEN_0 = @echo " GEN " $@;
+SOURCES = $(simple_scan_SOURCES)
+DIST_SOURCES = $(simple_scan_SOURCES)
+ETAGS = etags
+CTAGS = ctags
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+ALL_LINGUAS = @ALL_LINGUAS@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+CATALOGS = @CATALOGS@
+CATOBJEXT = @CATOBJEXT@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CFLAGS = @CFLAGS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DATADIRNAME = @DATADIRNAME@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DISABLE_DEPRECATED = @DISABLE_DEPRECATED@
+DISTCHECK_CONFIGURE_FLAGS = @DISTCHECK_CONFIGURE_FLAGS@
+DOC_USER_FORMATS = @DOC_USER_FORMATS@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+GCONF_SCHEMA_CONFIG_SOURCE = @GCONF_SCHEMA_CONFIG_SOURCE@
+GCONF_SCHEMA_FILE_DIR = @GCONF_SCHEMA_FILE_DIR@
+GETTEXT_PACKAGE = @GETTEXT_PACKAGE@
+GMOFILES = @GMOFILES@
+GMSGFMT = @GMSGFMT@
+GREP = @GREP@
+HELP_DIR = @HELP_DIR@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+INSTOBJEXT = @INSTOBJEXT@
+INTLLIBS = @INTLLIBS@
+INTLTOOL_EXTRACT = @INTLTOOL_EXTRACT@
+INTLTOOL_MERGE = @INTLTOOL_MERGE@
+INTLTOOL_PERL = @INTLTOOL_PERL@
+INTLTOOL_UPDATE = @INTLTOOL_UPDATE@
+LDFLAGS = @LDFLAGS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LN_S = @LN_S@
+LTLIBOBJS = @LTLIBOBJS@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MKDIR_P = @MKDIR_P@
+MKINSTALLDIRS = @MKINSTALLDIRS@
+MSGFMT = @MSGFMT@
+MSGFMT_OPTS = @MSGFMT_OPTS@
+MSGMERGE = @MSGMERGE@
+OBJEXT = @OBJEXT@
+OMF_DIR = @OMF_DIR@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PKG_CONFIG = @PKG_CONFIG@
+POFILES = @POFILES@
+POSUB = @POSUB@
+PO_IN_DATADIR_FALSE = @PO_IN_DATADIR_FALSE@
+PO_IN_DATADIR_TRUE = @PO_IN_DATADIR_TRUE@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SIMPLE_SCAN_CFLAGS = @SIMPLE_SCAN_CFLAGS@
+SIMPLE_SCAN_LIBS = @SIMPLE_SCAN_LIBS@
+STRIP = @STRIP@
+USE_NLS = @USE_NLS@
+VERSION = @VERSION@
+WARN_CFLAGS = @WARN_CFLAGS@
+XGETTEXT = @XGETTEXT@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_CC = @ac_ct_CC@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build_alias = @build_alias@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host_alias = @host_alias@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+srcdir = @srcdir@
+sysconfdir = @sysconfdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+simple_scan_SOURCES = \
+ book.c \
+ book.h \
+ book-view.c \
+ book-view.h \
+ page.c \
+ page.h \
+ page-view.c \
+ page-view.h \
+ simple-scan.c \
+ scanner.c \
+ scanner.h \
+ ui.c \
+ ui.h
+
+simple_scan_CFLAGS = \
+ $(SIMPLE_SCAN_CFLAGS) \
+ $(WARN_CFLAGS) \
+ -DVERSION=\"$(VERSION)\" \
+ -DGETTEXT_PACKAGE=\"$(GETTEXT_PACKAGE)\" \
+ -DLOCALE_DIR=\"$(localedir)\" \
+ -DUI_DIR=\"$(datadir)/simple-scan/\" \
+ -DICON_DIR=\"$(datadir)/simple-scan/icons\" \
+ -DGCONF_DIR=\"/apps/simple-scan\" \
+ -DSIMPLE_SCAN_BINARY=\"simple-scan\"
+
+simple_scan_LDADD = \
+ $(SIMPLE_SCAN_LIBS) \
+ -lsane \
+ -lm
+
+DISTCLEANFILES = \
+ Makefile.in
+
+all: all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .o .obj
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --gnu src/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --gnu src/Makefile
+.PRECIOUS: Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__depfiles_maybe)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__depfiles_maybe);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+install-binPROGRAMS: $(bin_PROGRAMS)
+ @$(NORMAL_INSTALL)
+ test -z "$(bindir)" || $(MKDIR_P) "$(DESTDIR)$(bindir)"
+ @list='$(bin_PROGRAMS)'; test -n "$(bindir)" || list=; \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed 's/$(EXEEXT)$$//' | \
+ while read p p1; do if test -f $$p; \
+ then echo "$$p"; echo "$$p"; else :; fi; \
+ done | \
+ sed -e 'p;s,.*/,,;n;h' -e 's|.*|.|' \
+ -e 'p;x;s,.*/,,;s/$(EXEEXT)$$//;$(transform);s/$$/$(EXEEXT)/' | \
+ sed 'N;N;N;s,\n, ,g' | \
+ $(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1 } \
+ { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \
+ if ($$2 == $$4) files[d] = files[d] " " $$1; \
+ else { print "f", $$3 "/" $$4, $$1; } } \
+ END { for (d in files) print "f", d, files[d] }' | \
+ while read type dir files; do \
+ if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \
+ test -z "$$files" || { \
+ echo " $(INSTALL_PROGRAM_ENV) $(INSTALL_PROGRAM) $$files '$(DESTDIR)$(bindir)$$dir'"; \
+ $(INSTALL_PROGRAM_ENV) $(INSTALL_PROGRAM) $$files "$(DESTDIR)$(bindir)$$dir" || exit $$?; \
+ } \
+ ; done
+
+uninstall-binPROGRAMS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(bin_PROGRAMS)'; test -n "$(bindir)" || list=; \
+ files=`for p in $$list; do echo "$$p"; done | \
+ sed -e 'h;s,^.*/,,;s/$(EXEEXT)$$//;$(transform)' \
+ -e 's/$$/$(EXEEXT)/' `; \
+ test -n "$$list" || exit 0; \
+ echo " ( cd '$(DESTDIR)$(bindir)' && rm -f" $$files ")"; \
+ cd "$(DESTDIR)$(bindir)" && rm -f $$files
+
+clean-binPROGRAMS:
+ -test -z "$(bin_PROGRAMS)" || rm -f $(bin_PROGRAMS)
+simple-scan$(EXEEXT): $(simple_scan_OBJECTS) $(simple_scan_DEPENDENCIES)
+ @rm -f simple-scan$(EXEEXT)
+ $(AM_V_CCLD)$(simple_scan_LINK) $(simple_scan_OBJECTS) $(simple_scan_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/simple_scan-book-view.Po@am__quote@
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/simple_scan-book.Po@am__quote@
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/simple_scan-page-view.Po@am__quote@
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/simple_scan-page.Po@am__quote@
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/simple_scan-scanner.Po@am__quote@
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/simple_scan-simple-scan.Po@am__quote@
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/simple_scan-ui.Po@am__quote@
+
+.c.o:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@am__fastdepCC_FALSE@ $(AM_V_CC) @AM_BACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(COMPILE) -c $<
+
+.c.obj:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@am__fastdepCC_FALSE@ $(AM_V_CC) @AM_BACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(COMPILE) -c `$(CYGPATH_W) '$<'`
+
+simple_scan-book.o: book.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(simple_scan_CFLAGS) $(CFLAGS) -MT simple_scan-book.o -MD -MP -MF $(DEPDIR)/simple_scan-book.Tpo -c -o simple_scan-book.o `test -f 'book.c' || echo '$(srcdir)/'`book.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/simple_scan-book.Tpo $(DEPDIR)/simple_scan-book.Po
+@am__fastdepCC_FALSE@ $(AM_V_CC) @AM_BACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ source='book.c' object='simple_scan-book.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(simple_scan_CFLAGS) $(CFLAGS) -c -o simple_scan-book.o `test -f 'book.c' || echo '$(srcdir)/'`book.c
+
+simple_scan-book.obj: book.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(simple_scan_CFLAGS) $(CFLAGS) -MT simple_scan-book.obj -MD -MP -MF $(DEPDIR)/simple_scan-book.Tpo -c -o simple_scan-book.obj `if test -f 'book.c'; then $(CYGPATH_W) 'book.c'; else $(CYGPATH_W) '$(srcdir)/book.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/simple_scan-book.Tpo $(DEPDIR)/simple_scan-book.Po
+@am__fastdepCC_FALSE@ $(AM_V_CC) @AM_BACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ source='book.c' object='simple_scan-book.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(simple_scan_CFLAGS) $(CFLAGS) -c -o simple_scan-book.obj `if test -f 'book.c'; then $(CYGPATH_W) 'book.c'; else $(CYGPATH_W) '$(srcdir)/book.c'; fi`
+
+simple_scan-book-view.o: book-view.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(simple_scan_CFLAGS) $(CFLAGS) -MT simple_scan-book-view.o -MD -MP -MF $(DEPDIR)/simple_scan-book-view.Tpo -c -o simple_scan-book-view.o `test -f 'book-view.c' || echo '$(srcdir)/'`book-view.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/simple_scan-book-view.Tpo $(DEPDIR)/simple_scan-book-view.Po
+@am__fastdepCC_FALSE@ $(AM_V_CC) @AM_BACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ source='book-view.c' object='simple_scan-book-view.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(simple_scan_CFLAGS) $(CFLAGS) -c -o simple_scan-book-view.o `test -f 'book-view.c' || echo '$(srcdir)/'`book-view.c
+
+simple_scan-book-view.obj: book-view.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(simple_scan_CFLAGS) $(CFLAGS) -MT simple_scan-book-view.obj -MD -MP -MF $(DEPDIR)/simple_scan-book-view.Tpo -c -o simple_scan-book-view.obj `if test -f 'book-view.c'; then $(CYGPATH_W) 'book-view.c'; else $(CYGPATH_W) '$(srcdir)/book-view.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/simple_scan-book-view.Tpo $(DEPDIR)/simple_scan-book-view.Po
+@am__fastdepCC_FALSE@ $(AM_V_CC) @AM_BACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ source='book-view.c' object='simple_scan-book-view.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(simple_scan_CFLAGS) $(CFLAGS) -c -o simple_scan-book-view.obj `if test -f 'book-view.c'; then $(CYGPATH_W) 'book-view.c'; else $(CYGPATH_W) '$(srcdir)/book-view.c'; fi`
+
+simple_scan-page.o: page.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(simple_scan_CFLAGS) $(CFLAGS) -MT simple_scan-page.o -MD -MP -MF $(DEPDIR)/simple_scan-page.Tpo -c -o simple_scan-page.o `test -f 'page.c' || echo '$(srcdir)/'`page.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/simple_scan-page.Tpo $(DEPDIR)/simple_scan-page.Po
+@am__fastdepCC_FALSE@ $(AM_V_CC) @AM_BACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ source='page.c' object='simple_scan-page.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(simple_scan_CFLAGS) $(CFLAGS) -c -o simple_scan-page.o `test -f 'page.c' || echo '$(srcdir)/'`page.c
+
+simple_scan-page.obj: page.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(simple_scan_CFLAGS) $(CFLAGS) -MT simple_scan-page.obj -MD -MP -MF $(DEPDIR)/simple_scan-page.Tpo -c -o simple_scan-page.obj `if test -f 'page.c'; then $(CYGPATH_W) 'page.c'; else $(CYGPATH_W) '$(srcdir)/page.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/simple_scan-page.Tpo $(DEPDIR)/simple_scan-page.Po
+@am__fastdepCC_FALSE@ $(AM_V_CC) @AM_BACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ source='page.c' object='simple_scan-page.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(simple_scan_CFLAGS) $(CFLAGS) -c -o simple_scan-page.obj `if test -f 'page.c'; then $(CYGPATH_W) 'page.c'; else $(CYGPATH_W) '$(srcdir)/page.c'; fi`
+
+simple_scan-page-view.o: page-view.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(simple_scan_CFLAGS) $(CFLAGS) -MT simple_scan-page-view.o -MD -MP -MF $(DEPDIR)/simple_scan-page-view.Tpo -c -o simple_scan-page-view.o `test -f 'page-view.c' || echo '$(srcdir)/'`page-view.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/simple_scan-page-view.Tpo $(DEPDIR)/simple_scan-page-view.Po
+@am__fastdepCC_FALSE@ $(AM_V_CC) @AM_BACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ source='page-view.c' object='simple_scan-page-view.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(simple_scan_CFLAGS) $(CFLAGS) -c -o simple_scan-page-view.o `test -f 'page-view.c' || echo '$(srcdir)/'`page-view.c
+
+simple_scan-page-view.obj: page-view.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(simple_scan_CFLAGS) $(CFLAGS) -MT simple_scan-page-view.obj -MD -MP -MF $(DEPDIR)/simple_scan-page-view.Tpo -c -o simple_scan-page-view.obj `if test -f 'page-view.c'; then $(CYGPATH_W) 'page-view.c'; else $(CYGPATH_W) '$(srcdir)/page-view.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/simple_scan-page-view.Tpo $(DEPDIR)/simple_scan-page-view.Po
+@am__fastdepCC_FALSE@ $(AM_V_CC) @AM_BACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ source='page-view.c' object='simple_scan-page-view.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(simple_scan_CFLAGS) $(CFLAGS) -c -o simple_scan-page-view.obj `if test -f 'page-view.c'; then $(CYGPATH_W) 'page-view.c'; else $(CYGPATH_W) '$(srcdir)/page-view.c'; fi`
+
+simple_scan-simple-scan.o: simple-scan.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(simple_scan_CFLAGS) $(CFLAGS) -MT simple_scan-simple-scan.o -MD -MP -MF $(DEPDIR)/simple_scan-simple-scan.Tpo -c -o simple_scan-simple-scan.o `test -f 'simple-scan.c' || echo '$(srcdir)/'`simple-scan.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/simple_scan-simple-scan.Tpo $(DEPDIR)/simple_scan-simple-scan.Po
+@am__fastdepCC_FALSE@ $(AM_V_CC) @AM_BACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ source='simple-scan.c' object='simple_scan-simple-scan.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(simple_scan_CFLAGS) $(CFLAGS) -c -o simple_scan-simple-scan.o `test -f 'simple-scan.c' || echo '$(srcdir)/'`simple-scan.c
+
+simple_scan-simple-scan.obj: simple-scan.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(simple_scan_CFLAGS) $(CFLAGS) -MT simple_scan-simple-scan.obj -MD -MP -MF $(DEPDIR)/simple_scan-simple-scan.Tpo -c -o simple_scan-simple-scan.obj `if test -f 'simple-scan.c'; then $(CYGPATH_W) 'simple-scan.c'; else $(CYGPATH_W) '$(srcdir)/simple-scan.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/simple_scan-simple-scan.Tpo $(DEPDIR)/simple_scan-simple-scan.Po
+@am__fastdepCC_FALSE@ $(AM_V_CC) @AM_BACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ source='simple-scan.c' object='simple_scan-simple-scan.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(simple_scan_CFLAGS) $(CFLAGS) -c -o simple_scan-simple-scan.obj `if test -f 'simple-scan.c'; then $(CYGPATH_W) 'simple-scan.c'; else $(CYGPATH_W) '$(srcdir)/simple-scan.c'; fi`
+
+simple_scan-scanner.o: scanner.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(simple_scan_CFLAGS) $(CFLAGS) -MT simple_scan-scanner.o -MD -MP -MF $(DEPDIR)/simple_scan-scanner.Tpo -c -o simple_scan-scanner.o `test -f 'scanner.c' || echo '$(srcdir)/'`scanner.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/simple_scan-scanner.Tpo $(DEPDIR)/simple_scan-scanner.Po
+@am__fastdepCC_FALSE@ $(AM_V_CC) @AM_BACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ source='scanner.c' object='simple_scan-scanner.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(simple_scan_CFLAGS) $(CFLAGS) -c -o simple_scan-scanner.o `test -f 'scanner.c' || echo '$(srcdir)/'`scanner.c
+
+simple_scan-scanner.obj: scanner.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(simple_scan_CFLAGS) $(CFLAGS) -MT simple_scan-scanner.obj -MD -MP -MF $(DEPDIR)/simple_scan-scanner.Tpo -c -o simple_scan-scanner.obj `if test -f 'scanner.c'; then $(CYGPATH_W) 'scanner.c'; else $(CYGPATH_W) '$(srcdir)/scanner.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/simple_scan-scanner.Tpo $(DEPDIR)/simple_scan-scanner.Po
+@am__fastdepCC_FALSE@ $(AM_V_CC) @AM_BACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ source='scanner.c' object='simple_scan-scanner.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(simple_scan_CFLAGS) $(CFLAGS) -c -o simple_scan-scanner.obj `if test -f 'scanner.c'; then $(CYGPATH_W) 'scanner.c'; else $(CYGPATH_W) '$(srcdir)/scanner.c'; fi`
+
+simple_scan-ui.o: ui.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(simple_scan_CFLAGS) $(CFLAGS) -MT simple_scan-ui.o -MD -MP -MF $(DEPDIR)/simple_scan-ui.Tpo -c -o simple_scan-ui.o `test -f 'ui.c' || echo '$(srcdir)/'`ui.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/simple_scan-ui.Tpo $(DEPDIR)/simple_scan-ui.Po
+@am__fastdepCC_FALSE@ $(AM_V_CC) @AM_BACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ source='ui.c' object='simple_scan-ui.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(simple_scan_CFLAGS) $(CFLAGS) -c -o simple_scan-ui.o `test -f 'ui.c' || echo '$(srcdir)/'`ui.c
+
+simple_scan-ui.obj: ui.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(simple_scan_CFLAGS) $(CFLAGS) -MT simple_scan-ui.obj -MD -MP -MF $(DEPDIR)/simple_scan-ui.Tpo -c -o simple_scan-ui.obj `if test -f 'ui.c'; then $(CYGPATH_W) 'ui.c'; else $(CYGPATH_W) '$(srcdir)/ui.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/simple_scan-ui.Tpo $(DEPDIR)/simple_scan-ui.Po
+@am__fastdepCC_FALSE@ $(AM_V_CC) @AM_BACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ source='ui.c' object='simple_scan-ui.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(simple_scan_CFLAGS) $(CFLAGS) -c -o simple_scan-ui.obj `if test -f 'ui.c'; then $(CYGPATH_W) 'ui.c'; else $(CYGPATH_W) '$(srcdir)/ui.c'; fi`
+
+ID: $(HEADERS) $(SOURCES) $(LISP) $(TAGS_FILES)
+ list='$(SOURCES) $(HEADERS) $(LISP) $(TAGS_FILES)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | \
+ $(AWK) '{ files[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in files) print i; }; }'`; \
+ mkid -fID $$unique
+tags: TAGS
+
+TAGS: $(HEADERS) $(SOURCES) $(TAGS_DEPENDENCIES) \
+ $(TAGS_FILES) $(LISP)
+ set x; \
+ here=`pwd`; \
+ list='$(SOURCES) $(HEADERS) $(LISP) $(TAGS_FILES)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | \
+ $(AWK) '{ files[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in files) print i; }; }'`; \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: CTAGS
+CTAGS: $(HEADERS) $(SOURCES) $(TAGS_DEPENDENCIES) \
+ $(TAGS_FILES) $(LISP)
+ list='$(SOURCES) $(HEADERS) $(LISP) $(TAGS_FILES)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | \
+ $(AWK) '{ files[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in files) print i; }; }'`; \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+distdir: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+check: check-am
+all-am: Makefile $(PROGRAMS)
+installdirs:
+ for dir in "$(DESTDIR)$(bindir)"; do \
+ test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+ done
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ `test -z '$(STRIP)' || \
+ echo "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'"` install
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+ -test -z "$(DISTCLEANFILES)" || rm -f $(DISTCLEANFILES)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-binPROGRAMS clean-generic mostlyclean-am
+
+distclean: distclean-am
+ -rm -rf ./$(DEPDIR)
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am:
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am: install-binPROGRAMS
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+ -rm -rf ./$(DEPDIR)
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-binPROGRAMS
+
+.MAKE: install-am install-strip
+
+.PHONY: CTAGS GTAGS all all-am check check-am clean clean-binPROGRAMS \
+ clean-generic ctags distclean distclean-compile \
+ distclean-generic distclean-tags distdir dvi dvi-am html \
+ html-am info info-am install install-am install-binPROGRAMS \
+ install-data install-data-am install-dvi install-dvi-am \
+ install-exec install-exec-am install-html install-html-am \
+ install-info install-info-am install-man install-pdf \
+ install-pdf-am install-ps install-ps-am install-strip \
+ installcheck installcheck-am installdirs maintainer-clean \
+ maintainer-clean-generic mostlyclean mostlyclean-compile \
+ mostlyclean-generic pdf pdf-am ps ps-am tags uninstall \
+ uninstall-am uninstall-binPROGRAMS
+
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/book-view.c b/src/book-view.c
new file mode 100644
index 0000000..e45e22c
--- /dev/null
+++ b/src/book-view.c
@@ -0,0 +1,743 @@
+/*
+ * Copyright (C) 2009 Canonical Ltd.
+ * Author: Robert Ancell <robert.ancell@canonical.com>
+ *
+ * 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. See http://www.gnu.org/copyleft/gpl.html the full text of the
+ * license.
+ */
+
+#include <gdk/gdkkeysyms.h>
+
+#include "book-view.h"
+#include "page-view.h"
+
+// FIXME: When scrolling, copy existing render sideways?
+// FIXME: Only render pages that change and only the part that changed
+
+enum {
+ PAGE_SELECTED,
+ SHOW_PAGE,
+ LAST_SIGNAL
+};
+static guint signals[LAST_SIGNAL] = { 0, };
+
+struct BookViewPrivate
+{
+ /* Book being rendered */
+ Book *book;
+ GHashTable *page_data;
+
+ /* True if the view needs to be laid out again */
+ gboolean need_layout, laying_out, show_selected_page;
+
+ /* Currently selected page */
+ PageView *selected_page;
+
+ /* Widget being rendered to */
+ GtkWidget *widget;
+
+ /* Horizontal adjustment */
+ GtkAdjustment *adjustment;
+
+ GtkWidget *box, *scroll;
+
+ GtkWidget *page_menu;
+
+ gint cursor;
+};
+
+G_DEFINE_TYPE (BookView, book_view, G_TYPE_OBJECT);
+
+
+BookView *
+book_view_new ()
+{
+ return g_object_new (BOOK_VIEW_TYPE, NULL);
+}
+
+
+static PageView *
+get_nth_page (BookView *view, gint n)
+{
+ Page *page = book_get_page (view->priv->book, n);
+ return g_hash_table_lookup (view->priv->page_data, page);
+}
+
+
+static PageView *
+get_next_page (BookView *view, PageView *page)
+{
+ gint i;
+
+ for (i = 0; ; i++) {
+ Page *p;
+ p = book_get_page (view->priv->book, i);
+ if (!p)
+ break;
+ if (p == page_view_get_page (page)) {
+ p = book_get_page (view->priv->book, i + 1);
+ if (p)
+ return g_hash_table_lookup (view->priv->page_data, p);
+ }
+ }
+
+ return page;
+}
+
+
+static PageView *
+get_prev_page (BookView *view, PageView *page)
+{
+ gint i;
+ PageView *prev_page = page;
+
+ for (i = 0; ; i++) {
+ Page *p;
+ p = book_get_page (view->priv->book, i);
+ if (!p)
+ break;
+ if (p == page_view_get_page (page))
+ return prev_page;
+ prev_page = g_hash_table_lookup (view->priv->page_data, p);
+ }
+
+ return page;
+}
+
+
+static void
+page_view_changed_cb (PageView *page, BookView *view)
+{
+ book_view_redraw (view);
+}
+
+
+static void
+page_view_size_changed_cb (PageView *page, BookView *view)
+{
+ view->priv->need_layout = TRUE;
+ book_view_redraw (view);
+}
+
+
+static void
+add_cb (Book *book, Page *page, BookView *view)
+{
+ PageView *page_view;
+ page_view = page_view_new (page);
+ g_signal_connect (page_view, "changed", G_CALLBACK (page_view_changed_cb), view);
+ g_signal_connect (page_view, "size-changed", G_CALLBACK (page_view_size_changed_cb), view);
+ g_hash_table_insert (view->priv->page_data, page, page_view);
+ view->priv->need_layout = TRUE;
+ book_view_redraw (view);
+}
+
+
+static void
+set_selected_page (BookView *view, PageView *page)
+{
+ /* Deselect existing page if changed */
+ if (view->priv->selected_page && page != view->priv->selected_page)
+ page_view_set_selected (view->priv->selected_page, FALSE);
+
+ view->priv->selected_page = page;
+ if (!view->priv->selected_page)
+ return;
+
+ /* Select new page if widget has focus */
+ if (!gtk_widget_has_focus (view->priv->widget))
+ page_view_set_selected (view->priv->selected_page, FALSE);
+ else
+ page_view_set_selected (view->priv->selected_page, TRUE);
+}
+
+
+static void
+set_x_offset (BookView *view, gint offset)
+{
+ gtk_adjustment_set_value (view->priv->adjustment, offset);
+}
+
+
+static gint
+get_x_offset (BookView *view)
+{
+ return (gint) gtk_adjustment_get_value (view->priv->adjustment);
+}
+
+
+static void
+show_page (BookView *view, PageView *page)
+{
+ gint left_edge, right_edge;
+
+ if (!page || !gtk_widget_get_visible (view->priv->scroll))
+ return;
+
+ left_edge = page_view_get_x_offset (page);
+ right_edge = page_view_get_x_offset (page) + page_view_get_width (page);
+
+ if (left_edge - get_x_offset (view) < 0)
+ set_x_offset(view, left_edge);
+ else if (right_edge - get_x_offset (view) > view->priv->widget->allocation.width)
+ set_x_offset(view, right_edge - view->priv->widget->allocation.width);
+}
+
+
+static void
+select_page (BookView *view, PageView *page)
+{
+ Page *p = NULL;
+
+ if (view->priv->selected_page == page)
+ return;
+
+ set_selected_page (view, page);
+
+ if (view->priv->need_layout)
+ view->priv->show_selected_page = TRUE;
+ else
+ show_page (view, page);
+
+ if (page)
+ p = page_view_get_page (page);
+ g_signal_emit (view, signals[PAGE_SELECTED], 0, p);
+}
+
+
+static void
+remove_cb (Book *book, Page *page, BookView *view)
+{
+ PageView *new_selection = view->priv->selected_page;
+
+ /* Select previous page or next if removing the selected page */
+ if (page == book_view_get_selected (view)) {
+ new_selection = get_prev_page (view, view->priv->selected_page);
+ if (new_selection == view->priv->selected_page)
+ new_selection = get_next_page (view, view->priv->selected_page);
+ view->priv->selected_page = NULL;
+ }
+
+ g_hash_table_remove (view->priv->page_data, page);
+
+ select_page (view, new_selection);
+
+ view->priv->need_layout = TRUE;
+ book_view_redraw (view);
+}
+
+
+static void
+clear_cb (Book *book, BookView *view)
+{
+ g_hash_table_remove_all (view->priv->page_data);
+ view->priv->selected_page = NULL;
+ g_signal_emit (view, signals[PAGE_SELECTED], 0, NULL);
+ view->priv->need_layout = TRUE;
+ book_view_redraw (view);
+}
+
+
+void
+book_view_set_book (BookView *view, Book *book)
+{
+ gint i, n_pages;
+
+ g_return_if_fail (view != NULL);
+ g_return_if_fail (book != NULL);
+
+ view->priv->book = g_object_ref (book);
+
+ /* Load existing pages */
+ n_pages = book_get_n_pages (view->priv->book);
+ for (i = 0; i < n_pages; i++) {
+ Page *page = book_get_page (book, i);
+ add_cb (book, page, view);
+ }
+
+ book_view_select_page (view, book_get_page (book, 0));
+
+ /* Watch for new pages */
+ g_signal_connect (book, "page-added", G_CALLBACK (add_cb), view);
+ g_signal_connect (book, "page-removed", G_CALLBACK (remove_cb), view);
+ g_signal_connect (book, "cleared", G_CALLBACK (clear_cb), view);
+}
+
+
+Book *
+book_view_get_book (BookView *view)
+{
+ g_return_val_if_fail (view != NULL, NULL);
+
+ return view->priv->book;
+}
+
+
+static gboolean
+configure_cb (GtkWidget *widget, GdkEventConfigure *event, BookView *view)
+{
+ view->priv->need_layout = TRUE;
+ return FALSE;
+}
+
+
+static void
+layout_into (BookView *view, gint width, gint height, gint *book_width, gint *book_height)
+{
+ gint spacing = 12;
+ gint max_width = 0, max_height = 0;
+ gdouble aspect, max_aspect;
+ gint x_offset = 0;
+ gint i, n_pages;
+ gint max_dpi = 0;
+
+ n_pages = book_get_n_pages (view->priv->book);
+
+ /* Get maximum page resolution */
+ for (i = 0; i < n_pages; i++) {
+ Page *page = book_get_page (view->priv->book, i);
+ if (page_get_dpi (page) > max_dpi)
+ max_dpi = page_get_dpi (page);
+ }
+
+ /* Get area required to fit all pages */
+ for (i = 0; i < n_pages; i++) {
+ Page *page = book_get_page (view->priv->book, i);
+ gint w, h;
+
+ w = page_get_width (page);
+ h = page_get_height (page);
+
+ /* Scale to the same DPI */
+ w = (double)w * max_dpi / page_get_dpi (page) + 0.5;
+ h = (double)h * max_dpi / page_get_dpi (page) + 0.5;
+
+ if (w > max_width)
+ max_width = w;
+ if (h > max_height)
+ max_height = h;
+ }
+
+ aspect = (double)width / height;
+ max_aspect = (double)max_width / max_height;
+
+ /* Get total dimensions of all pages */
+ *book_width = 0;
+ *book_height = 0;
+ for (i = 0; i < n_pages; i++) {
+ PageView *page = get_nth_page (view, i);
+ Page *p = page_view_get_page (page);
+ gint h;
+
+ /* NOTE: Using double to avoid overflow for large images */
+ if (max_aspect > aspect) {
+ /* Set width scaled on DPI and maximum width */
+ gint w = (double)page_get_width (p) * max_dpi * width / (page_get_dpi (p) * max_width);
+ page_view_set_width (page, w);
+ }
+ else {
+ /* Set height scaled on DPI and maximum height */
+ gint h = (double)page_get_height (p) * max_dpi * height / (page_get_dpi (p) * max_height);
+ page_view_set_height (page, h);
+ }
+
+ h = page_view_get_height (page);
+ if (h > *book_height)
+ *book_height = h;
+ *book_width += page_view_get_width (page);
+ if (i != 0)
+ *book_width += spacing;
+ }
+
+ for (i = 0; i < n_pages; i++) {
+ PageView *page = get_nth_page (view, i);
+
+ /* Layout pages left to right */
+ page_view_set_x_offset (page, x_offset);
+ x_offset += page_view_get_width (page) + spacing;
+
+ /* Centre page vertically */
+ page_view_set_y_offset (page, (height - page_view_get_height (page)) / 2);
+ }
+}
+
+
+static void
+layout (BookView *view)
+{
+ gint width, height, book_width, book_height;
+ gboolean right_aligned = TRUE;
+
+ if (!view->priv->need_layout)
+ return;
+
+ view->priv->laying_out = TRUE;
+
+ /* If scroll is right aligned then keep that after layout */
+ if (gtk_adjustment_get_value (view->priv->adjustment) < gtk_adjustment_get_upper (view->priv->adjustment) - gtk_adjustment_get_page_size (view->priv->adjustment))
+ right_aligned = FALSE;
+
+ /* Try and fit without scrollbar */
+ width = view->priv->widget->allocation.width;
+ height = view->priv->box->allocation.height;
+ layout_into (view, width, height, &book_width, &book_height);
+
+ /* Relayout with scrollbar */
+ if (book_width > view->priv->widget->allocation.width) {
+ gint max_offset;
+
+ /* Re-layout leaving space for scrollbar */
+ height = view->priv->widget->allocation.height;
+ layout_into (view, width, height, &book_width, &book_height);
+
+ /* Set scrollbar limits */
+ gtk_adjustment_set_lower (view->priv->adjustment, 0);
+ gtk_adjustment_set_upper (view->priv->adjustment, book_width);
+ gtk_adjustment_set_page_size (view->priv->adjustment, view->priv->widget->allocation.width);
+
+ /* Keep right-aligned */
+ max_offset = book_width - view->priv->widget->allocation.width;
+ if (right_aligned || get_x_offset (view) > max_offset)
+ set_x_offset(view, max_offset);
+
+ gtk_widget_show (view->priv->scroll);
+ } else {
+ gint offset;
+ gtk_widget_hide (view->priv->scroll);
+ offset = (book_width - view->priv->widget->allocation.width) / 2;
+ gtk_adjustment_set_lower (view->priv->adjustment, offset);
+ gtk_adjustment_set_upper (view->priv->adjustment, offset);
+ gtk_adjustment_set_page_size (view->priv->adjustment, 0);
+ set_x_offset(view, offset);
+ }
+
+ if (view->priv->show_selected_page)
+ show_page (view, view->priv->selected_page);
+
+ view->priv->need_layout = FALSE;
+ view->priv->show_selected_page = FALSE;
+ view->priv->laying_out = FALSE;
+}
+
+
+static gboolean
+expose_cb (GtkWidget *widget, GdkEventExpose *event, BookView *view)
+{
+ gint i, n_pages;
+ cairo_t *context;
+
+ n_pages = book_get_n_pages (view->priv->book);
+ if (n_pages == 0)
+ return FALSE;
+
+ layout (view);
+
+ context = gdk_cairo_create (widget->window);
+
+ /* Render each page */
+ for (i = 0; i < n_pages; i++) {
+ PageView *page = get_nth_page (view, i);
+ gint left_edge, right_edge;
+
+ left_edge = page_view_get_x_offset (page) - get_x_offset (view);
+ right_edge = page_view_get_x_offset (page) + page_view_get_width (page) - get_x_offset (view);
+
+ /* Page not visible, don't render */
+ if (right_edge < event->area.x || left_edge > event->area.x + event->area.width)
+ continue;
+
+ cairo_save (context);
+ cairo_translate (context, -get_x_offset (view), 0);
+ page_view_render (page, context);
+ cairo_restore (context);
+
+ if (page_view_get_selected (page))
+ gtk_paint_focus (gtk_widget_get_style (view->priv->widget),
+ gtk_widget_get_window (view->priv->widget),
+ GTK_STATE_SELECTED,
+ &event->area,
+ NULL,
+ NULL,
+ page_view_get_x_offset (page) - get_x_offset (view),
+ page_view_get_y_offset (page),
+ page_view_get_width (page),
+ page_view_get_height (page));
+ }
+
+ cairo_destroy (context);
+
+ return FALSE;
+}
+
+
+static PageView *
+get_page_at (BookView *view, gint x, gint y, gint *x_, gint *y_)
+{
+ gint i, n_pages;
+
+ n_pages = book_get_n_pages (view->priv->book);
+ for (i = 0; i < n_pages; i++) {
+ PageView *page;
+ gint left, right, top, bottom;
+
+ page = get_nth_page (view, i);
+ left = page_view_get_x_offset (page);
+ right = left + page_view_get_width (page);
+ top = page_view_get_y_offset (page);
+ bottom = top + page_view_get_height (page);
+ if (x >= left && x <= right && y >= top && y <= bottom)
+ {
+ *x_ = x - left;
+ *y_ = y - top;
+ return page;
+ }
+ }
+
+ return NULL;
+}
+
+
+static gboolean
+button_cb (GtkWidget *widget, GdkEventButton *event, BookView *view)
+{
+ gint x, y;
+
+ layout (view);
+
+ gtk_widget_grab_focus (view->priv->widget);
+
+ if (event->type == GDK_BUTTON_PRESS)
+ select_page (view, get_page_at (view, event->x + get_x_offset (view), event->y, &x, &y));
+
+ if (!view->priv->selected_page)
+ return FALSE;
+
+ /* Modify page */
+ if (event->button == 1) {
+ if (event->type == GDK_BUTTON_PRESS)
+ page_view_button_press (view->priv->selected_page, x, y);
+ else if (event->type == GDK_BUTTON_RELEASE)
+ page_view_button_release (view->priv->selected_page, x, y);
+ else if (event->type == GDK_2BUTTON_PRESS)
+ g_signal_emit (view, signals[SHOW_PAGE], 0, book_view_get_selected (view));
+ }
+
+ /* Show pop-up menu on right click */
+ if (event->button == 3) {
+ gtk_menu_popup (GTK_MENU (view->priv->page_menu), NULL, NULL, NULL, NULL,
+ event->button, event->time);
+ }
+
+ return FALSE;
+}
+
+
+static void
+set_cursor (BookView *view, gint cursor)
+{
+ GdkCursor *c;
+
+ if (view->priv->cursor == cursor)
+ return;
+ view->priv->cursor = cursor;
+
+ c = gdk_cursor_new (cursor);
+ gdk_window_set_cursor (gtk_widget_get_window (view->priv->widget), c);
+ gdk_cursor_destroy (c);
+}
+
+
+static gboolean
+motion_cb (GtkWidget *widget, GdkEventMotion *event, BookView *view)
+{
+ gint x, y;
+ gint cursor = GDK_ARROW;
+
+ /* Dragging */
+ if (view->priv->selected_page && (event->state & GDK_BUTTON1_MASK) != 0) {
+ x = event->x + get_x_offset (view) - page_view_get_x_offset (view->priv->selected_page);
+ y = event->y - page_view_get_y_offset (view->priv->selected_page);
+ page_view_motion (view->priv->selected_page, x, y);
+ cursor = page_view_get_cursor (view->priv->selected_page);
+ }
+ else {
+ PageView *over_page;
+ over_page = get_page_at (view, event->x + get_x_offset (view), event->y, &x, &y);
+ if (over_page) {
+ page_view_motion (over_page, x, y);
+ cursor = page_view_get_cursor (over_page);
+ }
+ }
+
+ set_cursor (view, cursor);
+
+ return FALSE;
+}
+
+
+static gboolean
+key_cb (GtkWidget *widget, GdkEventKey *event, BookView *view)
+{
+ switch (event->keyval) {
+ case GDK_Home:
+ book_view_select_page (view, book_get_page (view->priv->book, 0));
+ return TRUE;
+ case GDK_Left:
+ select_page (view, get_prev_page (view, view->priv->selected_page));
+ return TRUE;
+ case GDK_Right:
+ select_page (view, get_next_page (view, view->priv->selected_page));
+ return TRUE;
+ case GDK_End:
+ book_view_select_page (view, book_get_page (view->priv->book, book_get_n_pages (view->priv->book) - 1));
+ return TRUE;
+
+ default:
+ return FALSE;
+ }
+}
+
+
+static gboolean
+focus_cb (GtkWidget *widget, GdkEventFocus *event, BookView *view)
+{
+ set_selected_page (view, view->priv->selected_page);
+ return FALSE;
+}
+
+
+static void
+scroll_cb (GtkAdjustment *adjustment, BookView *view)
+{
+ if (!view->priv->laying_out)
+ book_view_redraw (view);
+}
+
+
+void
+book_view_set_widgets (BookView *view, GtkWidget *box, GtkWidget *area, GtkWidget *scroll, GtkWidget *page_menu)
+{
+ g_return_if_fail (view != NULL);
+ g_return_if_fail (view->priv->widget == NULL);
+
+ view->priv->widget = area;
+ view->priv->box = box;
+ view->priv->scroll = scroll;
+ view->priv->adjustment = gtk_range_get_adjustment (GTK_RANGE (scroll));
+ view->priv->page_menu = page_menu;
+
+ g_signal_connect (area, "configure-event", G_CALLBACK (configure_cb), view);
+ g_signal_connect (area, "expose-event", G_CALLBACK (expose_cb), view);
+ g_signal_connect (area, "motion-notify-event", G_CALLBACK (motion_cb), view);
+ g_signal_connect (area, "key-press-event", G_CALLBACK (key_cb), view);
+ g_signal_connect (area, "button-press-event", G_CALLBACK (button_cb), view);
+ g_signal_connect (area, "button-release-event", G_CALLBACK (button_cb), view);
+ g_signal_connect_after (area, "focus-in-event", G_CALLBACK (focus_cb), view);
+ g_signal_connect_after (area, "focus-out-event", G_CALLBACK (focus_cb), view);
+ g_signal_connect (view->priv->adjustment, "value-changed", G_CALLBACK (scroll_cb), view);
+}
+
+
+void
+book_view_redraw (BookView *view)
+{
+ g_return_if_fail (view != NULL);
+ gtk_widget_queue_draw (view->priv->widget);
+}
+
+
+void
+book_view_select_page (BookView *view, Page *page)
+{
+ g_return_if_fail (view != NULL);
+
+ if (book_view_get_selected (view) == page)
+ return;
+
+ if (page)
+ select_page (view, g_hash_table_lookup (view->priv->page_data, page));
+ else
+ select_page (view, NULL);
+}
+
+
+void
+book_view_select_next_page (BookView *view)
+{
+ g_return_if_fail (view != NULL);
+ select_page (view, get_next_page (view, view->priv->selected_page));
+}
+
+
+void
+book_view_select_prev_page (BookView *view)
+{
+ g_return_if_fail (view != NULL);
+ select_page (view, get_prev_page (view, view->priv->selected_page));
+}
+
+
+Page *
+book_view_get_selected (BookView *view)
+{
+ g_return_val_if_fail (view != NULL, NULL);
+
+ if (view->priv->selected_page)
+ return page_view_get_page (view->priv->selected_page);
+ else
+ return NULL;
+}
+
+
+static void
+book_view_finalize (GObject *object)
+{
+ BookView *view = BOOK_VIEW (object);
+ g_object_unref (view->priv->book);
+ view->priv->book = NULL;
+ g_hash_table_unref (view->priv->page_data);
+ view->priv->page_data = NULL;
+ G_OBJECT_CLASS (book_view_parent_class)->finalize (object);
+}
+
+
+static void
+book_view_class_init (BookViewClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->finalize = book_view_finalize;
+
+ signals[PAGE_SELECTED] =
+ g_signal_new ("page-selected",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (BookViewClass, page_selected),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__POINTER,
+ G_TYPE_NONE, 1, G_TYPE_POINTER);
+ signals[SHOW_PAGE] =
+ g_signal_new ("show-page",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (BookViewClass, show_page),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__POINTER,
+ G_TYPE_NONE, 1, G_TYPE_POINTER);
+
+ g_type_class_add_private (klass, sizeof (BookViewPrivate));
+}
+
+
+static void
+book_view_init (BookView *view)
+{
+ view->priv = G_TYPE_INSTANCE_GET_PRIVATE (view, BOOK_VIEW_TYPE, BookViewPrivate);
+ view->priv->need_layout = TRUE;
+ view->priv->page_data = g_hash_table_new_full (g_direct_hash, g_direct_equal,
+ NULL, (GDestroyNotify) g_object_unref);
+ view->priv->cursor = GDK_ARROW;
+}
diff --git a/src/book-view.h b/src/book-view.h
new file mode 100644
index 0000000..acc3899
--- /dev/null
+++ b/src/book-view.h
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2009 Canonical Ltd.
+ * Author: Robert Ancell <robert.ancell@canonical.com>
+ *
+ * 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. See http://www.gnu.org/copyleft/gpl.html the full text of the
+ * license.
+ */
+
+#ifndef _BOOK_VIEW_H_
+#define _BOOK_VIEW_H_
+
+#include <glib-object.h>
+#include <gtk/gtk.h>
+#include <cairo.h>
+#include "book.h"
+
+G_BEGIN_DECLS
+
+#define BOOK_VIEW_TYPE (book_view_get_type ())
+#define BOOK_VIEW(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), BOOK_VIEW_TYPE, BookView))
+
+
+typedef struct BookViewPrivate BookViewPrivate;
+
+typedef struct
+{
+ GObject parent_instance;
+ BookViewPrivate *priv;
+} BookView;
+
+typedef struct
+{
+ GObjectClass parent_class;
+
+ void (*page_selected) (BookView *view, Page *page);
+ void (*show_page) (BookView *view, Page *page);
+} BookViewClass;
+
+
+GType book_view_get_type (void);
+
+BookView *book_view_new (void);
+
+// FIXME: Book view should extend GtkVBox
+void book_view_set_widgets (BookView *view, GtkWidget *box, GtkWidget *area, GtkWidget *scroll, GtkWidget *page_menu);
+
+// FIXME: Should be part of book_view_new
+void book_view_set_book (BookView *view, Book *book);
+
+void book_view_redraw (BookView *view);
+
+Book *book_view_get_book (BookView *view);
+
+void book_view_select_page (BookView *view, Page *page);
+
+void book_view_select_next_page (BookView *view);
+
+void book_view_select_prev_page (BookView *view);
+
+Page *book_view_get_selected (BookView *view);
+
+#endif /* _BOOK_VIEW_H_ */
diff --git a/src/book.c b/src/book.c
new file mode 100644
index 0000000..6bf4d0a
--- /dev/null
+++ b/src/book.c
@@ -0,0 +1,466 @@
+/*
+ * Copyright (C) 2009 Canonical Ltd.
+ * Author: Robert Ancell <robert.ancell@canonical.com>
+ *
+ * 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. See http://www.gnu.org/copyleft/gpl.html the full text of the
+ * license.
+ */
+
+#include <string.h>
+#include <math.h>
+#include <gdk/gdk.h>
+#include <gdk-pixbuf/gdk-pixbuf.h>
+#include <cairo/cairo-pdf.h>
+#include <cairo/cairo-ps.h>
+#include <unistd.h> // TEMP: Needed for close() in get_temporary_filename()
+
+#include "book.h"
+
+
+enum {
+ PAGE_ADDED,
+ PAGE_REMOVED,
+ CLEARED,
+ LAST_SIGNAL
+};
+static guint signals[LAST_SIGNAL] = { 0, };
+
+struct BookPrivate
+{
+ GList *pages;
+};
+
+G_DEFINE_TYPE (Book, book, G_TYPE_OBJECT);
+
+
+Book *
+book_new ()
+{
+ return g_object_new (BOOK_TYPE, NULL);
+}
+
+
+void
+book_clear (Book *book)
+{
+ GList *iter;
+ for (iter = book->priv->pages; iter; iter = iter->next) {
+ Page *page = iter->data;
+ g_object_unref (page);
+ }
+ g_list_free (book->priv->pages);
+ book->priv->pages = NULL;
+ g_signal_emit (book, signals[CLEARED], 0);
+}
+
+
+Page *
+book_append_page (Book *book, gint width, gint height, gint dpi, Orientation orientation)
+{
+ Page *page;
+
+ page = page_new ();
+ page_setup (page, width, height, dpi, orientation);
+
+ book->priv->pages = g_list_append (book->priv->pages, page);
+
+ g_signal_emit (book, signals[PAGE_ADDED], 0, page);
+
+ return page;
+}
+
+
+void
+book_delete_page (Book *book, Page *page)
+{
+ g_signal_emit (book, signals[PAGE_REMOVED], 0, page);
+
+ book->priv->pages = g_list_remove (book->priv->pages, page);
+ g_object_unref (page);
+}
+
+
+gint
+book_get_n_pages (Book *book)
+{
+ return g_list_length (book->priv->pages);
+}
+
+
+Page *
+book_get_page (Book *book, gint page_number)
+{
+ if (page_number < 0)
+ page_number = g_list_length (book->priv->pages) + page_number;
+ return g_list_nth_data (book->priv->pages, page_number);
+}
+
+
+static GFile *
+make_indexed_file (const gchar *uri, gint i)
+{
+ gchar *basename, *suffix, *indexed_uri;
+ GFile *file;
+
+ if (i == 0)
+ return g_file_new_for_uri (uri);
+
+ basename = g_path_get_basename (uri);
+ suffix = g_strrstr (basename, ".");
+
+ if (suffix)
+ indexed_uri = g_strdup_printf ("%.*s-%d%s", (int) (strlen (uri) - strlen (suffix)), uri, i, suffix);
+ else
+ indexed_uri = g_strdup_printf ("%s-%d", uri, i);
+ g_free (basename);
+
+ file = g_file_new_for_uri (indexed_uri);
+ g_free (indexed_uri);
+
+ return file;
+}
+
+
+static gboolean
+book_save_multi_file (Book *book, const gchar *type, GFile *file, GError **error)
+{
+ GList *iter;
+ gboolean result = TRUE;
+ gint i;
+ gchar *uri;
+
+ uri = g_file_get_uri (file);
+ for (iter = book->priv->pages, i = 0; iter && result; iter = iter->next, i++) {
+ Page *page = iter->data;
+ GFile *file;
+
+ file = make_indexed_file (uri, i);
+ result = page_save (page, type, file, error);
+ g_object_unref (file);
+ }
+ g_free (uri);
+
+ return result;
+}
+
+
+static void
+save_ps_pdf_surface (cairo_surface_t *surface, GdkPixbuf *image, gdouble dpi)
+{
+ cairo_t *context;
+
+ context = cairo_create (surface);
+
+ cairo_scale (context, 72.0 / dpi, 72.0 / dpi);
+ gdk_cairo_set_source_pixbuf (context, image, 0, 0);
+ cairo_pattern_set_filter (cairo_get_source (context), CAIRO_FILTER_BEST);
+ cairo_paint (context);
+
+ cairo_destroy (context);
+}
+
+
+static cairo_status_t
+write_cairo_data (GFileOutputStream *stream, unsigned char *data, unsigned int length)
+{
+ gboolean result;
+ GError *error = NULL;
+
+ result = g_output_stream_write_all (G_OUTPUT_STREAM (stream), data, length, NULL, NULL, &error);
+
+ if (error) {
+ g_warning ("Error writing data: %s", error->message);
+ g_error_free (error);
+ }
+
+ return result ? CAIRO_STATUS_SUCCESS : CAIRO_STATUS_WRITE_ERROR;
+}
+
+
+static gboolean
+book_save_ps (Book *book, GFile *file, GError **error)
+{
+ GFileOutputStream *stream;
+ GList *iter;
+ cairo_surface_t *surface;
+
+ stream = g_file_replace (file, NULL, FALSE, G_FILE_CREATE_NONE, NULL, error);
+ if (!stream)
+ return FALSE;
+
+ surface = cairo_ps_surface_create_for_stream ((cairo_write_func_t) write_cairo_data,
+ stream, 0, 0);
+
+ for (iter = book->priv->pages; iter; iter = iter->next) {
+ Page *page = iter->data;
+ double width, height;
+ GdkPixbuf *image;
+
+ image = page_get_cropped_image (page);
+
+ width = gdk_pixbuf_get_width (image) * 72.0 / page_get_dpi (page);
+ height = gdk_pixbuf_get_height (image) * 72.0 / page_get_dpi (page);
+ cairo_ps_surface_set_size (surface, width, height);
+ save_ps_pdf_surface (surface, image, page_get_dpi (page));
+ cairo_surface_show_page (surface);
+
+ g_object_unref (image);
+ }
+
+ cairo_surface_destroy (surface);
+
+ g_object_unref (stream);
+
+ return TRUE;
+}
+
+
+// TEMP: Copied from simple-scan.c
+static GFile *
+get_temporary_file (const gchar *prefix, const gchar *extension)
+{
+ gint fd;
+ GFile *file;
+ gchar *filename, *path;
+ GError *error = NULL;
+
+ /* NOTE: I'm not sure if this is a 100% safe strategy to use g_file_open_tmp(), close and
+ * use the filename but it appears to work in practise */
+
+ filename = g_strdup_printf ("%s-XXXXXX.%s", prefix, extension);
+ fd = g_file_open_tmp (filename, &path, &error);
+ g_free (filename);
+ if (fd < 0) {
+ g_warning ("Error saving email attachment: %s", error->message);
+ g_clear_error (&error);
+ return NULL;
+ }
+ close (fd);
+
+ file = g_file_new_for_path (path);
+ g_free (path);
+
+ return file;
+}
+
+
+static goffset
+get_file_size (GFile *file)
+{
+ GFileInfo *info;
+ goffset size = 0;
+
+ info = g_file_query_info (file,
+ G_FILE_ATTRIBUTE_STANDARD_SIZE,
+ G_FILE_QUERY_INFO_NONE,
+ NULL,
+ NULL);
+ if (info) {
+ size = g_file_info_get_size (info);
+ g_object_unref (info);
+ }
+
+ return size;
+}
+
+
+static gboolean
+book_save_pdf_with_imagemagick (Book *book, GFile *file, GError **error)
+{
+ GList *iter;
+ GString *command_line;
+ gboolean result = TRUE;
+ gint exit_status = 0;
+ GFile *output_file = NULL;
+ GList *link, *temporary_files = NULL;
+
+ /* ImageMagick command to create a PDF */
+ command_line = g_string_new ("convert -adjoin");
+
+ /* Save each page to a file */
+ for (iter = book->priv->pages; iter && result; iter = iter->next) {
+ Page *page = iter->data;
+ GFile *jpeg_file, *tiff_file;
+ gchar *path;
+ gint jpeg_size, tiff_size;
+
+ jpeg_file = get_temporary_file ("simple-scan", "jpg");
+ result = page_save (page, "jpeg", jpeg_file, error);
+ jpeg_size = get_file_size (jpeg_file);
+ temporary_files = g_list_append (temporary_files, jpeg_file);
+
+ tiff_file = get_temporary_file ("simple-scan", "tiff");
+ result = page_save (page, "tiff", tiff_file, error);
+ tiff_size = get_file_size (tiff_file);
+ temporary_files = g_list_append (temporary_files, tiff_file);
+
+ /* Use the smallest file */
+ if (jpeg_size < tiff_size)
+ path = g_file_get_path (jpeg_file);
+ else
+ path = g_file_get_path (tiff_file);
+ g_string_append_printf (command_line, " %s", path);
+ g_free (path);
+ }
+
+ /* Use ImageMagick command to create a PDF */
+ if (result) {
+ gchar *path, *stdout_text = NULL, *stderr_text = NULL;
+
+ output_file = get_temporary_file ("simple-scan", "pdf");
+ path = g_file_get_path (output_file);
+ g_string_append_printf (command_line, " %s", path);
+ g_free (path);
+
+ result = g_spawn_command_line_sync (command_line->str, &stdout_text, &stderr_text, &exit_status, error);
+ if (result && exit_status != 0) {
+ g_warning ("ImageMagick returned error code %d, command line was: %s", exit_status, command_line->str);
+ g_warning ("stdout: %s", stdout_text);
+ g_warning ("stderr: %s", stderr_text);
+ result = FALSE;
+ }
+ g_free (stdout_text);
+ g_free (stderr_text);
+ }
+
+ /* Move to target URI */
+ if (result) {
+ GFile *dest;
+ result = g_file_move (output_file, file, G_FILE_COPY_OVERWRITE, NULL, NULL, NULL, error);
+ g_object_unref (dest);
+ }
+
+ /* Delete page files */
+ for (link = temporary_files; link; link = link->next) {
+ GFile *f = link->data;
+
+ g_file_delete (f, NULL, NULL);
+ g_object_unref (f);
+ }
+ g_list_free (temporary_files);
+
+ if (output_file)
+ g_object_unref (output_file);
+ g_string_free (command_line, TRUE);
+
+ return result;
+}
+
+
+static gboolean
+book_save_pdf (Book *book, GFile *file, GError **error)
+{
+ GFileOutputStream *stream;
+ GList *iter;
+ cairo_surface_t *surface;
+ gchar *imagemagick_executable;
+
+ /* Use ImageMagick if it is available as then we can compress the images */
+ imagemagick_executable = g_find_program_in_path ("convert");
+ if (imagemagick_executable) {
+ g_free (imagemagick_executable);
+ return book_save_pdf_with_imagemagick (book, file, error);
+ }
+
+ stream = g_file_replace (file, NULL, FALSE, G_FILE_CREATE_NONE, NULL, error);
+ if (!stream)
+ return FALSE;
+
+ surface = cairo_pdf_surface_create_for_stream ((cairo_write_func_t) write_cairo_data,
+ stream, 0, 0);
+
+ for (iter = book->priv->pages; iter; iter = iter->next) {
+ Page *page = iter->data;
+ double width, height;
+ GdkPixbuf *image;
+
+ image = page_get_cropped_image (page);
+
+ width = gdk_pixbuf_get_width (image) * 72.0 / page_get_dpi (page);
+ height = gdk_pixbuf_get_height (image) * 72.0 / page_get_dpi (page);
+ cairo_pdf_surface_set_size (surface, width, height);
+ save_ps_pdf_surface (surface, image, page_get_dpi (page));
+ cairo_surface_show_page (surface);
+
+ g_object_unref (image);
+ }
+
+ cairo_surface_destroy (surface);
+
+ g_object_unref (stream);
+
+ return TRUE;
+}
+
+
+gboolean
+book_save (Book *book, const gchar *type, GFile *file, GError **error)
+{
+ if (strcmp (type, "jpeg") == 0)
+ return book_save_multi_file (book, "jpeg", file, error);
+ else if (strcmp (type, "png") == 0)
+ return book_save_multi_file (book, "png", file, error);
+ else if (strcmp (type, "tiff") == 0)
+ return book_save_multi_file (book, "tiff", file, error);
+ else if (strcmp (type, "ps") == 0)
+ return book_save_ps (book, file, error);
+ else if (strcmp (type, "pdf") == 0)
+ return book_save_pdf (book, file, error);
+ else
+ return FALSE;
+}
+
+
+static void
+book_finalize (GObject *object)
+{
+ Book *book = BOOK (object);
+ book_clear (book);
+ G_OBJECT_CLASS (book_parent_class)->finalize (object);
+}
+
+
+static void
+book_class_init (BookClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->finalize = book_finalize;
+
+ signals[PAGE_ADDED] =
+ g_signal_new ("page-added",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (BookClass, page_added),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__POINTER,
+ G_TYPE_NONE, 1, G_TYPE_POINTER);
+ signals[PAGE_REMOVED] =
+ g_signal_new ("page-removed",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (BookClass, page_removed),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__POINTER,
+ G_TYPE_NONE, 1, G_TYPE_POINTER);
+ signals[CLEARED] =
+ g_signal_new ("cleared",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (BookClass, cleared),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE, 0);
+
+ g_type_class_add_private (klass, sizeof (BookPrivate));
+}
+
+
+static void
+book_init (Book *book)
+{
+ book->priv = G_TYPE_INSTANCE_GET_PRIVATE (book, BOOK_TYPE, BookPrivate);
+}
diff --git a/src/book.h b/src/book.h
new file mode 100644
index 0000000..4e9e05e
--- /dev/null
+++ b/src/book.h
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2009 Canonical Ltd.
+ * Author: Robert Ancell <robert.ancell@canonical.com>
+ *
+ * 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. See http://www.gnu.org/copyleft/gpl.html the full text of the
+ * license.
+ */
+
+#ifndef _BOOK_H_
+#define _BOOK_H_
+
+#include <glib-object.h>
+#include <gio/gio.h>
+#include <cairo.h>
+#include "page.h"
+
+G_BEGIN_DECLS
+
+#define BOOK_TYPE (book_get_type ())
+#define BOOK(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), BOOK_TYPE, Book))
+
+
+typedef struct BookPrivate BookPrivate;
+
+typedef struct
+{
+ GObject parent_instance;
+ BookPrivate *priv;
+} Book;
+
+typedef struct
+{
+ GObjectClass parent_class;
+
+ void (*page_added) (Book *book, Page *page);
+ void (*page_removed) (Book *book, Page *page);
+ void (*cleared) (Book *book);
+} BookClass;
+
+
+GType book_get_type (void);
+
+Book *book_new (void);
+
+void book_clear (Book *book);
+
+Page *book_append_page (Book *book, gint width, gint height, gint dpi, Orientation orientation);
+
+void book_delete_page (Book *book, Page *page);
+
+gint book_get_n_pages (Book *book);
+
+Page *book_get_page (Book *book, gint page_number);
+
+gboolean book_save (Book *book, const gchar *type, GFile *file, GError **error);
+
+#endif /* _BOOK_H_ */
diff --git a/src/page-view.c b/src/page-view.c
new file mode 100644
index 0000000..2c37fc2
--- /dev/null
+++ b/src/page-view.c
@@ -0,0 +1,1170 @@
+/*
+ * Copyright (C) 2009 Canonical Ltd.
+ * Author: Robert Ancell <robert.ancell@canonical.com>
+ *
+ * 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. See http://www.gnu.org/copyleft/gpl.html the full text of the
+ * license.
+ */
+
+#include <math.h>
+
+#include "page-view.h"
+
+enum {
+ CHANGED,
+ SIZE_CHANGED,
+ LAST_SIGNAL
+};
+static guint signals[LAST_SIGNAL] = { 0, };
+
+enum {
+ PROP_0,
+ PROP_PAGE
+};
+
+typedef enum
+{
+ CROP_NONE = 0,
+ CROP_MIDDLE,
+ CROP_TOP,
+ CROP_BOTTOM,
+ CROP_LEFT,
+ CROP_RIGHT,
+ CROP_TOP_LEFT,
+ CROP_TOP_RIGHT,
+ CROP_BOTTOM_LEFT,
+ CROP_BOTTOM_RIGHT
+} CropLocation;
+
+struct PageViewPrivate
+{
+ /* Page being rendered */
+ Page *page;
+
+ /* Image to render at current resolution */
+ GdkPixbuf *image;
+
+ /* Border around image */
+ gboolean selected;
+ gint border_width;
+
+ /* True if image needs to be regenerated */
+ gboolean update_image;
+
+ /* Next scan line to render */
+ gint scan_line;
+
+ /* Dimensions of image to generate */
+ gint width, height;
+
+ /* Location to place this page */
+ gint x, y;
+
+ CropLocation crop_location;
+ gdouble selected_crop_px, selected_crop_py;
+ gint selected_crop_x, selected_crop_y;
+ gint selected_crop_w, selected_crop_h;
+
+ /* Cursor over this page */
+ gint cursor;
+
+ gint animate_n_segments, animate_segment;
+ guint animate_timeout;
+};
+
+G_DEFINE_TYPE (PageView, page_view, G_TYPE_OBJECT);
+
+
+PageView *
+page_view_new (Page *page)
+{
+ return g_object_new (PAGE_VIEW_TYPE, "page", page, NULL);
+}
+
+
+Page *
+page_view_get_page (PageView *view)
+{
+ g_return_val_if_fail (view != NULL, NULL);
+ return view->priv->page;
+}
+
+
+void
+page_view_set_selected (PageView *view, gboolean selected)
+{
+ g_return_if_fail (view != NULL);
+ if ((view->priv->selected && selected) || (!view->priv->selected && !selected))
+ return;
+ view->priv->selected = selected;
+ g_signal_emit (view, signals[CHANGED], 0);
+}
+
+
+gboolean page_view_get_selected (PageView *view)
+{
+ g_return_val_if_fail (view != NULL, FALSE);
+ return view->priv->selected;
+}
+
+
+void
+page_view_set_x_offset (PageView *view, gint offset)
+{
+ g_return_if_fail (view != NULL);
+ view->priv->x = offset;
+}
+
+
+void
+page_view_set_y_offset (PageView *view, gint offset)
+{
+ g_return_if_fail (view != NULL);
+ view->priv->y = offset;
+}
+
+
+gint
+page_view_get_x_offset (PageView *view)
+{
+ g_return_val_if_fail (view != NULL, 0);
+ return view->priv->x;
+}
+
+
+gint
+page_view_get_y_offset (PageView *view)
+{
+ g_return_val_if_fail (view != NULL, 0);
+ return view->priv->y;
+}
+
+
+static guchar *
+get_pixel (guchar *input, gint rowstride, gint n_channels, gint x, gint y)
+{
+ return input + rowstride * y + x * n_channels;
+}
+
+
+static void
+set_pixel (guchar *input, gint rowstride, gint n_channels,
+ double l, double r, double t, double b, guchar *pixel)
+{
+ gint x, y;
+ gint L, R, T, B;
+ double scale, red, green, blue;
+
+ /* Decimation:
+ *
+ * Target pixel is defined by (t,l)-(b,r)
+ * It touches 16 pixels in original image
+ * It completely covers 4 pixels in original image (T,L)-(B,R)
+ * Add covered pixels and add weighted partially covered pixels.
+ * Divide by total area.
+ *
+ * l L R r
+ * +-----+-----+-----+-----+
+ * | | | | |
+ * t | +--+-----+-----+---+ |
+ * T +--+--+-----+-----+---+-+
+ * | | | | | | |
+ * | | | | | | |
+ * +--+--+-----+-----+---+-+
+ * | | | | | | |
+ * | | | | | | |
+ * B +--+--+-----+-----+---+-+
+ * | | | | | | |
+ * b | +--+-----+-----+---+ |
+ * +-----+-----+-----+-----+
+ *
+ *
+ * Interpolation:
+ *
+ * l r
+ * +-----+-----+-----+-----+
+ * | | | | |
+ * | | | | |
+ * +-----+-----+-----+-----+
+ * t | | +-+--+ | |
+ * | | | | | | |
+ * +-----+---+-+--+--+-----+
+ * b | | +-+--+ | |
+ * | | | | |
+ * +-----+-----+-----+-----+
+ * | | | | |
+ * | | | | |
+ * +-----+-----+-----+-----+
+ *
+ * Same again, just no completely covered pixels.
+ */
+
+ L = l;
+ if (L != l)
+ L++;
+ R = r;
+ T = t;
+ if (T != t)
+ T++;
+ B = b;
+
+ red = green = blue = 0.0;
+
+ /* Target can fit inside one source pixel
+ * +-----+
+ * | |
+ * | +--+| +-----+-----+ +-----+ +-----+ +-----+
+ * +-+--++ or | +-++ | or | +-+ | or | +--+| or | +--+
+ * | +--+| | +-++ | | +-+ | | | || | | |
+ * | | +-----+-----+ +-----+ +-+--++ +--+--+
+ * +-----+
+ */
+ if ((r - l <= 1.0 && (gint)r == (gint)l) || (b - t <= 1.0 && (gint)b == (gint)t)) {
+ /* Inside */
+ if ((gint)l == (gint)r || (gint)t == (gint)b) {
+ guchar *p = get_pixel (input, rowstride, n_channels, (gint)l, (gint)t);
+ pixel[0] = p[0];
+ pixel[1] = p[1];
+ pixel[2] = p[2];
+ return;
+ }
+
+ /* Stradling horizontal edge */
+ if (L > R) {
+ guchar *p = get_pixel (input, rowstride, n_channels, R, T-1);
+ red += p[0] * (r-l)*(T-t);
+ green += p[1] * (r-l)*(T-t);
+ blue += p[2] * (r-l)*(T-t);
+ for (y = T; y < B; y++) {
+ guchar *p = get_pixel (input, rowstride, n_channels, R, y);
+ red += p[0] * (r-l);
+ green += p[1] * (r-l);
+ blue += p[2] * (r-l);
+ }
+ p = get_pixel (input, rowstride, n_channels, R, B);
+ red += p[0] * (r-l)*(b-B);
+ green += p[1] * (r-l)*(b-B);
+ blue += p[2] * (r-l)*(b-B);
+ }
+ /* Stradling vertical edge */
+ else {
+ guchar *p = get_pixel (input, rowstride, n_channels, L - 1, B);
+ red += p[0] * (b-t)*(L-l);
+ green += p[1] * (b-t)*(L-l);
+ blue += p[2] * (b-t)*(L-l);
+ for (x = L; x < R; x++) {
+ guchar *p = get_pixel (input, rowstride, n_channels, x, B);
+ red += p[0] * (b-t);
+ green += p[1] * (b-t);
+ blue += p[2] * (b-t);
+ }
+ p = get_pixel (input, rowstride, n_channels, R, B);
+ red += p[0] * (b-t)*(r-R);
+ green += p[1] * (b-t)*(r-R);
+ blue += p[2] * (b-t)*(r-R);
+ }
+
+ scale = 1.0 / ((r - l) * (b - t));
+ pixel[0] = (guchar)(red * scale + 0.5);
+ pixel[1] = (guchar)(green * scale + 0.5);
+ pixel[2] = (guchar)(blue * scale + 0.5);
+ return;
+ }
+
+ /* Add the middle pixels */
+ for (x = L; x < R; x++) {
+ for (y = T; y < B; y++) {
+ guchar *p = get_pixel (input, rowstride, n_channels, x, y);
+ red += p[0];
+ green += p[1];
+ blue += p[2];
+ }
+ }
+
+ /* Add the weighted top and bottom pixels */
+ for (x = L; x < R; x++) {
+ if (t != T) {
+ guchar *p = get_pixel (input, rowstride, n_channels, x, T - 1);
+ red += p[0] * (T - t);
+ green += p[1] * (T - t);
+ blue += p[2] * (T - t);
+ }
+
+ if (b != B) {
+ guchar *p = get_pixel (input, rowstride, n_channels, x, B);
+ red += p[0] * (b - B);
+ green += p[1] * (b - B);
+ blue += p[2] * (b - B);
+ }
+ }
+
+ /* Add the left and right pixels */
+ for (y = T; y < B; y++) {
+ if (l != L) {
+ guchar *p = get_pixel (input, rowstride, n_channels, L - 1, y);
+ red += p[0] * (L - l);
+ green += p[1] * (L - l);
+ blue += p[2] * (L - l);
+ }
+
+ if (r != R) {
+ guchar *p = get_pixel (input, rowstride, n_channels, R, y);
+ red += p[0] * (r - R);
+ green += p[1] * (r - R);
+ blue += p[2] * (r - R);
+ }
+ }
+
+ /* Add the corner pixels */
+ if (l != L && t != T) {
+ guchar *p = get_pixel (input, rowstride, n_channels, L - 1, T - 1);
+ red += p[0] * (L - l)*(T - t);
+ green += p[1] * (L - l)*(T - t);
+ blue += p[2] * (L - l)*(T - t);
+ }
+ if (r != R && t != T) {
+ guchar *p = get_pixel (input, rowstride, n_channels, R, T - 1);
+ red += p[0] * (r - R)*(T - t);
+ green += p[1] * (r - R)*(T - t);
+ blue += p[2] * (r - R)*(T - t);
+ }
+ if (r != R && b != B) {
+ guchar *p = get_pixel (input, rowstride, n_channels, R, B);
+ red += p[0] * (r - R)*(b - B);
+ green += p[1] * (r - R)*(b - B);
+ blue += p[2] * (r - R)*(b - B);
+ }
+ if (l != L && b != B) {
+ guchar *p = get_pixel (input, rowstride, n_channels, L - 1, B);
+ red += p[0] * (L - l)*(b - B);
+ green += p[1] * (L - l)*(b - B);
+ blue += p[2] * (L - l)*(b - B);
+ }
+
+ /* Scale pixel values and clamp in range [0, 255] */
+ scale = 1.0 / ((r - l) * (b - t));
+ pixel[0] = (guchar)(red * scale + 0.5);
+ pixel[1] = (guchar)(green * scale + 0.5);
+ pixel[2] = (guchar)(blue * scale + 0.5);
+}
+
+
+static void
+update_preview (GdkPixbuf *image,
+ GdkPixbuf **output_image, gint output_width, gint output_height,
+ Orientation orientation, gint old_scan_line, gint scan_line)
+{
+ guchar *input, *output;
+ gint input_width, input_height;
+ gint input_rowstride, input_n_channels;
+ gint output_rowstride, output_n_channels;
+ gint x, y;
+ gint L, R, T, B;
+
+ input = gdk_pixbuf_get_pixels (image);
+ input_width = gdk_pixbuf_get_width (image);
+ input_height = gdk_pixbuf_get_height (image);
+ input_rowstride = gdk_pixbuf_get_rowstride (image);
+ input_n_channels = gdk_pixbuf_get_n_channels (image);
+
+ /* Create new image if one does not exist or has changed size */
+ if (!*output_image ||
+ gdk_pixbuf_get_width (*output_image) != output_width ||
+ gdk_pixbuf_get_height (*output_image) != output_height) {
+ if (*output_image)
+ g_object_unref (*output_image);
+ *output_image = gdk_pixbuf_new (GDK_COLORSPACE_RGB,
+ FALSE,
+ 8,
+ output_width,
+ output_height);
+
+ /* Update entire image */
+ L = 0;
+ R = output_width - 1;
+ T = 0;
+ B = output_height - 1;
+ }
+ /* Otherwise only update changed area */
+ else {
+ switch (orientation) {
+ case TOP_TO_BOTTOM:
+ L = 0;
+ R = output_width - 1;
+ T = (gint)((double)old_scan_line * output_height / input_height);
+ B = (gint)((double)scan_line * output_height / input_height + 0.5);
+ break;
+ case LEFT_TO_RIGHT:
+ L = (gint)((double)old_scan_line * output_width / input_width);
+ R = (gint)((double)scan_line * output_width / input_width + 0.5);
+ T = 0;
+ B = output_height - 1;
+ break;
+ case BOTTOM_TO_TOP:
+ L = 0;
+ R = output_width - 1;
+ T = (gint)((double)(input_height - scan_line) * output_height / input_height);
+ B = (gint)((double)(input_height - old_scan_line) * output_height / input_height + 0.5);
+ break;
+ case RIGHT_TO_LEFT:
+ L = (gint)((double)(input_width - scan_line) * output_width / input_width);
+ R = (gint)((double)(input_width - old_scan_line) * output_width / input_width + 0.5);
+ T = 0;
+ B = output_height - 1;
+ break;
+ default:
+ L = R = B = T = 0;
+ break;
+ }
+ }
+
+ /* FIXME: There's an off by one error in there somewhere... */
+ if (R >= output_width)
+ R = output_width - 1;
+ if (B >= output_height)
+ B = output_height - 1;
+
+ g_return_if_fail (L >= 0);
+ g_return_if_fail (R < output_width);
+ g_return_if_fail (T >= 0);
+ g_return_if_fail (B < output_height);
+ g_return_if_fail (*output_image != NULL);
+
+ output = gdk_pixbuf_get_pixels (*output_image);
+ output_rowstride = gdk_pixbuf_get_rowstride (*output_image);
+ output_n_channels = gdk_pixbuf_get_n_channels (*output_image);
+
+ /* Update changed area */
+ for (x = L; x <= R; x++) {
+ double l, r;
+
+ l = (double)x * input_width / output_width;
+ r = (double)(x + 1) * input_width / output_width;
+
+ for (y = T; y <= B; y++) {
+ double t, b;
+
+ t = (double)y * input_height / output_height;
+ b = (double)(y + 1) * input_height / output_height;
+
+ set_pixel (input, input_rowstride, input_n_channels,
+ l, r, t, b,
+ get_pixel (output, output_rowstride, output_n_channels, x, y));
+ }
+ }
+}
+
+
+static gint
+get_preview_width (PageView *view)
+{
+ return view->priv->width - view->priv->border_width * 2;
+}
+
+
+static gint
+get_preview_height (PageView *view)
+{
+ return view->priv->height - view->priv->border_width * 2;
+}
+
+
+static void
+update_page_view (PageView *view)
+{
+ GdkPixbuf *image;
+ gint old_scan_line, scan_line;
+
+ if (!view->priv->update_image)
+ return;
+
+ image = page_get_image (view->priv->page);
+ old_scan_line = view->priv->scan_line;
+ scan_line = page_get_scan_line (view->priv->page);
+
+ update_preview (image,
+ &view->priv->image,
+ get_preview_width (view),
+ get_preview_height (view),
+ page_get_orientation (view->priv->page), old_scan_line, scan_line);
+ g_object_unref (image);
+
+ view->priv->update_image = FALSE;
+ view->priv->scan_line = scan_line;
+}
+
+
+static gint
+page_to_screen_x (PageView *view, gint x)
+{
+ return (double) x * get_preview_width (view) / page_get_width (view->priv->page) + 0.5;
+}
+
+
+static gint
+page_to_screen_y (PageView *view, gint y)
+{
+ return (double) y * get_preview_height (view) / page_get_height (view->priv->page) + 0.5;
+}
+
+
+static gint
+screen_to_page_x (PageView *view, gint x)
+{
+ return (double) x * page_get_width (view->priv->page) / get_preview_width (view) + 0.5;
+}
+
+
+static gint
+screen_to_page_y (PageView *view, gint y)
+{
+ return (double) y * page_get_height (view->priv->page) / get_preview_height (view) + 0.5;
+}
+
+
+static CropLocation
+get_crop_location (PageView *view, gint x, gint y)
+{
+ gint cx, cy, cw, ch;
+ gint dx, dy, dw, dh;
+ gint ix, iy;
+ gint crop_border = 20;
+ gchar *name;
+
+ if (!page_has_crop (view->priv->page))
+ return 0;
+
+ page_get_crop (view->priv->page, &cx, &cy, &cw, &ch);
+ dx = page_to_screen_x (view, cx);
+ dy = page_to_screen_y (view, cy);
+ dw = page_to_screen_x (view, cw);
+ dh = page_to_screen_y (view, ch);
+ ix = x - dx;
+ iy = y - dy;
+
+ if (ix < 0 || ix > dw || iy < 0 || iy > dh)
+ return CROP_NONE;
+
+ /* Can't resize named crops */
+ name = page_get_named_crop (view->priv->page);
+ if (name != NULL) {
+ g_free (name);
+ return CROP_MIDDLE;
+ }
+
+ /* Adjust borders so can select */
+ if (dw < crop_border * 3)
+ crop_border = dw / 3;
+ if (dh < crop_border * 3)
+ crop_border = dh / 3;
+
+ /* Top left */
+ if (ix < crop_border && iy < crop_border)
+ return CROP_TOP_LEFT;
+ /* Top right */
+ if (ix > dw - crop_border && iy < crop_border)
+ return CROP_TOP_RIGHT;
+ /* Bottom left */
+ if (ix < crop_border && iy > dh - crop_border)
+ return CROP_BOTTOM_LEFT;
+ /* Bottom right */
+ if (ix > dw - crop_border && iy > dh - crop_border)
+ return CROP_BOTTOM_RIGHT;
+
+ /* Left */
+ if (ix < crop_border)
+ return CROP_LEFT;
+ /* Right */
+ if (ix > dw - crop_border)
+ return CROP_RIGHT;
+ /* Top */
+ if (iy < crop_border)
+ return CROP_TOP;
+ /* Bottom */
+ if (iy > dh - crop_border)
+ return CROP_BOTTOM;
+
+ /* In the middle */
+ return CROP_MIDDLE;
+}
+
+
+void
+page_view_button_press (PageView *view, gint x, gint y)
+{
+ CropLocation location;
+
+ g_return_if_fail (view != NULL);
+
+ /* See if selecting crop */
+ location = get_crop_location (view, x, y);;
+ if (location != CROP_NONE) {
+ view->priv->crop_location = location;
+ view->priv->selected_crop_px = x;
+ view->priv->selected_crop_py = y;
+ page_get_crop (view->priv->page,
+ &view->priv->selected_crop_x,
+ &view->priv->selected_crop_y,
+ &view->priv->selected_crop_w,
+ &view->priv->selected_crop_h);
+ }
+}
+
+
+void
+page_view_motion (PageView *view, gint x, gint y)
+{
+ gint pw, ph;
+ gint cx, cy, cw, ch, dx, dy;
+ gint new_x, new_y, new_w, new_h;
+ CropLocation location;
+ gint cursor;
+ gint min_size;
+
+ min_size = screen_to_page_x (view, 15);
+
+ g_return_if_fail (view != NULL);
+
+ location = get_crop_location (view, x, y);
+ switch (location) {
+ case CROP_MIDDLE:
+ cursor = GDK_HAND1;
+ break;
+ case CROP_TOP:
+ cursor = GDK_TOP_SIDE;
+ break;
+ case CROP_BOTTOM:
+ cursor = GDK_BOTTOM_SIDE;
+ break;
+ case CROP_LEFT:
+ cursor = GDK_LEFT_SIDE;
+ break;
+ case CROP_RIGHT:
+ cursor = GDK_RIGHT_SIDE;
+ break;
+ case CROP_TOP_LEFT:
+ cursor = GDK_TOP_LEFT_CORNER;
+ break;
+ case CROP_TOP_RIGHT:
+ cursor = GDK_TOP_RIGHT_CORNER;
+ break;
+ case CROP_BOTTOM_LEFT:
+ cursor = GDK_BOTTOM_LEFT_CORNER;
+ break;
+ case CROP_BOTTOM_RIGHT:
+ cursor = GDK_BOTTOM_RIGHT_CORNER;
+ break;
+ default:
+ cursor = GDK_ARROW;
+ break;
+ }
+
+ if (view->priv->crop_location == CROP_NONE) {
+ view->priv->cursor = cursor;
+ return;
+ }
+
+ /* Move the crop */
+ pw = page_get_width (view->priv->page);
+ ph = page_get_height (view->priv->page);
+ page_get_crop (view->priv->page, &cx, &cy, &cw, &ch);
+
+ dx = screen_to_page_x (view, x - view->priv->selected_crop_px);
+ dy = screen_to_page_y (view, y - view->priv->selected_crop_py);
+
+ new_x = view->priv->selected_crop_x;
+ new_y = view->priv->selected_crop_y;
+ new_w = view->priv->selected_crop_w;
+ new_h = view->priv->selected_crop_h;
+
+ /* Limit motion to remain within page and minimum crop size */
+ if (view->priv->crop_location == CROP_TOP_LEFT ||
+ view->priv->crop_location == CROP_LEFT ||
+ view->priv->crop_location == CROP_BOTTOM_LEFT) {
+ if (dx > new_w - min_size)
+ dx = new_w - min_size;
+ if (new_x + dx < 0)
+ dx = -new_x;
+ }
+ if (view->priv->crop_location == CROP_TOP_LEFT ||
+ view->priv->crop_location == CROP_TOP ||
+ view->priv->crop_location == CROP_TOP_RIGHT) {
+ if (dy > new_h - min_size)
+ dy = new_h - min_size;
+ if (new_y + dy < 0)
+ dy = -new_y;
+ }
+
+ if (view->priv->crop_location == CROP_TOP_RIGHT ||
+ view->priv->crop_location == CROP_RIGHT ||
+ view->priv->crop_location == CROP_BOTTOM_RIGHT) {
+ if (dx < min_size - new_w)
+ dx = min_size - new_w;
+ if (new_x + new_w + dx > pw)
+ dx = pw - new_x - new_w;
+ }
+ if (view->priv->crop_location == CROP_BOTTOM_LEFT ||
+ view->priv->crop_location == CROP_BOTTOM ||
+ view->priv->crop_location == CROP_BOTTOM_RIGHT) {
+ if (dy < min_size - new_h)
+ dy = min_size - new_h;
+ if (new_y + new_h + dy > ph)
+ dy = ph - new_y - new_h;
+ }
+ if (view->priv->crop_location == CROP_MIDDLE) {
+ if (new_x + dx + new_w > pw)
+ dx = pw - new_x - new_w;
+ if (new_x + dx < 0)
+ dx = -new_x;
+ if (new_y + dy + new_h > ph)
+ dy = ph - new_y - new_h;
+ if (new_y + dy < 0)
+ dy = -new_y;
+ }
+
+ /* Move crop */
+ if (view->priv->crop_location == CROP_MIDDLE) {
+ new_x += dx;
+ new_y += dy;
+ }
+ if (view->priv->crop_location == CROP_TOP_LEFT ||
+ view->priv->crop_location == CROP_LEFT ||
+ view->priv->crop_location == CROP_BOTTOM_LEFT)
+ {
+ new_x += dx;
+ new_w -= dx;
+ }
+ if (view->priv->crop_location == CROP_TOP_LEFT ||
+ view->priv->crop_location == CROP_TOP ||
+ view->priv->crop_location == CROP_TOP_RIGHT) {
+ new_y += dy;
+ new_h -= dy;
+ }
+
+ if (view->priv->crop_location == CROP_TOP_RIGHT ||
+ view->priv->crop_location == CROP_RIGHT ||
+ view->priv->crop_location == CROP_BOTTOM_RIGHT) {
+ new_w += dx;
+ }
+ if (view->priv->crop_location == CROP_BOTTOM_LEFT ||
+ view->priv->crop_location == CROP_BOTTOM ||
+ view->priv->crop_location == CROP_BOTTOM_RIGHT) {
+ new_h += dy;
+ }
+
+ page_move_crop (view->priv->page, new_x, new_y);
+
+ /* If reshaped crop, must be a custom crop */
+ if (new_w != cw || new_h != ch)
+ page_set_custom_crop (view->priv->page, new_w, new_h);
+}
+
+
+void
+page_view_button_release (PageView *view, gint x, gint y)
+{
+ g_return_if_fail (view != NULL);
+
+ /* Complete crop */
+ view->priv->crop_location = CROP_NONE;
+ g_signal_emit (view, signals[CHANGED], 0);
+}
+
+
+gint
+page_view_get_cursor (PageView *view)
+{
+ g_return_val_if_fail (view != NULL, 0);
+ return view->priv->cursor;
+}
+
+
+static gboolean
+animation_cb (PageView *view)
+{
+ view->priv->animate_segment = (view->priv->animate_segment + 1) % view->priv->animate_n_segments;
+ g_signal_emit (view, signals[CHANGED], 0);
+ return TRUE;
+}
+
+
+static void
+update_animation (PageView *view)
+{
+ gboolean animate, is_animating;
+
+ animate = page_is_scanning (view->priv->page) && !page_has_data (view->priv->page);
+ is_animating = view->priv->animate_timeout != 0;
+ if (animate == is_animating)
+ return;
+
+ if (animate) {
+ view->priv->animate_segment = 0;
+ if (view->priv->animate_timeout == 0)
+ view->priv->animate_timeout = g_timeout_add (150, (GSourceFunc) animation_cb, view);
+ }
+ else
+ {
+ if (view->priv->animate_timeout != 0)
+ g_source_remove (view->priv->animate_timeout);
+ view->priv->animate_timeout = 0;
+ }
+}
+
+
+void
+page_view_render (PageView *view, cairo_t *context)
+{
+ gint width, height;
+
+ g_return_if_fail (view != NULL);
+ g_return_if_fail (context != NULL);
+
+ update_animation (view);
+ update_page_view (view);
+
+ width = get_preview_width (view);
+ height = get_preview_height (view);
+
+ cairo_set_line_width (context, 1);
+ cairo_translate (context, view->priv->x, view->priv->y);
+
+ /* Draw page border */
+ cairo_set_source_rgb (context, 0, 0, 0);
+ cairo_set_line_width (context, view->priv->border_width);
+ cairo_rectangle (context,
+ (double)view->priv->border_width / 2,
+ (double)view->priv->border_width / 2,
+ view->priv->width - view->priv->border_width,
+ view->priv->height - view->priv->border_width);
+ cairo_stroke (context);
+
+ /* Draw image */
+ cairo_translate (context, view->priv->border_width, view->priv->border_width);
+ if (view->priv->image) {
+ gdk_cairo_set_source_pixbuf (context, view->priv->image, 0, 0);
+ cairo_paint (context);
+ }
+ else {
+ cairo_scale (context,
+ (double) get_preview_width (view) / page_get_width (view->priv->page),
+ (double) get_preview_height (view) / page_get_height (view->priv->page));
+ gdk_cairo_set_source_pixbuf (context, page_get_image (view->priv->page), 0, 0);
+
+ cairo_paint (context);
+ }
+
+ /* Draw throbber */
+ if (page_is_scanning (view->priv->page) && !page_has_data (view->priv->page)) {
+ gdouble inner_radius, outer_radius, x, y, arc, offset = 0.0;
+ gint i;
+
+ if (width > height)
+ outer_radius = 0.15 * width;
+ else
+ outer_radius = 0.15 * height;
+ arc = M_PI / view->priv->animate_n_segments;
+
+ /* Space circles */
+ x = outer_radius * sin (arc);
+ y = outer_radius * (cos (arc) - 1.0);
+ inner_radius = 0.6 * sqrt (x*x + y*y);
+
+ for (i = 0; i < view->priv->animate_n_segments; i++, offset += arc * 2) {
+ x = width / 2 + outer_radius * sin (offset);
+ y = height / 2 - outer_radius * cos (offset);
+ cairo_arc (context, x, y, inner_radius, 0, 2 * M_PI);
+
+ if (i == view->priv->animate_segment) {
+ cairo_set_source_rgb (context, 0.75, 0.75, 0.75);
+ cairo_fill_preserve (context);
+ }
+
+ cairo_set_source_rgb (context, 0.5, 0.5, 0.5);
+ cairo_stroke (context);
+ }
+ }
+
+ /* Draw scan line */
+ if (page_is_scanning (view->priv->page) && page_get_scan_line (view->priv->page) > 0) {
+ gint scan_line;
+ double s;
+ double x1, y1, x2, y2;
+
+ scan_line = page_get_scan_line (view->priv->page);
+
+ switch (page_get_orientation (view->priv->page)) {
+ case TOP_TO_BOTTOM:
+ s = page_to_screen_y (view, scan_line);
+ x1 = 0; y1 = s + 0.5;
+ x2 = width; y2 = s + 0.5;
+ break;
+ case BOTTOM_TO_TOP:
+ s = page_to_screen_y (view, scan_line);
+ x1 = 0; y1 = height - s + 0.5;
+ x2 = width; y2 = height - s + 0.5;
+ break;
+ case LEFT_TO_RIGHT:
+ s = page_to_screen_x (view, scan_line);
+ x1 = s + 0.5; y1 = 0;
+ x2 = s + 0.5; y2 = height;
+ break;
+ case RIGHT_TO_LEFT:
+ s = page_to_screen_x (view, scan_line);
+ x1 = width - s + 0.5; y1 = 0;
+ x2 = width - s + 0.5; y2 = height;
+ break;
+ default:
+ x1 = y1 = x2 = y2 = 0;
+ break;
+ }
+
+ cairo_move_to (context, x1, y1);
+ cairo_line_to (context, x2, y2);
+ cairo_set_source_rgb (context, 1.0, 0.0, 0.0);
+ cairo_stroke (context);
+ }
+
+ /* Draw crop */
+ if (page_has_crop (view->priv->page)) {
+ gint x, y, crop_width, crop_height;
+ gdouble dx, dy, dw, dh;
+
+ page_get_crop (view->priv->page, &x, &y, &crop_width, &crop_height);
+
+ dx = page_to_screen_x (view, x);
+ dy = page_to_screen_y (view, y);
+ dw = page_to_screen_x (view, crop_width);
+ dh = page_to_screen_y (view, crop_height);
+
+ /* Shade out cropped area */
+ cairo_rectangle (context,
+ 0, 0,
+ width, height);
+ cairo_new_sub_path (context);
+ cairo_rectangle (context, dx, dy, dw, dh);
+ cairo_set_fill_rule (context, CAIRO_FILL_RULE_EVEN_ODD);
+ cairo_set_source_rgba (context, 0.25, 0.25, 0.25, 0.2);
+ cairo_fill (context);
+
+ /* Show new edge */
+ cairo_rectangle (context, dx - 1.5, dy - 1.5, dw + 3, dh + 3);
+ cairo_set_source_rgb (context, 1.0, 1.0, 1.0);
+ cairo_stroke (context);
+ cairo_rectangle (context, dx - 0.5, dy - 0.5, dw + 1, dh + 1);
+ cairo_set_source_rgb (context, 0.0, 0.0, 0.0);
+ cairo_stroke (context);
+ }
+}
+
+
+void
+page_view_set_width (PageView *view, gint width)
+{
+ gint height;
+
+ g_return_if_fail (view != NULL);
+
+ // FIXME: Automatically update when get updated image
+ height = (double)width * page_get_height (view->priv->page) / page_get_width (view->priv->page);
+ if (view->priv->width == width && view->priv->height == height)
+ return;
+
+ view->priv->width = width;
+ view->priv->height = height;
+
+ /* Regenerate image */
+ view->priv->update_image = TRUE;
+
+ g_signal_emit (view, signals[SIZE_CHANGED], 0);
+ g_signal_emit (view, signals[CHANGED], 0);
+}
+
+
+void
+page_view_set_height (PageView *view, gint height)
+{
+ gint width;
+
+ g_return_if_fail (view != NULL);
+
+ // FIXME: Automatically update when get updated image
+ width = (double)height * page_get_width (view->priv->page) / page_get_height (view->priv->page);
+ if (view->priv->width == width && view->priv->height == height)
+ return;
+
+ view->priv->width = width;
+ view->priv->height = height;
+
+ /* Regenerate image */
+ view->priv->update_image = TRUE;
+
+ g_signal_emit (view, signals[SIZE_CHANGED], 0);
+ g_signal_emit (view, signals[CHANGED], 0);
+}
+
+
+gint
+page_view_get_width (PageView *view)
+{
+ g_return_val_if_fail (view != NULL, 0);
+ return view->priv->width;
+}
+
+
+gint
+page_view_get_height (PageView *view)
+{
+ g_return_val_if_fail (view != NULL, 0);
+ return view->priv->height;
+}
+
+
+static void
+page_image_changed_cb (Page *p, PageView *view)
+{
+ /* Regenerate image */
+ view->priv->update_image = TRUE;
+ g_signal_emit (view, signals[CHANGED], 0);
+}
+
+
+static void
+page_size_changed_cb (Page *p, PageView *view)
+{
+ /* Regenerate image */
+ view->priv->update_image = TRUE;
+ g_signal_emit (view, signals[SIZE_CHANGED], 0);
+ g_signal_emit (view, signals[CHANGED], 0);
+}
+
+
+static void
+page_overlay_changed_cb (Page *p, PageView *view)
+{
+ g_signal_emit (view, signals[CHANGED], 0);
+}
+
+
+static void
+page_view_set_page (PageView *view, Page *page)
+{
+ g_return_if_fail (view != NULL);
+ g_return_if_fail (view->priv->page == NULL);
+
+ view->priv->page = g_object_ref (page);
+ g_signal_connect (view->priv->page, "image-changed", G_CALLBACK (page_image_changed_cb), view);
+ g_signal_connect (view->priv->page, "size-changed", G_CALLBACK (page_size_changed_cb), view);
+ g_signal_connect (view->priv->page, "crop-changed", G_CALLBACK (page_overlay_changed_cb), view);
+ g_signal_connect (view->priv->page, "scan-line-changed", G_CALLBACK (page_overlay_changed_cb), view);
+}
+
+
+static void
+page_view_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ PageView *self;
+
+ self = PAGE_VIEW (object);
+
+ switch (prop_id) {
+ case PROP_PAGE:
+ page_view_set_page (self, g_value_get_object (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+
+static void
+page_view_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ PageView *self;
+
+ self = PAGE_VIEW (object);
+
+ switch (prop_id) {
+ case PROP_PAGE:
+ g_value_set_object (value, self->priv->page);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+
+static void
+page_view_finalize (GObject *object)
+{
+ PageView *view = PAGE_VIEW (object);
+ g_object_unref (view->priv->page);
+ view->priv->page = NULL;
+ if (view->priv->image)
+ g_object_unref (view->priv->image);
+ view->priv->image = NULL;
+ if (view->priv->animate_timeout != 0)
+ g_source_remove (view->priv->animate_timeout);
+ view->priv->animate_timeout = 0;
+ G_OBJECT_CLASS (page_view_parent_class)->finalize (object);
+}
+
+
+static void
+page_view_class_init (PageViewClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->get_property = page_view_get_property;
+ object_class->set_property = page_view_set_property;
+ object_class->finalize = page_view_finalize;
+
+ signals[CHANGED] =
+ g_signal_new ("changed",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (PageViewClass, changed),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE, 0);
+ signals[SIZE_CHANGED] =
+ g_signal_new ("size-changed",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (PageViewClass, size_changed),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE, 0);
+
+ g_type_class_add_private (klass, sizeof (PageViewPrivate));
+
+ g_object_class_install_property (object_class,
+ PROP_PAGE,
+ g_param_spec_object ("page",
+ "page",
+ "Page being rendered",
+ PAGE_TYPE,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
+}
+
+
+static void
+page_view_init (PageView *view)
+{
+ view->priv = G_TYPE_INSTANCE_GET_PRIVATE (view, PAGE_VIEW_TYPE, PageViewPrivate);
+ view->priv->update_image = TRUE;
+ view->priv->cursor = GDK_ARROW;
+ view->priv->border_width = 1;
+ view->priv->animate_n_segments = 7;
+}
diff --git a/src/page-view.h b/src/page-view.h
new file mode 100644
index 0000000..dd64197
--- /dev/null
+++ b/src/page-view.h
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2009 Canonical Ltd.
+ * Author: Robert Ancell <robert.ancell@canonical.com>
+ *
+ * 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. See http://www.gnu.org/copyleft/gpl.html the full text of the
+ * license.
+ */
+
+#ifndef _PAGE_VIEW_H_
+#define _PAGE_VIEW_H_
+
+#include <glib-object.h>
+#include <gtk/gtk.h>
+#include <cairo.h>
+#include "page.h"
+
+G_BEGIN_DECLS
+
+#define PAGE_VIEW_TYPE (page_view_get_type ())
+#define PAGE_VIEW(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), PAGE_VIEW_TYPE, PageView))
+
+
+typedef struct PageViewPrivate PageViewPrivate;
+
+typedef struct
+{
+ GObject parent_instance;
+ PageViewPrivate *priv;
+} PageView;
+
+typedef struct
+{
+ GObjectClass parent_class;
+
+ void (*changed) (PageView *view);
+ void (*size_changed) (PageView *view);
+} PageViewClass;
+
+
+GType page_view_get_type (void);
+
+PageView *page_view_new (Page *page);
+
+Page *page_view_get_page (PageView *view);
+
+void page_view_set_selected (PageView *view, gboolean selected);
+
+gboolean page_view_get_selected (PageView *view);
+
+void page_view_set_x_offset (PageView *view, gint offset);
+
+void page_view_set_y_offset (PageView *view, gint offset);
+
+gint page_view_get_x_offset (PageView *view);
+
+gint page_view_get_y_offset (PageView *view);
+
+void page_view_set_width (PageView *view, gint width);
+
+void page_view_set_height (PageView *view, gint height);
+
+gint page_view_get_width (PageView *view);
+
+gint page_view_get_height (PageView *view);
+
+void page_view_button_press (PageView *view, gint x, gint y);
+
+void page_view_motion (PageView *view, gint x, gint y);
+
+void page_view_button_release (PageView *view, gint x, gint y);
+
+gint page_view_get_cursor (PageView *view);
+
+void page_view_render (PageView *view, cairo_t *context);
+
+#endif /* _PAGE_VIEW_H_ */
diff --git a/src/page.c b/src/page.c
new file mode 100644
index 0000000..5888a46
--- /dev/null
+++ b/src/page.c
@@ -0,0 +1,951 @@
+/*
+ * Copyright (C) 2009 Canonical Ltd.
+ * Author: Robert Ancell <robert.ancell@canonical.com>
+ *
+ * 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. See http://www.gnu.org/copyleft/gpl.html the full text of the
+ * license.
+ */
+
+#include <string.h>
+#include "page.h"
+
+
+enum {
+ IMAGE_CHANGED,
+ SIZE_CHANGED,
+ SCAN_LINE_CHANGED,
+ ORIENTATION_CHANGED,
+ CROP_CHANGED,
+ LAST_SIGNAL
+};
+static guint signals[LAST_SIGNAL] = { 0, };
+
+struct PagePrivate
+{
+ /* Resolution of page */
+ gint dpi;
+
+ /* Number of rows in this page or -1 if currently unknown */
+ gint rows;
+
+ /* Color profile */
+ gchar *color_profile;
+
+ /* Scanned image data */
+ GdkPixbuf *image;
+
+ /* Page is getting data */
+ gboolean scanning;
+
+ /* TRUE if have some page data */
+ gboolean has_data;
+
+ /* Expected next scan row */
+ gint scan_line;
+
+ /* Rotation of scanned data */
+ Orientation orientation;
+
+ /* Crop */
+ gboolean has_crop;
+ gchar *crop_name;
+ gint crop_x, crop_y, crop_width, crop_height;
+};
+
+G_DEFINE_TYPE (Page, page, G_TYPE_OBJECT);
+
+
+Page *
+page_new ()
+{
+ return g_object_new (PAGE_TYPE, NULL);
+}
+
+
+void
+page_setup (Page *page, gint width, gint height, gint dpi, Orientation orientation)
+{
+ page->priv->orientation = orientation;
+ page->priv->dpi = dpi;
+ if (orientation == LEFT_TO_RIGHT || orientation == RIGHT_TO_LEFT)
+ page->priv->rows = width;
+ else
+ page->priv->rows = height;
+ page->priv->image = gdk_pixbuf_new (GDK_COLORSPACE_RGB, FALSE,
+ 8,
+ width,
+ height);
+ g_return_if_fail (page->priv->image != NULL);
+ gdk_pixbuf_fill (page->priv->image, 0xFFFFFFFF);
+}
+
+
+void
+page_set_scan_area (Page *page, gint width, gint rows, gint dpi)
+{
+ gint height;
+
+ g_return_if_fail (page != NULL);
+
+ /* Variable height, try 50% of the width for now */
+ if (rows < 0)
+ height = width / 2;
+ else
+ height = rows;
+
+ /* Rotate page */
+ if (page->priv->orientation == LEFT_TO_RIGHT || page->priv->orientation == RIGHT_TO_LEFT) {
+ gint t;
+ t = width;
+ width = height;
+ height = t;
+ }
+
+ page->priv->rows = rows;
+ page->priv->dpi = dpi;
+
+ /* Create a white page */
+ /* NOTE: Pixbuf only supports 8 bit RGB images */
+ if (page->priv->image)
+ g_object_unref (page->priv->image);
+ page->priv->image = gdk_pixbuf_new (GDK_COLORSPACE_RGB, FALSE,
+ 8,
+ width,
+ height);
+ g_return_if_fail (page->priv->image != NULL);
+
+ gdk_pixbuf_fill (page->priv->image, 0xFFFFFFFF);
+ g_signal_emit (page, signals[SIZE_CHANGED], 0);
+ g_signal_emit (page, signals[IMAGE_CHANGED], 0);
+}
+
+
+void
+page_start (Page *page)
+{
+ g_return_if_fail (page != NULL);
+
+ page->priv->scanning = TRUE;
+ g_signal_emit (page, signals[SCAN_LINE_CHANGED], 0);
+}
+
+
+gboolean page_is_scanning (Page *page)
+{
+ g_return_val_if_fail (page != NULL, FALSE);
+
+ return page->priv->scanning;
+}
+
+
+static gint
+get_sample (guchar *data, gint depth, gint index)
+{
+ gint i, offset, value, n_bits;
+
+ /* Optimise if using 8 bit samples */
+ if (depth == 8)
+ return data[index];
+
+ /* Bit offset for this sample */
+ offset = depth * index;
+
+ /* Get the remaining bits in the octet this sample starts in */
+ i = offset / 8;
+ n_bits = 8 - offset % 8;
+ value = data[i] & (0xFF >> (8 - n_bits));
+
+ /* Add additional octets until get enough bits */
+ while (n_bits < depth) {
+ value = value << 8 | data[i++];
+ n_bits += 8;
+ }
+
+ /* Trim remaining bits off */
+ if (n_bits > depth)
+ value >>= n_bits - depth;
+
+ return value;
+}
+
+
+gboolean page_has_data (Page *page)
+{
+ g_return_val_if_fail (page != NULL, FALSE);
+ return page->priv->has_data;
+}
+
+
+gint page_get_scan_line (Page *page)
+{
+ g_return_val_if_fail (page != NULL, -1);
+ return page->priv->scan_line;
+}
+
+
+static void
+set_pixel (ScanLine *line, gint n, gint x, guchar *pixel)
+{
+ gint sample;
+ guchar *data;
+
+ data = line->data + line->data_length * n;
+
+ switch (line->format) {
+ case LINE_RGB:
+ pixel[0] = get_sample (data, line->depth, x*3) * 0xFF / ((1 << line->depth) - 1);
+ pixel[1] = get_sample (data, line->depth, x*3+1) * 0xFF / ((1 << line->depth) - 1);
+ pixel[2] = get_sample (data, line->depth, x*3+2) * 0xFF / ((1 << line->depth) - 1);
+ break;
+ case LINE_GRAY:
+ /* Bitmap, 0 = white, 1 = black */
+ sample = get_sample (data, line->depth, x) * 0xFF / ((1 << line->depth) - 1);
+ if (line->depth == 1)
+ sample = sample ? 0x00 : 0xFF;
+
+ pixel[0] = pixel[1] = pixel[2] = sample;
+ break;
+ case LINE_RED:
+ pixel[0] = get_sample (data, line->depth, x) * 0xFF / ((1 << line->depth) - 1);
+ break;
+ case LINE_GREEN:
+ pixel[1] = get_sample (data, line->depth, x) * 0xFF / ((1 << line->depth) - 1);
+ break;
+ case LINE_BLUE:
+ pixel[2] = get_sample (data, line->depth, x) * 0xFF / ((1 << line->depth) - 1);
+ break;
+ }
+}
+
+
+static void
+parse_line (Page *page, ScanLine *line, gint n, gboolean *size_changed)
+{
+ guchar *pixels;
+ gint line_number;
+ gint i, x = 0, y = 0, x_step = 0, y_step = 0;
+ gint rowstride, n_channels;
+
+ line_number = line->number + n;
+
+ /* Extend image if necessary */
+ while (line_number >= page_get_scan_height (page)) {
+ GdkPixbuf *image;
+ gint height, width, new_width, new_height;
+
+ /* Extend image */
+ new_width = width = gdk_pixbuf_get_width (page->priv->image);
+ new_height = height = gdk_pixbuf_get_height (page->priv->image);
+ if (page->priv->orientation == TOP_TO_BOTTOM || page->priv->orientation == BOTTOM_TO_TOP) {
+ new_height = height + width / 2;
+ g_debug("Extending image height from %d pixels to %d pixels", height, new_height);
+ }
+ else {
+ new_width = width + height / 2;
+ g_debug("Extending image width from %d pixels to %d pixels", width, new_width);
+ }
+ image = gdk_pixbuf_new (GDK_COLORSPACE_RGB, FALSE,
+ 8, new_width, new_height);
+
+ /* Copy old data */
+ gdk_pixbuf_fill (page->priv->image, 0xFFFFFFFF);
+ if (page->priv->orientation == TOP_TO_BOTTOM || page->priv->orientation == LEFT_TO_RIGHT)
+ gdk_pixbuf_copy_area (page->priv->image, 0, 0, width, height,
+ image, 0, 0);
+ else
+ gdk_pixbuf_copy_area (page->priv->image, 0, 0, width, height,
+ image, new_width - width, new_height - height);
+
+ g_object_unref (page->priv->image);
+ page->priv->image = image;
+
+ *size_changed = TRUE;
+ }
+
+ switch (page->priv->orientation) {
+ case TOP_TO_BOTTOM:
+ x = 0;
+ y = line_number;
+ x_step = 1;
+ y_step = 0;
+ break;
+ case BOTTOM_TO_TOP:
+ x = page_get_width (page) - 1;
+ y = page_get_height (page) - line_number - 1;
+ x_step = -1;
+ y_step = 0;
+ break;
+ case LEFT_TO_RIGHT:
+ x = line_number;
+ y = page_get_height (page) - 1;
+ x_step = 0;
+ y_step = -1;
+ break;
+ case RIGHT_TO_LEFT:
+ x = page_get_width (page) - line_number - 1;
+ y = 0;
+ x_step = 0;
+ y_step = 1;
+ break;
+ }
+ pixels = gdk_pixbuf_get_pixels (page->priv->image);
+ rowstride = gdk_pixbuf_get_rowstride (page->priv->image);
+ n_channels = gdk_pixbuf_get_n_channels (page->priv->image);
+ for (i = 0; i < line->width; i++) {
+ guchar *pixel;
+
+ pixel = pixels + y * rowstride + x * n_channels;
+ set_pixel (line, n, i, pixel);
+ x += x_step;
+ y += y_step;
+ }
+
+ page->priv->scan_line = line_number;
+}
+
+
+void
+page_parse_scan_line (Page *page, ScanLine *line)
+{
+ gint i;
+ gboolean size_changed = FALSE;
+
+ g_return_if_fail (page != NULL);
+
+ for (i = 0; i < line->n_lines; i++)
+ parse_line (page, line, i, &size_changed);
+
+ page->priv->has_data = TRUE;
+
+ if (size_changed)
+ g_signal_emit (page, signals[SIZE_CHANGED], 0);
+ g_signal_emit (page, signals[SCAN_LINE_CHANGED], 0);
+ g_signal_emit (page, signals[IMAGE_CHANGED], 0);
+}
+
+
+void
+page_finish (Page *page)
+{
+ gboolean size_changed = FALSE;
+
+ g_return_if_fail (page != NULL);
+
+ /* Trim page */
+ if (page->priv->rows < 0 &&
+ page->priv->scan_line != gdk_pixbuf_get_height (page->priv->image)) {
+ GdkPixbuf *image;
+ gint width, height, new_width, new_height;
+
+ new_width = width = gdk_pixbuf_get_width (page->priv->image);
+ new_height = height = gdk_pixbuf_get_height (page->priv->image);
+ if (page->priv->orientation == TOP_TO_BOTTOM || page->priv->orientation == BOTTOM_TO_TOP) {
+ new_height = page->priv->scan_line;
+ g_debug("Trimming image height from %d pixels to %d pixels", height, new_height);
+ }
+ else {
+ new_width = page->priv->scan_line;
+ g_debug("Trimming image width from %d pixels to %d pixels", width, new_width);
+ }
+ image = gdk_pixbuf_new (GDK_COLORSPACE_RGB, FALSE,
+ 8,
+ new_width, new_height);
+
+ /* Copy old data */
+ if (page->priv->orientation == TOP_TO_BOTTOM || page->priv->orientation == LEFT_TO_RIGHT)
+ gdk_pixbuf_copy_area (page->priv->image, 0, 0, width, height,
+ image, 0, 0);
+ else
+ gdk_pixbuf_copy_area (page->priv->image, width - new_width, height - new_height, width, height,
+ image, 0, 0);
+
+ g_object_unref (page->priv->image);
+ page->priv->image = image;
+ size_changed = TRUE;
+ }
+ page->priv->scanning = FALSE;
+
+ if (size_changed)
+ g_signal_emit (page, signals[SIZE_CHANGED], 0);
+ g_signal_emit (page, signals[SCAN_LINE_CHANGED], 0);
+}
+
+
+Orientation
+page_get_orientation (Page *page)
+{
+ g_return_val_if_fail (page != NULL, TOP_TO_BOTTOM);
+
+ return page->priv->orientation;
+}
+
+
+void
+page_set_orientation (Page *page, Orientation orientation)
+{
+ gint left_steps, t;
+ GdkPixbuf *image;
+ gboolean size_changed = FALSE;
+ gint width, height;
+
+ g_return_if_fail (page != NULL);
+
+ if (page->priv->orientation == orientation)
+ return;
+
+ /* Work out how many times it has been rotated to the left */
+ left_steps = orientation - page->priv->orientation;
+ if (left_steps < 0)
+ left_steps += 4;
+
+ width = page_get_width (page);
+ height = page_get_height (page);
+
+ /* Rotate image */
+ if (left_steps == 1)
+ image = gdk_pixbuf_rotate_simple (page->priv->image, GDK_PIXBUF_ROTATE_COUNTERCLOCKWISE);
+ else if (left_steps == 2)
+ image = gdk_pixbuf_rotate_simple (page->priv->image, GDK_PIXBUF_ROTATE_UPSIDEDOWN);
+ else
+ image = gdk_pixbuf_rotate_simple (page->priv->image, GDK_PIXBUF_ROTATE_CLOCKWISE);
+ g_object_unref (page->priv->image);
+ page->priv->image = image;
+ if (left_steps != 2)
+ size_changed = TRUE;
+
+ /* Rotate crop */
+ if (page->priv->has_crop) {
+ switch (left_steps) {
+ /* 90 degrees counter-clockwise */
+ case 1:
+ t = page->priv->crop_x;
+ page->priv->crop_x = page->priv->crop_y;
+ page->priv->crop_y = width - (t + page->priv->crop_width);
+ t = page->priv->crop_width;
+ page->priv->crop_width = page->priv->crop_height;
+ page->priv->crop_height = t;
+ break;
+ /* 180 degrees */
+ case 2:
+ page->priv->crop_x = width - (page->priv->crop_x + page->priv->crop_width);
+ page->priv->crop_y = width - (page->priv->crop_y + page->priv->crop_height);
+ break;
+ /* 90 degrees clockwise */
+ case 3:
+ t = page->priv->crop_y;
+ page->priv->crop_y = page->priv->crop_x;
+ page->priv->crop_x = height - (t + page->priv->crop_height);
+ t = page->priv->crop_width;
+ page->priv->crop_width = page->priv->crop_height;
+ page->priv->crop_height = t;
+ break;
+ }
+ }
+
+ page->priv->orientation = orientation;
+ if (size_changed)
+ g_signal_emit (page, signals[SIZE_CHANGED], 0);
+ g_signal_emit (page, signals[IMAGE_CHANGED], 0);
+ g_signal_emit (page, signals[ORIENTATION_CHANGED], 0);
+ g_signal_emit (page, signals[CROP_CHANGED], 0);
+}
+
+
+void
+page_rotate_left (Page *page)
+{
+ Orientation orientation;
+
+ g_return_if_fail (page != NULL);
+
+ orientation = page_get_orientation (page);
+ if (orientation == RIGHT_TO_LEFT)
+ orientation = TOP_TO_BOTTOM;
+ else
+ orientation++;
+ page_set_orientation (page, orientation);
+}
+
+
+void
+page_rotate_right (Page *page)
+{
+ Orientation orientation;
+
+ orientation = page_get_orientation (page);
+ if (orientation == TOP_TO_BOTTOM)
+ orientation = RIGHT_TO_LEFT;
+ else
+ orientation--;
+ page_set_orientation (page, orientation);
+}
+
+
+gint
+page_get_dpi (Page *page)
+{
+ g_return_val_if_fail (page != NULL, 0);
+
+ return page->priv->dpi;
+}
+
+
+gboolean
+page_is_landscape (Page *page)
+{
+ return page_get_width (page) > page_get_height (page);
+}
+
+
+gint
+page_get_width (Page *page)
+{
+ g_return_val_if_fail (page != NULL, 0);
+ return gdk_pixbuf_get_width (page->priv->image);
+}
+
+
+gint
+page_get_height (Page *page)
+{
+ g_return_val_if_fail (page != NULL, 0);
+ return gdk_pixbuf_get_height (page->priv->image);
+}
+
+
+gint
+page_get_scan_width (Page *page)
+{
+ g_return_val_if_fail (page != NULL, 0);
+
+ if (page->priv->orientation == TOP_TO_BOTTOM || page->priv->orientation == BOTTOM_TO_TOP)
+ return gdk_pixbuf_get_width (page->priv->image);
+ else
+ return gdk_pixbuf_get_height (page->priv->image);
+}
+
+
+gint
+page_get_scan_height (Page *page)
+{
+ g_return_val_if_fail (page != NULL, 0);
+
+ if (page->priv->orientation == TOP_TO_BOTTOM || page->priv->orientation == BOTTOM_TO_TOP)
+ return gdk_pixbuf_get_height (page->priv->image);
+ else
+ return gdk_pixbuf_get_width (page->priv->image);
+}
+
+
+void page_set_color_profile (Page *page, const gchar *color_profile)
+{
+ g_free (page->priv->color_profile);
+ page->priv->color_profile = g_strdup (color_profile);
+}
+
+
+const gchar *page_get_color_profile (Page *page)
+{
+ return page->priv->color_profile;
+}
+
+
+void
+page_set_no_crop (Page *page)
+{
+ g_return_if_fail (page != NULL);
+
+ if (!page->priv->has_crop)
+ return;
+ page->priv->has_crop = FALSE;
+ g_signal_emit (page, signals[CROP_CHANGED], 0);
+}
+
+
+void
+page_set_custom_crop (Page *page, gint width, gint height)
+{
+ //gint pw, ph;
+
+ g_return_if_fail (page != NULL);
+ g_return_if_fail (width >= 1);
+ g_return_if_fail (height >= 1);
+
+ if (!page->priv->crop_name &&
+ page->priv->has_crop &&
+ page->priv->crop_width == width &&
+ page->priv->crop_height == height)
+ return;
+ g_free (page->priv->crop_name);
+ page->priv->crop_name = NULL;
+ page->priv->has_crop = TRUE;
+
+ page->priv->crop_width = width;
+ page->priv->crop_height = height;
+
+ /*pw = page_get_width (page);
+ ph = page_get_height (page);
+ if (page->priv->crop_width < pw)
+ page->priv->crop_x = (pw - page->priv->crop_width) / 2;
+ else
+ page->priv->crop_x = 0;
+ if (page->priv->crop_height < ph)
+ page->priv->crop_y = (ph - page->priv->crop_height) / 2;
+ else
+ page->priv->crop_y = 0;*/
+
+ g_signal_emit (page, signals[CROP_CHANGED], 0);
+}
+
+
+void
+page_set_named_crop (Page *page, const gchar *name)
+{
+ struct {
+ const gchar *name;
+ /* Width and height in inches */
+ gdouble width, height;
+ } named_crops[] =
+ {
+ {"A4", 8.3, 11.7},
+ {"A5", 5.8, 8.3},
+ {"A6", 4.1, 5.8},
+ {"letter", 8.5, 11},
+ {"legal", 8.5, 14},
+ {"4x6", 4, 6},
+ {NULL, 0, 0}
+ };
+ gint i;
+ gint pw, ph;
+ double width, height;
+
+ g_return_if_fail (page != NULL);
+
+ for (i = 0; named_crops[i].name && strcmp (name, named_crops[i].name) != 0; i++);
+ width = named_crops[i].width;
+ height = named_crops[i].height;
+
+ if (!named_crops[i].name) {
+ g_warning ("Unknown paper size '%s'", name);
+ return;
+ }
+
+ g_free (page->priv->crop_name);
+ page->priv->crop_name = g_strdup (name);
+ page->priv->has_crop = TRUE;
+
+ pw = page_get_width (page);
+ ph = page_get_height (page);
+
+ /* Rotate to match original aspect */
+ if (pw > ph) {
+ double t;
+ t = width;
+ width = height;
+ height = t;
+ }
+
+ /* Custom crop, make slightly smaller than original */
+ page->priv->crop_width = (int) (width * page->priv->dpi + 0.5);
+ page->priv->crop_height = (int) (height * page->priv->dpi + 0.5);
+
+ if (page->priv->crop_width < pw)
+ page->priv->crop_x = (pw - page->priv->crop_width) / 2;
+ else
+ page->priv->crop_x = 0;
+ if (page->priv->crop_height < ph)
+ page->priv->crop_y = (ph - page->priv->crop_height) / 2;
+ else
+ page->priv->crop_y = 0;
+ g_signal_emit (page, signals[CROP_CHANGED], 0);
+}
+
+
+void
+page_move_crop (Page *page, gint x, gint y)
+{
+ g_return_if_fail (x >= 0);
+ g_return_if_fail (y >= 0);
+ g_return_if_fail (x < page_get_width (page));
+ g_return_if_fail (y < page_get_height (page));
+
+ page->priv->crop_x = x;
+ page->priv->crop_y = y;
+ g_signal_emit (page, signals[CROP_CHANGED], 0);
+}
+
+
+void
+page_rotate_crop (Page *page)
+{
+ gint t;
+
+ g_return_if_fail (page != NULL);
+
+ if (!page->priv->has_crop)
+ return;
+
+ t = page->priv->crop_width;
+ page->priv->crop_width = page->priv->crop_height;
+ page->priv->crop_height = t;
+
+ /* Clip custom crops */
+ if (!page->priv->crop_name) {
+ gint w, h;
+
+ w = page_get_width (page);
+ h = page_get_height (page);
+
+ if (page->priv->crop_x + page->priv->crop_width > w)
+ page->priv->crop_x = w - page->priv->crop_width;
+ if (page->priv->crop_x < 0) {
+ page->priv->crop_x = 0;
+ page->priv->crop_width = w;
+ }
+ if (page->priv->crop_y + page->priv->crop_height > h)
+ page->priv->crop_y = h - page->priv->crop_height;
+ if (page->priv->crop_y < 0) {
+ page->priv->crop_y = 0;
+ page->priv->crop_height = h;
+ }
+ }
+
+ g_signal_emit (page, signals[CROP_CHANGED], 0);
+}
+
+
+gboolean
+page_has_crop (Page *page)
+{
+ g_return_val_if_fail (page != NULL, FALSE);
+ return page->priv->has_crop;
+}
+
+
+void
+page_get_crop (Page *page, gint *x, gint *y, gint *width, gint *height)
+{
+ g_return_if_fail (page != NULL);
+
+ if (x)
+ *x = page->priv->crop_x;
+ if (y)
+ *y = page->priv->crop_y;
+ if (width)
+ *width = page->priv->crop_width;
+ if (height)
+ *height = page->priv->crop_height;
+}
+
+
+gchar *
+page_get_named_crop (Page *page)
+{
+ g_return_val_if_fail (page != NULL, NULL);
+
+ if (page->priv->crop_name)
+ return g_strdup (page->priv->crop_name);
+ else
+ return NULL;
+}
+
+
+GdkPixbuf *
+page_get_image (Page *page)
+{
+ g_return_val_if_fail (page != NULL, NULL);
+ return g_object_ref (page->priv->image);
+}
+
+
+GdkPixbuf *
+page_get_cropped_image (Page *page)
+{
+ GdkPixbuf *image, *cropped_image;
+ gint x, y, w, h, pw, ph;
+
+ g_return_val_if_fail (page != NULL, NULL);
+
+ image = page_get_image (page);
+
+ if (!page->priv->has_crop)
+ return image;
+
+ x = page->priv->crop_x;
+ y = page->priv->crop_y;
+ w = page->priv->crop_width;
+ h = page->priv->crop_height;
+ pw = gdk_pixbuf_get_width (image);
+ ph = gdk_pixbuf_get_height (image);
+
+ /* Trim crop */
+ if (x + w >= pw)
+ w = pw - x;
+ if (y + h >= ph)
+ h = ph - y;
+
+ cropped_image = gdk_pixbuf_new_subpixbuf (image, x, y, w, h);
+ g_object_unref (image);
+
+ return cropped_image;
+}
+
+
+static gboolean
+write_pixbuf_data (const gchar *buf, gsize count, GError **error, GFileOutputStream *stream)
+{
+ return g_output_stream_write_all (G_OUTPUT_STREAM (stream), buf, count, NULL, NULL, error);
+}
+
+
+static gchar *
+get_icc_data_encoded (const gchar *icc_profile_filename)
+{
+ gchar *contents = NULL;
+ gchar *contents_encode = NULL;
+ gsize length;
+ gboolean ret;
+ GError *error = NULL;
+
+ /* Get binary data */
+ ret = g_file_get_contents (icc_profile_filename, &contents, &length, &error);
+ if (!ret) {
+ g_warning ("failed to get icc profile data: %s", error->message);
+ g_error_free (error);
+ }
+ else {
+ /* Encode into base64 */
+ contents_encode = g_base64_encode ((const guchar *) contents, length);
+ }
+
+ g_free (contents);
+ return contents_encode;
+}
+
+
+gboolean
+page_save (Page *page, const gchar *type, GFile *file, GError **error)
+{
+ GFileOutputStream *stream;
+ GdkPixbuf *image;
+ gboolean result = FALSE;
+ gchar *icc_profile_data = NULL;
+
+ stream = g_file_replace (file, NULL, FALSE, G_FILE_CREATE_NONE, NULL, error);
+ if (!stream)
+ return FALSE;
+
+ image = page_get_cropped_image (page);
+
+ if (page->priv->color_profile != NULL)
+ icc_profile_data = get_icc_data_encoded (page->priv->color_profile);
+
+ if (strcmp (type, "jpeg") == 0) {
+ /* ICC profile is awaiting review in gtk2+ bugzilla */
+ gchar *keys[] = { "quality", /* "icc-profile", */ NULL };
+ gchar *values[] = { "90", /* icc_profile_data, */ NULL };
+ result = gdk_pixbuf_save_to_callbackv (image,
+ (GdkPixbufSaveFunc) write_pixbuf_data, stream,
+ "jpeg", keys, values, error);
+ }
+ else if (strcmp (type, "png") == 0) {
+ gchar *keys[] = { "icc-profile", NULL };
+ gchar *values[] = { icc_profile_data, NULL };
+ if (icc_profile_data == NULL)
+ keys[0] = NULL;
+ result = gdk_pixbuf_save_to_callbackv (image,
+ (GdkPixbufSaveFunc) write_pixbuf_data, stream,
+ "png", keys, values, error);
+ }
+ else if (strcmp (type, "tiff") == 0) {
+ gchar *keys[] = { "compression", "icc-profile", NULL };
+ gchar *values[] = { "8" /* Deflate compression */, icc_profile_data, NULL };
+ if (icc_profile_data == NULL)
+ keys[1] = NULL;
+ result = gdk_pixbuf_save_to_callbackv (image,
+ (GdkPixbufSaveFunc) write_pixbuf_data, stream,
+ "tiff", keys, values, error);
+ }
+ else
+ result = FALSE; // FIXME: Set GError
+
+ g_free (icc_profile_data);
+ g_object_unref (image);
+ g_object_unref (stream);
+
+ return result;
+}
+
+
+static void
+page_finalize (GObject *object)
+{
+ Page *page = PAGE (object);
+ if (page->priv->image)
+ g_object_unref (page->priv->image);
+ page->priv->image = NULL;
+ G_OBJECT_CLASS (page_parent_class)->finalize (object);
+}
+
+
+static void
+page_class_init (PageClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->finalize = page_finalize;
+
+ signals[IMAGE_CHANGED] =
+ g_signal_new ("image-changed",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (PageClass, image_changed),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE, 0);
+ signals[SIZE_CHANGED] =
+ g_signal_new ("size-changed",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (PageClass, size_changed),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE, 0);
+ signals[SCAN_LINE_CHANGED] =
+ g_signal_new ("scan-line-changed",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (PageClass, scan_line_changed),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE, 0);
+ signals[ORIENTATION_CHANGED] =
+ g_signal_new ("orientation-changed",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (PageClass, orientation_changed),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE, 0);
+ signals[CROP_CHANGED] =
+ g_signal_new ("crop-changed",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (PageClass, crop_changed),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE, 0);
+
+ g_type_class_add_private (klass, sizeof (PagePrivate));
+}
+
+
+static void
+page_init (Page *page)
+{
+ page->priv = G_TYPE_INSTANCE_GET_PRIVATE (page, PAGE_TYPE, PagePrivate);
+ page->priv->orientation = TOP_TO_BOTTOM;
+}
diff --git a/src/page.h b/src/page.h
new file mode 100644
index 0000000..b36fe32
--- /dev/null
+++ b/src/page.h
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2009 Canonical Ltd.
+ * Author: Robert Ancell <robert.ancell@canonical.com>
+ *
+ * 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. See http://www.gnu.org/copyleft/gpl.html the full text of the
+ * license.
+ */
+
+#ifndef _PAGE_H_
+#define _PAGE_H_
+
+#include <glib-object.h>
+#include <gio/gio.h>
+#include <gdk-pixbuf/gdk-pixbuf.h>
+#include "scanner.h"
+
+G_BEGIN_DECLS
+
+#define PAGE_TYPE (page_get_type ())
+#define PAGE(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), PAGE_TYPE, Page))
+
+typedef enum
+{
+ TOP_TO_BOTTOM,
+ LEFT_TO_RIGHT,
+ BOTTOM_TO_TOP,
+ RIGHT_TO_LEFT
+} Orientation;
+
+
+typedef struct PagePrivate PagePrivate;
+
+typedef struct
+{
+ GObject parent_instance;
+ PagePrivate *priv;
+} Page;
+
+typedef struct
+{
+ GObjectClass parent_class;
+
+ void (*image_changed) (Page *page);
+ void (*size_changed) (Page *page);
+ void (*scan_line_changed) (Page *page);
+ void (*orientation_changed) (Page *page);
+ void (*crop_changed) (Page *page);
+} PageClass;
+
+
+GType page_get_type (void);
+
+Page *page_new (void);
+
+// FIXME: Should be part of page_new
+void page_setup (Page *page, gint width, gint height, gint dpi, Orientation orientation);
+
+void page_set_scan_area (Page *page, gint width, gint rows, gint dpi);
+
+gint page_get_dpi (Page *page);
+
+gboolean page_is_landscape (Page *page);
+
+gint page_get_width (Page *page);
+
+gint page_get_height (Page *page);
+
+gint page_get_scan_width (Page *page);
+
+gint page_get_scan_height (Page *page);
+
+void page_set_color_profile (Page *page, const gchar *color_profile);
+
+const gchar *page_get_color_profile (Page *page);
+
+void page_start (Page *page);
+
+gboolean page_is_scanning (Page *page);
+
+gboolean page_has_data (Page *page);
+
+gint page_get_scan_line (Page *page);
+
+void page_parse_scan_line (Page *page, ScanLine *line);
+
+void page_finish (Page *page);
+
+Orientation page_get_orientation (Page *page);
+
+void page_set_orientation (Page *page, Orientation orientation);
+
+void page_rotate_left (Page *page);
+
+void page_rotate_right (Page *page);
+
+void page_set_no_crop (Page *page);
+
+void page_set_custom_crop (Page *page, gint width, gint height);
+
+void page_set_named_crop (Page *page, const gchar *name);
+
+void page_move_crop (Page *page, gint x, gint y);
+
+void page_rotate_crop (Page *page);
+
+gboolean page_has_crop (Page *page);
+
+void page_get_crop (Page *page, gint *x, gint *y, gint *width, gint *height);
+
+gchar *page_get_named_crop (Page *page);
+
+GdkPixbuf *page_get_image (Page *page);
+
+GdkPixbuf *page_get_cropped_image (Page *page);
+
+gboolean page_save (Page *page, const gchar *type, GFile *file, GError **error);
+
+#endif /* _PAGE_H_ */
diff --git a/src/scanner.c b/src/scanner.c
new file mode 100644
index 0000000..daf1b15
--- /dev/null
+++ b/src/scanner.c
@@ -0,0 +1,1630 @@
+/*
+ * Copyright (C) 2009 Canonical Ltd.
+ * Author: Robert Ancell <robert.ancell@canonical.com>
+ *
+ * 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. See http://www.gnu.org/copyleft/gpl.html the full text of the
+ * license.
+ */
+
+#include <stdlib.h>
+#include <string.h>
+#include <math.h>
+#include <sane/sane.h>
+#include <sane/saneopts.h>
+#include <glib/gi18n.h>
+
+#include "scanner.h"
+
+/* TODO: Could indicate the start of the next page immediately after the last page is received (i.e. before the sane_cancel()) */
+
+enum {
+ UPDATE_DEVICES,
+ AUTHORIZE,
+ EXPECT_PAGE,
+ GOT_PAGE_INFO,
+ GOT_LINE,
+ SCAN_FAILED,
+ PAGE_DONE,
+ DOCUMENT_DONE,
+ SCANNING_CHANGED,
+ LAST_SIGNAL
+};
+static guint signals[LAST_SIGNAL] = { 0, };
+
+typedef struct
+{
+ Scanner *instance;
+ guint sig;
+ gpointer data;
+} SignalInfo;
+
+typedef struct
+{
+ gchar *device;
+ gdouble dpi;
+ ScanMode scan_mode;
+ gint depth;
+ gboolean type;
+ gint page_width, page_height;
+} ScanJob;
+
+typedef struct
+{
+ enum
+ {
+ REQUEST_CANCEL,
+ REQUEST_REDETECT,
+ REQUEST_START_SCAN,
+ REQUEST_QUIT
+ } type;
+ ScanJob *job;
+} ScanRequest;
+
+typedef struct
+{
+ gchar *username, *password;
+} Credentials;
+
+typedef enum
+{
+ STATE_IDLE = 0,
+ STATE_REDETECT,
+ STATE_OPEN,
+ STATE_GET_OPTION,
+ STATE_START,
+ STATE_GET_PARAMETERS,
+ STATE_READ
+} ScanState;
+
+struct ScannerPrivate
+{
+ GAsyncQueue *scan_queue, *authorize_queue;
+ GThread *thread;
+
+ gchar *default_device;
+
+ ScanState state;
+ gboolean redetect;
+
+ GList *job_queue;
+
+ /* Handle to SANE device */
+ SANE_Handle handle;
+ gchar *current_device;
+
+ SANE_Parameters parameters;
+
+ /* Last option read */
+ SANE_Int option_index;
+
+ /* Buffer for received line */
+ SANE_Byte *buffer;
+ SANE_Int buffer_size, n_used;
+
+ SANE_Int bytes_remaining, line_count, pass_number, page_number, notified_page;
+
+ gboolean scanning;
+};
+
+G_DEFINE_TYPE (Scanner, scanner, G_TYPE_OBJECT);
+
+
+/* Table of scanner objects for each thread (required for authorization callback) */
+static GHashTable *scanners;
+
+
+static gboolean
+send_signal (SignalInfo *info)
+{
+ g_signal_emit (info->instance, signals[info->sig], 0, info->data);
+
+ switch (info->sig) {
+ case UPDATE_DEVICES:
+ {
+ GList *iter, *devices = info->data;
+ for (iter = devices; iter; iter = iter->next) {
+ ScanDevice *device = iter->data;
+ g_free (device->name);
+ g_free (device->label);
+ g_free (device);
+ }
+ g_list_free (devices);
+ }
+ break;
+ case AUTHORIZE:
+ {
+ gchar *resource = info->data;
+ g_free (resource);
+ }
+ break;
+ case GOT_PAGE_INFO:
+ {
+ ScanPageInfo *page_info = info->data;
+ g_free (page_info);
+ }
+ break;
+ case GOT_LINE:
+ {
+ ScanLine *line = info->data;
+ g_free(line->data);
+ g_free(line);
+ }
+ break;
+ case SCAN_FAILED:
+ {
+ GError *error = info->data;
+ g_error_free (error);
+ }
+ break;
+ default:
+ case EXPECT_PAGE:
+ case PAGE_DONE:
+ case DOCUMENT_DONE:
+ case LAST_SIGNAL:
+ g_assert (info->data == NULL);
+ break;
+ }
+ g_free (info);
+
+ return FALSE;
+}
+
+
+/* Emit signals in main loop */
+static void
+emit_signal (Scanner *scanner, guint sig, gpointer data)
+{
+ SignalInfo *info;
+
+ info = g_malloc(sizeof(SignalInfo));
+ info->instance = scanner;
+ info->sig = sig;
+ info->data = data;
+ g_idle_add ((GSourceFunc) send_signal, info);
+}
+
+
+static void
+set_scanning (Scanner *scanner, gboolean is_scanning)
+{
+ if ((scanner->priv->scanning && !is_scanning) || (!scanner->priv->scanning && is_scanning)) {
+ scanner->priv->scanning = is_scanning;
+ emit_signal (scanner, SCANNING_CHANGED, NULL);
+ }
+}
+
+
+static gint
+get_device_weight (const gchar *device)
+{
+ /* NOTE: This is using trends in the naming of SANE devices, SANE should be able to provide this information better */
+
+ /* Use webcams as a last resort */
+ if (g_str_has_prefix (device, "vfl:"))
+ return 2;
+
+ /* Use locally connected devices first */
+ if (strstr (device, "usb"))
+ return 0;
+
+ return 1;
+}
+
+
+static gint
+compare_devices (ScanDevice *device1, ScanDevice *device2)
+{
+ gint weight1, weight2;
+
+ /* TODO: Should do some fuzzy matching on the last selected device and set that to the default */
+
+ weight1 = get_device_weight (device1->name);
+ weight2 = get_device_weight (device2->name);
+ if (weight1 != weight2)
+ return weight1 - weight2;
+
+ return strcmp (device1->label, device2->label);
+}
+
+
+static const char *
+get_status_string (SANE_Status status)
+{
+ struct {
+ SANE_Status status;
+ const char *name;
+ } status_names[] = {
+ { SANE_STATUS_GOOD, "SANE_STATUS_GOOD"},
+ { SANE_STATUS_UNSUPPORTED, "SANE_STATUS_UNSUPPORTED"},
+ { SANE_STATUS_CANCELLED, "SANE_STATUS_CANCELLED"},
+ { SANE_STATUS_DEVICE_BUSY, "SANE_STATUS_DEVICE_BUSY"},
+ { SANE_STATUS_INVAL, "SANE_STATUS_INVAL"},
+ { SANE_STATUS_EOF, "SANE_STATUS_EOF"},
+ { SANE_STATUS_JAMMED, "SANE_STATUS_JAMMED"},
+ { SANE_STATUS_NO_DOCS, "SANE_STATUS_NO_DOCS"},
+ { SANE_STATUS_COVER_OPEN, "SANE_STATUS_COVER_OPEN"},
+ { SANE_STATUS_IO_ERROR, "SANE_STATUS_IO_ERROR"},
+ { SANE_STATUS_NO_MEM, "SANE_STATUS_NO_MEM"},
+ { SANE_STATUS_ACCESS_DENIED, "SANE_STATUS_ACCESS_DENIED"},
+ { -1, NULL}
+ };
+ static char *unknown_string = NULL;
+ int i;
+
+ for (i = 0; status_names[i].name != NULL && status_names[i].status != status; i++);
+
+ if (status_names[i].name == NULL) {
+ g_free (unknown_string);
+ unknown_string = g_strdup_printf ("SANE_STATUS(%d)", status);
+ return unknown_string; /* Note result is undefined on second call to this function */
+ }
+
+ return status_names[i].name;
+}
+
+
+static const char *
+get_action_string (SANE_Action action)
+{
+ struct {
+ SANE_Action action;
+ const char *name;
+ } action_names[] = {
+ { SANE_ACTION_GET_VALUE, "SANE_ACTION_GET_VALUE" },
+ { SANE_ACTION_SET_VALUE, "SANE_ACTION_SET_VALUE" },
+ { SANE_ACTION_SET_AUTO, "SANE_ACTION_SET_AUTO" },
+ { -1, NULL}
+ };
+ static char *unknown_string = NULL;
+ int i;
+
+ for (i = 0; action_names[i].name != NULL && action_names[i].action != action; i++);
+
+ if (action_names[i].name == NULL) {
+ g_free (unknown_string);
+ unknown_string = g_strdup_printf ("SANE_ACTION(%d)", action);
+ return unknown_string; /* Note result is undefined on second call to this function */
+ }
+
+ return action_names[i].name;
+}
+
+
+static const char *
+get_frame_string (SANE_Frame frame)
+{
+ struct {
+ SANE_Frame frame;
+ const char *name;
+ } frame_names[] = {
+ { SANE_FRAME_GRAY, "SANE_FRAME_GRAY" },
+ { SANE_FRAME_RGB, "SANE_FRAME_RGB" },
+ { SANE_FRAME_RED, "SANE_FRAME_RED" },
+ { SANE_FRAME_GREEN, "SANE_FRAME_GREEN" },
+ { SANE_FRAME_BLUE, "SANE_FRAME_BLUE" },
+ { -1, NULL}
+ };
+ static char *unknown_string = NULL;
+ int i;
+
+ for (i = 0; frame_names[i].name != NULL && frame_names[i].frame != frame; i++);
+
+ if (frame_names[i].name == NULL) {
+ g_free (unknown_string);
+ unknown_string = g_strdup_printf ("SANE_FRAME(%d)", frame);
+ return unknown_string; /* Note result is undefined on second call to this function */
+ }
+
+ return frame_names[i].name;
+}
+
+
+static void
+do_redetect (Scanner *scanner)
+{
+ const SANE_Device **device_list, **device_iter;
+ SANE_Status status;
+ GList *devices = NULL;
+
+ status = sane_get_devices (&device_list, SANE_FALSE);
+ g_debug ("sane_get_devices () -> %s", get_status_string (status));
+ if (status != SANE_STATUS_GOOD) {
+ g_warning ("Unable to get SANE devices: %s", sane_strstatus(status));
+ scanner->priv->state = STATE_IDLE;
+ return;
+ }
+
+ for (device_iter = device_list; *device_iter; device_iter++) {
+ const SANE_Device *device = *device_iter;
+ ScanDevice *scan_device;
+ gchar *c, *label;
+
+ g_debug ("Device: name=\"%s\" vendor=\"%s\" model=\"%s\" type=\"%s\"",
+ device->name, device->vendor, device->model, device->type);
+
+ scan_device = g_malloc0 (sizeof (ScanDevice));
+
+ scan_device->name = g_strdup (device->name);
+
+ /* Abbreviate HP as it is a long string and does not match what is on the physical scanner */
+ if (strcmp (device->vendor, "Hewlett-Packard") == 0)
+ label = g_strdup_printf ("HP %s", device->model);
+ else
+ label = g_strdup_printf ("%s %s", device->vendor, device->model);
+
+ /* Replace underscored in name */
+ for (c = label; *c; c++)
+ if (*c == '_')
+ *c = ' ';
+
+ scan_device->label = label;
+
+ devices = g_list_append (devices, scan_device);
+ }
+
+ /* Sort devices by priority */
+ devices = g_list_sort (devices, (GCompareFunc) compare_devices);
+
+ scanner->priv->redetect = FALSE;
+ scanner->priv->state = STATE_IDLE;
+
+ g_free (scanner->priv->default_device);
+ if (devices) {
+ ScanDevice *device = g_list_nth_data (devices, 0);
+ scanner->priv->default_device = g_strdup (device->name);
+ }
+ else
+ scanner->priv->default_device = NULL;
+
+ emit_signal (scanner, UPDATE_DEVICES, devices);
+}
+
+
+static gboolean
+control_option (SANE_Handle handle, const SANE_Option_Descriptor *option, SANE_Int index, SANE_Action action, void *value)
+{
+ SANE_Status status;
+ gchar *old_value;
+
+ switch (option->type) {
+ case SANE_TYPE_BOOL:
+ old_value = g_strdup_printf (*((SANE_Bool *) value) ? "SANE_TRUE" : "SANE_FALSE");
+ break;
+ case SANE_TYPE_INT:
+ old_value = g_strdup_printf ("%d", *((SANE_Int *) value));
+ break;
+ case SANE_TYPE_FIXED:
+ old_value = g_strdup_printf ("%f", SANE_UNFIX (*((SANE_Fixed *) value)));
+ break;
+ case SANE_TYPE_STRING:
+ old_value = g_strdup_printf ("\"%s\"", (char *) value);
+ break;
+ default:
+ old_value = g_strdup ("?");
+ break;
+ }
+
+ status = sane_control_option (handle, index, action, value, NULL);
+ switch (option->type) {
+ case SANE_TYPE_BOOL:
+ g_debug ("sane_control_option (%d, %s, %s) -> (%s, %s)",
+ index, get_action_string (action),
+ *((SANE_Bool *) value) ? "SANE_TRUE" : "SANE_FALSE",
+ get_status_string (status),
+ old_value);
+ break;
+ case SANE_TYPE_INT:
+ g_debug ("sane_control_option (%d, %s, %d) -> (%s, %s)",
+ index, get_action_string (action),
+ *((SANE_Int *) value),
+ get_status_string (status),
+ old_value);
+ break;
+ case SANE_TYPE_FIXED:
+ g_debug ("sane_control_option (%d, %s, %f) -> (%s, %s)",
+ index, get_action_string (action),
+ SANE_UNFIX (*((SANE_Fixed *) value)),
+ get_status_string (status),
+ old_value);
+ break;
+ case SANE_TYPE_STRING:
+ g_debug ("sane_control_option (%d, %s, \"%s\") -> (%s, %s)",
+ index, get_action_string (action),
+ (char *) value,
+ get_status_string (status),
+ old_value);
+ break;
+ default:
+ break;
+ }
+ g_free (old_value);
+
+ if (status != SANE_STATUS_GOOD)
+ g_warning ("Error setting option %s: %s", option->name, sane_strstatus(status));
+
+ return status == SANE_STATUS_GOOD;
+}
+
+
+static gboolean
+set_default_option (SANE_Handle handle, const SANE_Option_Descriptor *option, SANE_Int option_index)
+{
+ SANE_Status status;
+
+ status = sane_control_option (handle, option_index, SANE_ACTION_SET_AUTO, NULL, NULL);
+ g_debug ("sane_control_option (%d, SANE_ACTION_SET_AUTO) -> %s",
+ option_index, get_status_string (status));
+ if (status != SANE_STATUS_GOOD)
+ g_warning ("Error setting default option %s: %s", option->name, sane_strstatus(status));
+
+ return status == SANE_STATUS_GOOD;
+}
+
+
+static void
+set_bool_option (SANE_Handle handle, const SANE_Option_Descriptor *option, SANE_Int option_index, SANE_Bool value, SANE_Bool *result)
+{
+ SANE_Bool v = value;
+ g_return_if_fail (option->type == SANE_TYPE_BOOL);
+ control_option (handle, option, option_index, SANE_ACTION_SET_VALUE, &v);
+ if (result)
+ *result = v;
+}
+
+
+static void
+set_int_option (SANE_Handle handle, const SANE_Option_Descriptor *option, SANE_Int option_index, SANE_Int value, SANE_Int *result)
+{
+ SANE_Int v = value;
+
+ g_return_if_fail (option->type == SANE_TYPE_INT);
+
+ if (option->constraint_type == SANE_CONSTRAINT_RANGE) {
+ if (option->constraint.range->quant)
+ v *= option->constraint.range->quant;
+ if (v < option->constraint.range->min)
+ v = option->constraint.range->min;
+ if (v > option->constraint.range->max)
+ v = option->constraint.range->max;
+ }
+ else if (option->constraint_type == SANE_CONSTRAINT_WORD_LIST) {
+ int i;
+ SANE_Int distance = INT_MAX, nearest = 0;
+
+ /* Find nearest value to requested */
+ for (i = 0; i < option->constraint.word_list[0]; i++) {
+ SANE_Int x = option->constraint.word_list[i+1];
+ if (abs (x - v) < distance) {
+ distance = abs (x - v);
+ nearest = x;
+ }
+ }
+ v = nearest;
+ }
+
+ control_option (handle, option, option_index, SANE_ACTION_SET_VALUE, &v);
+ if (result)
+ *result = v;
+}
+
+
+static void
+set_fixed_option (SANE_Handle handle, const SANE_Option_Descriptor *option, SANE_Int option_index, gdouble value, gdouble *result)
+{
+ gdouble v = value;
+ SANE_Fixed v_fixed;
+
+ g_return_if_fail (option->type == SANE_TYPE_FIXED);
+
+ if (option->constraint_type == SANE_CONSTRAINT_RANGE) {
+ double min = SANE_UNFIX (option->constraint.range->min);
+ double max = SANE_UNFIX (option->constraint.range->max);
+
+ if (v < min)
+ v = min;
+ if (v > max)
+ v = max;
+ }
+ else if (option->constraint_type == SANE_CONSTRAINT_WORD_LIST) {
+ int i;
+ double distance = DBL_MAX, nearest = 0.0;
+
+ /* Find nearest value to requested */
+ for (i = 0; i < option->constraint.word_list[0]; i++) {
+ double x = SANE_UNFIX (option->constraint.word_list[i+1]);
+ if (fabs (x - v) < distance) {
+ distance = fabs (x - v);
+ nearest = x;
+ }
+ }
+ v = nearest;
+ }
+
+ v_fixed = SANE_FIX (v);
+ control_option (handle, option, option_index, SANE_ACTION_SET_VALUE, &v_fixed);
+ if (result)
+ *result = SANE_UNFIX (v_fixed);
+}
+
+
+static gboolean
+set_string_option (SANE_Handle handle, const SANE_Option_Descriptor *option, SANE_Int option_index, const char *value, char **result)
+{
+ char *string;
+ gsize value_size, size;
+ gboolean error;
+
+ g_return_val_if_fail (option->type == SANE_TYPE_STRING, FALSE);
+
+ value_size = strlen (value) + 1;
+ size = option->size > value_size ? option->size : value_size;
+ string = g_malloc(sizeof(char) * size);
+ strcpy (string, value);
+ error = control_option (handle, option, option_index, SANE_ACTION_SET_VALUE, string);
+ if (result)
+ *result = string;
+ else
+ g_free (string);
+
+ return error;
+}
+
+
+static gboolean
+set_constrained_string_option (SANE_Handle handle, const SANE_Option_Descriptor *option, SANE_Int option_index, const char *values[], char **result)
+{
+ gint i, j;
+
+ g_return_val_if_fail (option->type == SANE_TYPE_STRING, FALSE);
+ g_return_val_if_fail (option->constraint_type == SANE_CONSTRAINT_STRING_LIST, FALSE);
+
+ for (i = 0; values[i] != NULL; i++) {
+ for (j = 0; option->constraint.string_list[j]; j++) {
+ if (strcmp (values[i], option->constraint.string_list[j]) == 0)
+ break;
+ }
+
+ if (option->constraint.string_list[j] != NULL)
+ return set_string_option (handle, option, option_index, values[i], result);
+ }
+
+ return FALSE;
+}
+
+
+static void
+log_option (SANE_Int index, const SANE_Option_Descriptor *option)
+{
+ GString *string;
+ SANE_Word i;
+ SANE_Int cap;
+
+ string = g_string_new ("");
+
+ g_string_append_printf (string, "Option %d:", index);
+
+ if (option->name)
+ g_string_append_printf (string, " name='%s'", option->name);
+
+ if (option->title)
+ g_string_append_printf (string, " title='%s'", option->title);
+
+ switch (option->type) {
+ case SANE_TYPE_BOOL:
+ g_string_append (string, " type=bool");
+ break;
+ case SANE_TYPE_INT:
+ g_string_append (string, " type=int");
+ break;
+ case SANE_TYPE_FIXED:
+ g_string_append (string, " type=fixed");
+ break;
+ case SANE_TYPE_STRING:
+ g_string_append (string, " type=string");
+ break;
+ case SANE_TYPE_BUTTON:
+ g_string_append (string, " type=button");
+ break;
+ case SANE_TYPE_GROUP:
+ g_string_append (string, " type=group");
+ break;
+ default:
+ g_string_append_printf (string, " type=%d", option->type);
+ break;
+ }
+
+ g_string_append_printf (string, " size=%d", option->size);
+
+ switch (option->unit) {
+ case SANE_UNIT_NONE:
+ break;
+ case SANE_UNIT_PIXEL:
+ g_string_append (string, " unit=pixels");
+ break;
+ case SANE_UNIT_BIT:
+ g_string_append (string, " unit=bits");
+ break;
+ case SANE_UNIT_MM:
+ g_string_append (string, " unit=mm");
+ break;
+ case SANE_UNIT_DPI:
+ g_string_append (string, " unit=dpi");
+ break;
+ case SANE_UNIT_PERCENT:
+ g_string_append (string, " unit=percent");
+ break;
+ case SANE_UNIT_MICROSECOND:
+ g_string_append (string, " unit=microseconds");
+ break;
+ default:
+ g_string_append_printf (string, " unit=%d", option->unit);
+ break;
+ }
+
+ switch (option->constraint_type) {
+ case SANE_CONSTRAINT_RANGE:
+ if (option->type == SANE_TYPE_FIXED)
+ g_string_append_printf (string, " min=%f, max=%f, quant=%d",
+ SANE_UNFIX (option->constraint.range->min), SANE_UNFIX (option->constraint.range->max),
+ option->constraint.range->quant);
+ else
+ g_string_append_printf (string, " min=%d, max=%d, quant=%d",
+ option->constraint.range->min, option->constraint.range->max,
+ option->constraint.range->quant);
+ break;
+ case SANE_CONSTRAINT_WORD_LIST:
+ g_string_append (string, " values=[");
+ for (i = 0; i < option->constraint.word_list[0]; i++) {
+ if (i != 0)
+ g_string_append (string, ", ");
+ if (option->type == SANE_TYPE_INT)
+ g_string_append_printf (string, "%d", option->constraint.word_list[i+1]);
+ else
+ g_string_append_printf (string, "%f", SANE_UNFIX (option->constraint.word_list[i+1]));
+ }
+ g_string_append (string, "]");
+ break;
+ case SANE_CONSTRAINT_STRING_LIST:
+ g_string_append (string, " values=[");
+ for (i = 0; option->constraint.string_list[i]; i++) {
+ if (i != 0)
+ g_string_append (string, ", ");
+ g_string_append_printf (string, "\"%s\"", option->constraint.string_list[i]);
+ }
+ g_string_append (string, "]");
+ break;
+ default:
+ break;
+ }
+
+ cap = option->cap;
+ if (cap) {
+ struct {
+ SANE_Int cap;
+ const char *name;
+ } caps[] = {
+ { SANE_CAP_SOFT_SELECT, "soft-select"},
+ { SANE_CAP_HARD_SELECT, "hard-select"},
+ { SANE_CAP_SOFT_DETECT, "soft-detect"},
+ { SANE_CAP_EMULATED, "emulated"},
+ { SANE_CAP_AUTOMATIC, "automatic"},
+ { SANE_CAP_INACTIVE, "inactive"},
+ { SANE_CAP_ADVANCED, "advanced"},
+ { 0, NULL}
+ };
+ int i, n = 0;
+
+ g_string_append (string, " cap=");
+ for (i = 0; caps[i].cap > 0; i++) {
+ if (cap & caps[i].cap) {
+ cap &= ~caps[i].cap;
+ if (n != 0)
+ g_string_append (string, ",");
+ g_string_append (string, caps[i].name);
+ n++;
+ }
+ }
+ /* Unknown capabilities */
+ if (cap) {
+ if (n != 0)
+ g_string_append (string, ",");
+ g_string_append_printf (string, "%x", cap);
+ }
+ }
+
+ g_debug ("%s", string->str);
+ g_string_free (string, TRUE);
+
+ if (option->desc)
+ g_debug (" Description: %s", option->desc);
+}
+
+
+static void
+authorization_cb (SANE_String_Const resource, SANE_Char username[SANE_MAX_USERNAME_LEN], SANE_Char password[SANE_MAX_PASSWORD_LEN])
+{
+ Scanner *scanner;
+ Credentials *credentials;
+
+ scanner = g_hash_table_lookup (scanners, g_thread_self ());
+
+ emit_signal (scanner, AUTHORIZE, g_strdup (resource));
+
+ credentials = g_async_queue_pop (scanner->priv->authorize_queue);
+ strncpy (username, credentials->username, SANE_MAX_USERNAME_LEN);
+ strncpy (password, credentials->password, SANE_MAX_PASSWORD_LEN);
+ g_free (credentials);
+}
+
+
+void
+scanner_authorize (Scanner *scanner, const gchar *username, const gchar *password)
+{
+ Credentials *credentials;
+
+ credentials = g_malloc (sizeof (Credentials));
+ credentials->username = g_strdup (username);
+ credentials->password = g_strdup (password);
+ g_async_queue_push (scanner->priv->authorize_queue, credentials);
+}
+
+
+static void
+close_device (Scanner *scanner)
+{
+ GList *iter;
+
+ if (scanner->priv->handle) {
+ sane_cancel (scanner->priv->handle);
+ g_debug ("sane_cancel ()");
+
+ sane_close (scanner->priv->handle);
+ g_debug ("sane_close ()");
+ scanner->priv->handle = NULL;
+ }
+
+ g_free (scanner->priv->buffer);
+ scanner->priv->buffer = NULL;
+
+ for (iter = scanner->priv->job_queue; iter; iter = iter->next) {
+ ScanJob *job = (ScanJob *) iter->data;
+ g_free (job->device);
+ g_free (job);
+ }
+ g_list_free (scanner->priv->job_queue);
+ scanner->priv->job_queue = NULL;
+
+ set_scanning (scanner, FALSE);
+}
+
+static void
+fail_scan (Scanner *scanner, gint error_code, const gchar *error_string)
+{
+ close_device (scanner);
+ scanner->priv->state = STATE_IDLE;
+ emit_signal (scanner, SCAN_FAILED, g_error_new (SCANNER_TYPE, error_code, "%s", error_string));
+}
+
+
+static gboolean
+handle_requests (Scanner *scanner)
+{
+ gint request_count = 0;
+
+ /* Redetect when idle */
+ if (scanner->priv->state == STATE_IDLE && scanner->priv->redetect)
+ scanner->priv->state = STATE_REDETECT;
+
+ /* Process all requests */
+ while (TRUE) {
+ ScanRequest *request = NULL;
+
+ if ((scanner->priv->state == STATE_IDLE && request_count == 0) ||
+ g_async_queue_length (scanner->priv->scan_queue) > 0)
+ request = g_async_queue_pop (scanner->priv->scan_queue);
+ else
+ return TRUE;
+
+ g_debug ("Processing request");
+ request_count++;
+
+ switch (request->type) {
+ case REQUEST_REDETECT:
+ break;
+
+ case REQUEST_START_SCAN:
+ scanner->priv->job_queue = g_list_append (scanner->priv->job_queue, request->job);
+ break;
+
+ case REQUEST_CANCEL:
+ fail_scan (scanner, SANE_STATUS_CANCELLED, "Scan cancelled - do not report this error");
+ break;
+
+ case REQUEST_QUIT:
+ close_device (scanner);
+ g_free (request);
+ return FALSE;
+ }
+
+ g_free (request);
+ }
+
+ return TRUE;
+}
+
+
+static void
+do_open (Scanner *scanner)
+{
+ SANE_Status status;
+ ScanJob *job;
+
+ job = (ScanJob *) scanner->priv->job_queue->data;
+
+ scanner->priv->line_count = 0;
+ scanner->priv->pass_number = 0;
+ scanner->priv->page_number = 0;
+ scanner->priv->notified_page = -1;
+ scanner->priv->option_index = 0;
+
+ if (!job->device && scanner->priv->default_device)
+ job->device = g_strdup (scanner->priv->default_device);
+
+ if (!job->device) {
+ g_warning ("No scan device available");
+ fail_scan (scanner, status,
+ /* Error displayed when no scanners to scan with */
+ _("No scanners available. Please connect a scanner."));
+ return;
+ }
+
+ /* See if we can use the already open device */
+ if (scanner->priv->handle) {
+ if (strcmp (scanner->priv->current_device, job->device) == 0) {
+ scanner->priv->state = STATE_GET_OPTION;
+ return;
+ }
+
+ sane_close (scanner->priv->handle);
+ g_debug ("sane_close ()");
+ scanner->priv->handle = NULL;
+ }
+
+ g_free (scanner->priv->current_device);
+ scanner->priv->current_device = NULL;
+
+ status = sane_open (job->device, &scanner->priv->handle);
+ g_debug ("sane_open (\"%s\") -> %s", job->device, get_status_string (status));
+
+ if (status != SANE_STATUS_GOOD) {
+ g_warning ("Unable to get open device: %s", sane_strstatus (status));
+ scanner->priv->handle = NULL;
+ fail_scan (scanner, status,
+ /* Error displayed when cannot connect to scanner */
+ _("Unable to connect to scanner"));
+ return;
+ }
+
+ scanner->priv->current_device = g_strdup (job->device);
+ scanner->priv->state = STATE_GET_OPTION;
+}
+
+
+static void
+do_get_option (Scanner *scanner)
+{
+ const SANE_Option_Descriptor *option;
+ SANE_Int option_index;
+ ScanJob *job;
+
+ job = (ScanJob *) scanner->priv->job_queue->data;
+
+ option = sane_get_option_descriptor (scanner->priv->handle, scanner->priv->option_index);
+ g_debug ("sane_get_option_descriptor (%d)", scanner->priv->option_index);
+ option_index = scanner->priv->option_index;
+ scanner->priv->option_index++;
+
+ if (!option) {
+ scanner->priv->state = STATE_START;
+ return;
+ }
+
+ log_option (option_index, option);
+
+ /* Ignore groups */
+ if (option->type == SANE_TYPE_GROUP)
+ return;
+
+ /* Option disabled */
+ if (option->cap & SANE_CAP_INACTIVE)
+ return;
+
+ /* Some options are unnammed (e.g. Option 0) */
+ if (option->name == NULL)
+ return;
+
+ if (strcmp (option->name, SANE_NAME_SCAN_RESOLUTION) == 0) {
+ if (option->type == SANE_TYPE_FIXED) {
+ set_fixed_option (scanner->priv->handle, option, option_index, job->dpi, &job->dpi);
+ }
+ else {
+ SANE_Int dpi;
+ set_int_option (scanner->priv->handle, option, option_index, job->dpi, &dpi);
+ job->dpi = dpi;
+ }
+ }
+ else if (strcmp (option->name, SANE_NAME_SCAN_SOURCE) == 0) {
+ const char *flatbed_sources[] =
+ {
+ "Flatbed",
+ SANE_I18N ("Flatbed"),
+ "FlatBed",
+ "Normal",
+ SANE_I18N ("Normal"),
+ NULL
+ };
+
+ const char *adf_sources[] =
+ {
+ "Automatic Document Feeder",
+ SANE_I18N ("Automatic Document Feeder"),
+ "ADF",
+ "Automatic Document Feeder(left aligned)", /* Seen in the proprietary brother3 driver */
+ "Automatic Document Feeder(centrally aligned)", /* Seen in the proprietary brother3 driver */
+ NULL
+ };
+
+ const char *adf_front_sources[] =
+ {
+ "ADF Front",
+ SANE_I18N ("ADF Front"),
+ NULL
+ };
+
+ const char *adf_back_sources[] =
+ {
+ "ADF Back",
+ SANE_I18N ("ADF Back"),
+ NULL
+ };
+
+ const char *adf_duplex_sources[] =
+ {
+ "ADF Duplex",
+ SANE_I18N ("ADF Duplex"),
+ NULL
+ };
+
+ switch (job->type) {
+ case SCAN_SINGLE:
+ if (!set_default_option (scanner->priv->handle, option, option_index))
+ if (!set_constrained_string_option (scanner->priv->handle, option, option_index, flatbed_sources, NULL))
+ g_warning ("Unable to set single page source, please file a bug");
+ break;
+ case SCAN_ADF_FRONT:
+ if (!set_constrained_string_option (scanner->priv->handle, option, option_index, adf_front_sources, NULL))
+ if (!!set_constrained_string_option (scanner->priv->handle, option, option_index, adf_sources, NULL))
+ g_warning ("Unable to set front ADF source, please file a bug");
+ break;
+ case SCAN_ADF_BACK:
+ if (!set_constrained_string_option (scanner->priv->handle, option, option_index, adf_back_sources, NULL))
+ if (!set_constrained_string_option (scanner->priv->handle, option, option_index, adf_sources, NULL))
+ g_warning ("Unable to set back ADF source, please file a bug");
+ break;
+ case SCAN_ADF_BOTH:
+ if (!set_constrained_string_option (scanner->priv->handle, option, option_index, adf_duplex_sources, NULL))
+ if (!set_constrained_string_option (scanner->priv->handle, option, option_index, adf_sources, NULL))
+ g_warning ("Unable to set duplex ADF source, please file a bug");
+ break;
+ }
+ }
+ else if (strcmp (option->name, SANE_NAME_BIT_DEPTH) == 0) {
+ if (job->depth > 0)
+ set_int_option (scanner->priv->handle, option, option_index, job->depth, NULL);
+ }
+ else if (strcmp (option->name, SANE_NAME_SCAN_MODE) == 0) {
+ /* The names of scan modes often used in drivers, as taken from the sane-backends source */
+ const char *color_scan_modes[] =
+ {
+ SANE_VALUE_SCAN_MODE_COLOR,
+ "Color",
+ "24bit Color", /* Seen in the proprietary brother3 driver */
+ NULL
+ };
+ const char *gray_scan_modes[] =
+ {
+ SANE_VALUE_SCAN_MODE_GRAY,
+ "Gray",
+ "Grayscale",
+ SANE_I18N ("Grayscale"),
+ NULL
+ };
+ const char *lineart_scan_modes[] =
+ {
+ SANE_VALUE_SCAN_MODE_LINEART,
+ "Lineart",
+ "LineArt",
+ SANE_I18N ("LineArt"),
+ "Black & White",
+ SANE_I18N ("Black & White"),
+ "Binary",
+ SANE_I18N ("Binary"),
+ "Thresholded",
+ SANE_VALUE_SCAN_MODE_GRAY,
+ "Gray",
+ "Grayscale",
+ SANE_I18N ("Grayscale"),
+ NULL
+ };
+
+ switch (job->scan_mode) {
+ case SCAN_MODE_COLOR:
+ if (!set_constrained_string_option (scanner->priv->handle, option, option_index, color_scan_modes, NULL))
+ g_warning ("Unable to set Color mode, please file a bug");
+ break;
+ case SCAN_MODE_GRAY:
+ if (!set_constrained_string_option (scanner->priv->handle, option, option_index, gray_scan_modes, NULL))
+ g_warning ("Unable to set Gray mode, please file a bug");
+ break;
+ case SCAN_MODE_LINEART:
+ if (!set_constrained_string_option (scanner->priv->handle, option, option_index, lineart_scan_modes, NULL))
+ g_warning ("Unable to set Lineart mode, please file a bug");
+ break;
+ default:
+ break;
+ }
+ }
+ /* Disable compression, we will compress after scanning */
+ else if (strcmp (option->name, "compression") == 0) {
+ const char *disable_compression_names[] =
+ {
+ SANE_I18N ("None"),
+ SANE_I18N ("none"),
+ "None",
+ "none",
+ NULL
+ };
+
+ if (!set_constrained_string_option (scanner->priv->handle, option, option_index, disable_compression_names, NULL))
+ g_warning ("Unable to disable compression, please file a bug");
+ }
+ /* Always use maximum scan area - some scanners default to using partial areas. This should be patched in sane-backends */
+ else if (strcmp (option->name, SANE_NAME_SCAN_BR_X) == 0 ||
+ strcmp (option->name, SANE_NAME_SCAN_BR_Y) == 0) {
+ switch (option->constraint_type)
+ {
+ case SANE_CONSTRAINT_RANGE:
+ if (option->type == SANE_TYPE_FIXED)
+ set_fixed_option (scanner->priv->handle, option, option_index, SANE_UNFIX (option->constraint.range->max), NULL);
+ else
+ set_int_option (scanner->priv->handle, option, option_index, option->constraint.range->max, NULL);
+ break;
+ default:
+ break;
+ }
+ }
+ else if (strcmp (option->name, SANE_NAME_PAGE_WIDTH) == 0) {
+ if (job->page_width > 0.0) {
+ if (option->type == SANE_TYPE_FIXED)
+ set_fixed_option (scanner->priv->handle, option, option_index, job->page_width / 10.0, NULL);
+ else
+ set_int_option (scanner->priv->handle, option, option_index, job->page_width / 10, NULL);
+ }
+ }
+ else if (strcmp (option->name, SANE_NAME_PAGE_HEIGHT) == 0) {
+ if (job->page_height > 0.0) {
+ if (option->type == SANE_TYPE_FIXED)
+ set_fixed_option (scanner->priv->handle, option, option_index, job->page_height / 10.0, NULL);
+ else
+ set_int_option (scanner->priv->handle, option, option_index, job->page_height / 10, NULL);
+ }
+ }
+
+ /* Test scanner options (hoping will not effect other scanners...) */
+ if (strcmp (scanner->priv->current_device, "test") == 0) {
+ if (strcmp (option->name, "hand-scanner") == 0) {
+ set_bool_option (scanner->priv->handle, option, option_index, FALSE, NULL);
+ }
+ else if (strcmp (option->name, "three-pass") == 0) {
+ set_bool_option (scanner->priv->handle, option, option_index, FALSE, NULL);
+ }
+ else if (strcmp (option->name, "test-picture") == 0) {
+ set_string_option (scanner->priv->handle, option, option_index, "Color pattern", NULL);
+ }
+ else if (strcmp (option->name, "read-delay") == 0) {
+ set_bool_option (scanner->priv->handle, option, option_index, TRUE, NULL);
+ }
+ else if (strcmp (option->name, "read-delay-duration") == 0) {
+ set_int_option (scanner->priv->handle, option, option_index, 200000, NULL);
+ }
+ }
+}
+
+
+static void
+do_complete_document (Scanner *scanner)
+{
+ ScanJob *job;
+
+ job = (ScanJob *) scanner->priv->job_queue->data;
+ g_free (job->device);
+ g_free (job);
+ scanner->priv->job_queue = g_list_remove_link (scanner->priv->job_queue, scanner->priv->job_queue);
+
+ scanner->priv->state = STATE_IDLE;
+
+ /* Continue onto the next job */
+ if (scanner->priv->job_queue) {
+ scanner->priv->state = STATE_OPEN;
+ return;
+ }
+
+ /* Trigger timeout to close */
+ // TODO
+
+ emit_signal (scanner, DOCUMENT_DONE, NULL);
+ set_scanning (scanner, FALSE);
+}
+
+
+static void
+do_start (Scanner *scanner)
+{
+ SANE_Status status;
+
+ emit_signal (scanner, EXPECT_PAGE, NULL);
+
+ status = sane_start (scanner->priv->handle);
+ g_debug ("sane_start (page=%d, pass=%d) -> %s", scanner->priv->page_number, scanner->priv->pass_number, get_status_string (status));
+ if (status == SANE_STATUS_GOOD) {
+ scanner->priv->state = STATE_GET_PARAMETERS;
+ }
+ else if (status == SANE_STATUS_NO_DOCS) {
+ do_complete_document (scanner);
+ }
+ else {
+ g_warning ("Unable to start device: %s", sane_strstatus (status));
+ fail_scan (scanner, status,
+ /* Error display when unable to start scan */
+ _("Unable to start scan"));
+ }
+}
+
+
+static void
+do_get_parameters (Scanner *scanner)
+{
+ SANE_Status status;
+ ScanPageInfo *info;
+ ScanJob *job;
+
+ status = sane_get_parameters (scanner->priv->handle, &scanner->priv->parameters);
+ g_debug ("sane_get_parameters () -> %s", get_status_string (status));
+ if (status != SANE_STATUS_GOOD) {
+ g_warning ("Unable to get device parameters: %s", sane_strstatus (status));
+ fail_scan (scanner, status,
+ /* Error displayed when communication with scanner broken */
+ _("Error communicating with scanner"));
+ return;
+ }
+
+ job = (ScanJob *) scanner->priv->job_queue->data;
+
+ g_debug ("Parameters: format=%s last_frame=%s bytes_per_line=%d pixels_per_line=%d lines=%d depth=%d",
+ get_frame_string (scanner->priv->parameters.format),
+ scanner->priv->parameters.last_frame ? "SANE_TRUE" : "SANE_FALSE",
+ scanner->priv->parameters.bytes_per_line,
+ scanner->priv->parameters.pixels_per_line,
+ scanner->priv->parameters.lines,
+ scanner->priv->parameters.depth);
+
+ info = g_malloc(sizeof(ScanPageInfo));
+ info->width = scanner->priv->parameters.pixels_per_line;
+ info->height = scanner->priv->parameters.lines;
+ info->depth = scanner->priv->parameters.depth;
+ info->dpi = job->dpi; // FIXME: This is the requested DPI, not the actual DPI
+ info->device = g_strdup (scanner->priv->current_device);
+
+ if (scanner->priv->page_number != scanner->priv->notified_page) {
+ emit_signal (scanner, GOT_PAGE_INFO, info);
+ scanner->priv->notified_page = scanner->priv->page_number;
+ }
+
+ /* Prepare for read */
+ scanner->priv->buffer_size = scanner->priv->parameters.bytes_per_line + 1; /* Use +1 so buffer is not resized if driver returns one line per read */
+ scanner->priv->buffer = g_malloc (sizeof(SANE_Byte) * scanner->priv->buffer_size);
+ scanner->priv->n_used = 0;
+ scanner->priv->line_count = 0;
+ scanner->priv->pass_number = 0;
+ scanner->priv->state = STATE_READ;
+}
+
+
+static void
+do_complete_page (Scanner *scanner)
+{
+ ScanJob *job;
+
+ emit_signal (scanner, PAGE_DONE, NULL);
+
+ job = (ScanJob *) scanner->priv->job_queue->data;
+
+ /* If multi-pass then scan another page */
+ if (!scanner->priv->parameters.last_frame) {
+ scanner->priv->pass_number++;
+ scanner->priv->state = STATE_START;
+ return;
+ }
+
+ /* Go back for another page */
+ if (job->type != SCAN_SINGLE) {
+ scanner->priv->page_number++;
+ scanner->priv->pass_number = 0;
+ emit_signal (scanner, PAGE_DONE, NULL);
+ scanner->priv->state = STATE_START;
+ return;
+ }
+
+ sane_cancel (scanner->priv->handle);
+ g_debug ("sane_cancel ()");
+
+ do_complete_document (scanner);
+}
+
+
+static void
+do_read (Scanner *scanner)
+{
+ SANE_Status status;
+ SANE_Int n_to_read, n_read;
+ gboolean full_read = FALSE;
+
+ /* Read as many bytes as we expect */
+ n_to_read = scanner->priv->buffer_size - scanner->priv->n_used;
+
+ status = sane_read (scanner->priv->handle,
+ scanner->priv->buffer + scanner->priv->n_used,
+ n_to_read, &n_read);
+ g_debug ("sane_read (%d) -> (%s, %d)", n_to_read, get_status_string (status), n_read);
+
+ /* End of variable length frame */
+ if (status == SANE_STATUS_EOF &&
+ scanner->priv->parameters.lines == -1 &&
+ scanner->priv->bytes_remaining == scanner->priv->parameters.bytes_per_line) {
+ do_complete_page (scanner);
+ return;
+ }
+
+ /* Communication error */
+ if (status != SANE_STATUS_GOOD) {
+ g_warning ("Unable to read frame from device: %s", sane_strstatus (status));
+ fail_scan (scanner, status,
+ /* Error displayed when communication with scanner broken */
+ _("Error communicating with scanner"));
+ return;
+ }
+
+ if (scanner->priv->n_used == 0 && n_read == scanner->priv->buffer_size)
+ full_read = TRUE;
+ scanner->priv->n_used += n_read;
+
+ /* Feed out lines */
+ if (scanner->priv->n_used >= scanner->priv->parameters.bytes_per_line) {
+ ScanLine *line;
+
+ line = g_malloc(sizeof(ScanLine));
+ switch (scanner->priv->parameters.format) {
+ case SANE_FRAME_GRAY:
+ line->format = LINE_GRAY;
+ break;
+ case SANE_FRAME_RGB:
+ line->format = LINE_RGB;
+ break;
+ case SANE_FRAME_RED:
+ line->format = LINE_RED;
+ break;
+ case SANE_FRAME_GREEN:
+ line->format = LINE_GREEN;
+ break;
+ case SANE_FRAME_BLUE:
+ line->format = LINE_BLUE;
+ break;
+ }
+ line->width = scanner->priv->parameters.pixels_per_line;
+ line->depth = scanner->priv->parameters.depth;
+ line->data = scanner->priv->buffer;
+ line->data_length = scanner->priv->parameters.bytes_per_line;
+ line->number = scanner->priv->line_count;
+ line->n_lines = scanner->priv->n_used / line->data_length;
+
+ scanner->priv->buffer = NULL;
+ scanner->priv->line_count += line->n_lines;
+
+ /* On last line */
+ if (scanner->priv->parameters.lines > 0 && scanner->priv->line_count >= scanner->priv->parameters.lines) {
+ emit_signal (scanner, GOT_LINE, line);
+ do_complete_page (scanner);
+ }
+ else {
+ int i, n_remaining;
+
+ /* Increase buffer size if did full read */
+ if (full_read)
+ scanner->priv->buffer_size += scanner->priv->parameters.bytes_per_line;
+
+ scanner->priv->buffer = g_malloc(sizeof(SANE_Byte) * scanner->priv->buffer_size);
+ n_remaining = scanner->priv->n_used - (line->n_lines * line->data_length);
+ scanner->priv->n_used = 0;
+ for (i = 0; i < n_remaining; i++) {
+ scanner->priv->buffer[i] = line->data[i + (line->n_lines * line->data_length)];
+ scanner->priv->n_used++;
+ }
+ emit_signal (scanner, GOT_LINE, line);
+ }
+ }
+}
+
+
+static gpointer
+scan_thread (Scanner *scanner)
+{
+ SANE_Status status;
+ SANE_Int version_code;
+
+ g_hash_table_insert (scanners, g_thread_self (), scanner);
+
+ scanner->priv->state = STATE_IDLE;
+
+ status = sane_init (&version_code, authorization_cb);
+ g_debug ("sane_init () -> %s", get_status_string (status));
+ if (status != SANE_STATUS_GOOD) {
+ g_warning ("Unable to initialize SANE backend: %s", sane_strstatus(status));
+ return FALSE;
+ }
+ g_debug ("SANE version %d.%d.%d",
+ SANE_VERSION_MAJOR(version_code),
+ SANE_VERSION_MINOR(version_code),
+ SANE_VERSION_BUILD(version_code));
+
+ /* Scan for devices on first start */
+ scanner_redetect (scanner);
+
+ while (handle_requests (scanner)) {
+ switch (scanner->priv->state) {
+ case STATE_IDLE:
+ if (scanner->priv->job_queue) {
+ set_scanning (scanner, TRUE);
+ scanner->priv->state = STATE_OPEN;
+ }
+ break;
+ case STATE_REDETECT:
+ do_redetect (scanner);
+ break;
+ case STATE_OPEN:
+ do_open (scanner);
+ break;
+ case STATE_GET_OPTION:
+ do_get_option (scanner);
+ break;
+ case STATE_START:
+ do_start (scanner);
+ break;
+ case STATE_GET_PARAMETERS:
+ do_get_parameters (scanner);
+ break;
+ case STATE_READ:
+ do_read (scanner);
+ break;
+ }
+ }
+
+ return NULL;
+}
+
+
+Scanner *
+scanner_new ()
+{
+ return g_object_new (SCANNER_TYPE, NULL);
+}
+
+
+void
+scanner_start (Scanner *scanner)
+{
+ GError *error = NULL;
+ scanner->priv->thread = g_thread_create ((GThreadFunc) scan_thread, scanner, TRUE, &error);
+ if (error) {
+ g_critical ("Unable to create thread: %s", error->message);
+ g_error_free (error);
+ }
+}
+
+
+void
+scanner_redetect (Scanner *scanner)
+{
+ ScanRequest *request;
+
+ if (scanner->priv->redetect)
+ return;
+ scanner->priv->redetect = TRUE;
+
+ g_debug ("Requesting redetection of scan devices");
+
+ request = g_malloc0 (sizeof (ScanRequest));
+ request->type = REQUEST_REDETECT;
+ g_async_queue_push (scanner->priv->scan_queue, request);
+}
+
+
+gboolean
+scanner_is_scanning (Scanner *scanner)
+{
+ return scanner->priv->scanning;
+}
+
+
+void
+scanner_scan (Scanner *scanner, const char *device, ScanOptions *options)
+{
+ ScanRequest *request;
+ const gchar *type_string;
+
+ switch (options->type) {
+ case SCAN_SINGLE:
+ type_string = "SCAN_SINGLE";
+ break;
+ case SCAN_ADF_FRONT:
+ type_string = "SCAN_ADF_FRONT";
+ break;
+ case SCAN_ADF_BACK:
+ type_string = "SCAN_ADF_BACK";
+ break;
+ case SCAN_ADF_BOTH:
+ type_string = "SCAN_ADF_BOTH";
+ break;
+ default:
+ g_assert (FALSE);
+ return;
+ }
+
+ g_debug ("scanner_scan (\"%s\", %d, %s)", device ? device : "(null)", options->dpi, type_string);
+ request = g_malloc0 (sizeof (ScanRequest));
+ request->type = REQUEST_START_SCAN;
+ request->job = g_malloc0 (sizeof (ScanJob));
+ request->job->device = g_strdup (device);
+ request->job->dpi = options->dpi;
+ request->job->scan_mode = options->scan_mode;
+ request->job->depth = options->depth;
+ request->job->type = options->type;
+ request->job->page_width = options->paper_width;
+ request->job->page_height = options->paper_height;
+ g_async_queue_push (scanner->priv->scan_queue, request);
+}
+
+
+void
+scanner_cancel (Scanner *scanner)
+{
+ ScanRequest *request;
+
+ request = g_malloc0 (sizeof (ScanRequest));
+ request->type = REQUEST_CANCEL;
+ g_async_queue_push (scanner->priv->scan_queue, request);
+}
+
+
+void scanner_free (Scanner *scanner)
+{
+ ScanRequest *request;
+
+ g_debug ("Stopping scan thread");
+
+ request = g_malloc0 (sizeof (ScanRequest));
+ request->type = REQUEST_QUIT;
+ g_async_queue_push (scanner->priv->scan_queue, request);
+
+ if (scanner->priv->thread)
+ g_thread_join (scanner->priv->thread);
+
+ g_async_queue_unref (scanner->priv->scan_queue);
+ g_object_unref (scanner);
+
+ sane_exit ();
+ g_debug ("sane_exit ()");
+}
+
+
+static void
+scanner_class_init (ScannerClass *klass)
+{
+ signals[AUTHORIZE] =
+ g_signal_new ("authorize",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (ScannerClass, authorize),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__STRING,
+ G_TYPE_NONE, 1, G_TYPE_STRING);
+ signals[UPDATE_DEVICES] =
+ g_signal_new ("update-devices",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (ScannerClass, update_devices),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__POINTER,
+ G_TYPE_NONE, 1, G_TYPE_POINTER);
+ signals[EXPECT_PAGE] =
+ g_signal_new ("expect-page",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (ScannerClass, expect_page),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE, 0);
+ signals[GOT_PAGE_INFO] =
+ g_signal_new ("got-page-info",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (ScannerClass, got_page_info),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__POINTER,
+ G_TYPE_NONE, 1, G_TYPE_POINTER);
+ signals[GOT_LINE] =
+ g_signal_new ("got-line",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (ScannerClass, got_line),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__POINTER,
+ G_TYPE_NONE, 1, G_TYPE_POINTER);
+ signals[SCAN_FAILED] =
+ g_signal_new ("scan-failed",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (ScannerClass, scan_failed),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__POINTER,
+ G_TYPE_NONE, 1, G_TYPE_POINTER);
+ signals[PAGE_DONE] =
+ g_signal_new ("page-done",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (ScannerClass, page_done),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE, 0);
+ signals[DOCUMENT_DONE] =
+ g_signal_new ("document-done",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (ScannerClass, document_done),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE, 0);
+ signals[SCANNING_CHANGED] =
+ g_signal_new ("scanning-changed",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (ScannerClass, scanning_changed),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE, 0);
+
+ g_type_class_add_private (klass, sizeof (ScannerPrivate));
+
+ scanners = g_hash_table_new (g_direct_hash, g_direct_equal);
+}
+
+
+static void
+scanner_init (Scanner *scanner)
+{
+ scanner->priv = G_TYPE_INSTANCE_GET_PRIVATE (scanner, SCANNER_TYPE, ScannerPrivate);
+ scanner->priv->scan_queue = g_async_queue_new ();
+ scanner->priv->authorize_queue = g_async_queue_new ();
+}
diff --git a/src/scanner.h b/src/scanner.h
new file mode 100644
index 0000000..5acdc84
--- /dev/null
+++ b/src/scanner.h
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2009 Canonical Ltd.
+ * Author: Robert Ancell <robert.ancell@canonical.com>
+ *
+ * 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. See http://www.gnu.org/copyleft/gpl.html the full text of the
+ * license.
+ */
+
+#ifndef _SCANNER_H_
+#define _SCANNER_H_
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define SCANNER_TYPE (scanner_get_type ())
+#define SCANNER(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), SCANNER_TYPE, Scanner))
+
+
+typedef struct
+{
+ gchar *name, *label;
+} ScanDevice;
+
+typedef struct
+{
+ /* Width, height in pixels */
+ gint width, height;
+
+ /* Bit depth */
+ gint depth;
+
+ /* Resolution */
+ gdouble dpi;
+
+ /* The device this page came from */
+ gchar *device;
+} ScanPageInfo;
+
+typedef struct
+{
+ /* Line number */
+ gint number;
+
+ /* Number of lines in this packet */
+ gint n_lines;
+
+ /* Width in pixels and format */
+ gint width, depth;
+ enum
+ {
+ LINE_GRAY,
+ LINE_RGB,
+ LINE_RED,
+ LINE_GREEN,
+ LINE_BLUE
+ } format;
+
+ /* Raw line data */
+ guchar *data;
+ gsize data_length;
+} ScanLine;
+
+typedef enum
+{
+ SCAN_MODE_DEFAULT,
+ SCAN_MODE_COLOR,
+ SCAN_MODE_GRAY,
+ SCAN_MODE_LINEART
+} ScanMode;
+
+typedef enum
+{
+ SCAN_SINGLE,
+ SCAN_ADF_FRONT,
+ SCAN_ADF_BACK,
+ SCAN_ADF_BOTH
+} ScanType;
+
+typedef struct
+{
+ gint dpi;
+ ScanMode scan_mode;
+ gint depth;
+ ScanType type;
+ gint paper_width, paper_height;
+} ScanOptions;
+
+typedef struct ScannerPrivate ScannerPrivate;
+
+typedef struct
+{
+ GObject parent_instance;
+ ScannerPrivate *priv;
+} Scanner;
+
+typedef struct
+{
+ GObjectClass parent_class;
+
+ void (*update_devices) (Scanner *scanner, GList *devices);
+ void (*authorize) (Scanner *scanner, const gchar *resource);
+ void (*expect_page) (Scanner *scanner);
+ void (*got_page_info) (Scanner *scanner, ScanPageInfo *info);
+ void (*got_line) (Scanner *scanner, ScanLine *line);
+ void (*scan_failed) (Scanner *scanner, GError *error);
+ void (*page_done) (Scanner *scanner);
+ void (*document_done) (Scanner *scanner);
+ void (*scanning_changed) (Scanner *scanner);
+} ScannerClass;
+
+
+GType scanner_get_type (void);
+
+Scanner *scanner_new (void);
+
+void scanner_start (Scanner *scanner);
+
+void scanner_authorize (Scanner *scanner, const gchar *username, const gchar *password);
+
+void scanner_redetect (Scanner *scanner);
+
+gboolean scanner_is_scanning (Scanner *scanner);
+
+void scanner_scan (Scanner *scanner, const char *device, ScanOptions *options);
+
+void scanner_cancel (Scanner *scanner);
+
+void scanner_free (Scanner *scanner);
+
+#endif /* _SCANNER_H_ */
diff --git a/src/simple-scan.c b/src/simple-scan.c
new file mode 100644
index 0000000..ab59299
--- /dev/null
+++ b/src/simple-scan.c
@@ -0,0 +1,633 @@
+/*
+ * Copyright (C) 2009 Canonical Ltd.
+ * Author: Robert Ancell <robert.ancell@canonical.com>
+ *
+ * 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. See http://www.gnu.org/copyleft/gpl.html the full text of the
+ * license.
+ */
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <glib/gi18n.h>
+#include <gtk/gtk.h>
+#include <unistd.h>
+#include <gudev/gudev.h>
+#include <dbus/dbus-glib.h>
+
+#include <sane/sane.h> // For SANE_STATUS_CANCELLED
+
+#include "ui.h"
+#include "scanner.h"
+#include "book.h"
+
+
+static const char *default_device = NULL;
+
+static GUdevClient *udev_client;
+
+static SimpleScan *ui;
+
+static Scanner *scanner;
+
+static Book *book;
+
+static GTimer *log_timer;
+
+static FILE *log_file;
+
+static gboolean debug = FALSE;
+
+
+static void
+update_scan_devices_cb (Scanner *scanner, GList *devices)
+{
+ ui_set_scan_devices (ui, devices);
+}
+
+
+static void
+authorize_cb (Scanner *scanner, const gchar *resource)
+{
+ gchar *username = NULL, *password = NULL;
+ ui_authorize (ui, resource, &username, &password);
+ scanner_authorize (scanner, username, password);
+ g_free (username);
+ g_free (password);
+}
+
+
+static Page *
+append_page ()
+{
+ Page *page;
+ Orientation orientation = TOP_TO_BOTTOM;
+ gboolean do_crop = FALSE;
+ gchar *named_crop = NULL;
+ gint width = 100, height = 100, dpi = 100, cx, cy, cw, ch;
+
+ /* Use current page if not used */
+ page = book_get_page (book, -1);
+ if (page && !page_has_data (page)) {
+ ui_set_selected_page (ui, page);
+ page_start (page);
+ return page;
+ }
+
+ /* Copy info from previous page */
+ if (page) {
+ orientation = page_get_orientation (page);
+ width = page_get_width (page);
+ height = page_get_height (page);
+ dpi = page_get_dpi (page);
+
+ do_crop = page_has_crop (page);
+ if (do_crop) {
+ named_crop = page_get_named_crop (page);
+ page_get_crop (page, &cx, &cy, &cw, &ch);
+ }
+ }
+
+ page = book_append_page (book, width, height, dpi, orientation);
+ if (do_crop) {
+ if (named_crop) {
+ page_set_named_crop (page, named_crop);
+ g_free (named_crop);
+ }
+ else
+ page_set_custom_crop (page, cw, ch);
+ page_move_crop (page, cx, cy);
+ }
+ ui_set_selected_page (ui, page);
+ page_start (page);
+
+ return page;
+}
+
+
+static void
+scanner_new_page_cb (Scanner *scanner)
+{
+ append_page ();
+}
+
+
+static gchar *
+get_profile_for_device (const gchar *current_device)
+{
+ gboolean ret;
+ DBusGConnection *connection;
+ DBusGProxy *proxy;
+ GError *error = NULL;
+ GType custom_g_type_string_string;
+ GPtrArray *profile_data_array = NULL;
+ gchar *device_id = NULL;
+ gchar *icc_profile = NULL;
+
+ /* Connect to the color manager on the session bus */
+ connection = dbus_g_bus_get (DBUS_BUS_SESSION, NULL);
+ proxy = dbus_g_proxy_new_for_name (connection,
+ "org.gnome.ColorManager",
+ "/org/gnome/ColorManager",
+ "org.gnome.ColorManager");
+
+ /* Get color profile */
+ device_id = g_strdup_printf ("sane:%s", current_device);
+ custom_g_type_string_string = dbus_g_type_get_collection ("GPtrArray",
+ dbus_g_type_get_struct("GValueArray",
+ G_TYPE_STRING,
+ G_TYPE_STRING,
+ G_TYPE_INVALID));
+ ret = dbus_g_proxy_call (proxy, "GetProfilesForDevice", &error,
+ G_TYPE_STRING, device_id,
+ G_TYPE_STRING, "",
+ G_TYPE_INVALID,
+ custom_g_type_string_string, &profile_data_array,
+ G_TYPE_INVALID);
+ g_object_unref (proxy);
+ g_free (device_id);
+ if (!ret) {
+ g_debug ("The request failed: %s", error->message);
+ g_error_free (error);
+ return NULL;
+ }
+
+ if (profile_data_array->len > 0) {
+ GValueArray *gva;
+ GValue *gv = NULL;
+
+ /* Just use the preferred profile filename */
+ gva = (GValueArray *) g_ptr_array_index (profile_data_array, 0);
+ gv = g_value_array_get_nth (gva, 1);
+ icc_profile = g_value_dup_string (gv);
+ g_value_unset (gv);
+ }
+ else
+ g_debug ("There are no ICC profiles for the device sane:%s", current_device);
+ g_ptr_array_free (profile_data_array, TRUE);
+
+ return icc_profile;
+}
+
+
+static void
+scanner_page_info_cb (Scanner *scanner, ScanPageInfo *info)
+{
+ Page *page;
+
+ g_debug ("Page is %d pixels wide, %d pixels high, %d bits per pixel",
+ info->width, info->height, info->depth);
+
+ /* Add a new page */
+ page = append_page ();
+ page_set_scan_area (page, info->width, info->height, info->dpi);
+
+ /* Get ICC color profile */
+ /* FIXME: The ICC profile could change */
+ /* FIXME: Don't do a D-bus call for each page, cache color profiles */
+ page_set_color_profile (page, get_profile_for_device (info->device));
+}
+
+
+static void
+scanner_line_cb (Scanner *scanner, ScanLine *line)
+{
+ Page *page;
+
+ page = book_get_page (book, book_get_n_pages (book) - 1);
+ page_parse_scan_line (page, line);
+}
+
+
+static void
+scanner_page_done_cb (Scanner *scanner)
+{
+ Page *page;
+ page = book_get_page (book, book_get_n_pages (book) - 1);
+ page_finish (page);
+}
+
+
+static void
+remove_empty_page ()
+{
+ Page *page;
+
+ page = book_get_page (book, book_get_n_pages (book) - 1);
+
+ /* Remove a failed page */
+ if (page_has_data (page))
+ page_finish (page);
+ else
+ book_delete_page (book, page);
+}
+
+
+static void
+scanner_document_done_cb (Scanner *scanner)
+{
+ remove_empty_page ();
+}
+
+
+static void
+scanner_failed_cb (Scanner *scanner, GError *error)
+{
+ remove_empty_page ();
+ if (!g_error_matches (error, SCANNER_TYPE, SANE_STATUS_CANCELLED)) {
+ ui_show_error (ui,
+ /* Title of error dialog when scan failed */
+ _("Failed to scan"),
+ error->message,
+ TRUE);
+ }
+}
+
+
+static void
+scanner_scanning_changed_cb (Scanner *scanner)
+{
+ ui_set_scanning (ui, scanner_is_scanning (scanner));
+}
+
+
+static void
+scan_cb (SimpleScan *ui, const gchar *device, ScanOptions *options)
+{
+ /* Default filename to use when saving document (and extension will be added, e.g. .jpg) */
+ const gchar *filename_prefix = _("Scanned Document");
+ const gchar *extension;
+ gchar *filename;
+
+ if (options->scan_mode == SCAN_MODE_COLOR)
+ extension = "jpg";
+ else
+ extension = "pdf";
+
+ g_debug ("Requesting scan at %d dpi from device '%s'", options->dpi, device);
+
+ if (!scanner_is_scanning (scanner))
+ append_page ();
+
+ filename = g_strdup_printf ("%s.%s", filename_prefix, extension);
+ ui_set_default_file_name (ui, filename);
+ g_free (filename);
+ scanner_scan (scanner, device, options);
+}
+
+
+static void
+cancel_cb (SimpleScan *ui)
+{
+ scanner_cancel (scanner);
+}
+
+
+static gboolean
+save_book_by_extension (GFile *file, GError **error)
+{
+ gboolean result;
+ gchar *uri, *uri_lower;
+
+ uri = g_file_get_uri (file);
+ uri_lower = g_utf8_strdown (uri, -1);
+ if (g_str_has_suffix (uri_lower, ".pdf"))
+ result = book_save (book, "pdf", file, error);
+ else if (g_str_has_suffix (uri_lower, ".ps"))
+ result = book_save (book, "ps", file, error);
+ else if (g_str_has_suffix (uri_lower, ".png"))
+ result = book_save (book, "png", file, error);
+ else if (g_str_has_suffix (uri_lower, ".tif") || g_str_has_suffix (uri_lower, ".tiff"))
+ result = book_save (book, "tiff", file, error);
+ else
+ result = book_save (book, "jpeg", file, error);
+
+ g_free (uri);
+ g_free (uri_lower);
+
+ return result;
+}
+
+
+static void
+save_cb (SimpleScan *ui, const gchar *uri)
+{
+ GError *error = NULL;
+ GFile *file;
+
+ g_debug ("Saving to '%s'", uri);
+
+ file = g_file_new_for_uri (uri);
+ if (!save_book_by_extension (file, &error)) {
+ g_warning ("Error saving file: %s", error->message);
+ ui_show_error (ui,
+ /* Title of error dialog when save failed */
+ _("Failed to save file"),
+ error->message,
+ FALSE);
+ g_error_free (error);
+ }
+ g_object_unref (file);
+}
+
+
+static gchar *
+get_temporary_filename (const gchar *prefix, const gchar *extension)
+{
+ gint fd;
+ gchar *filename, *path;
+ GError *error = NULL;
+
+ /* NOTE: I'm not sure if this is a 100% safe strategy to use g_file_open_tmp(), close and
+ * use the filename but it appears to work in practise */
+
+ filename = g_strdup_printf ("%s-XXXXXX.%s", prefix, extension);
+ fd = g_file_open_tmp (filename, &path, &error);
+ g_free (filename);
+ if (fd < 0) {
+ g_warning ("Error saving email attachment: %s", error->message);
+ g_clear_error (&error);
+ return NULL;
+ }
+ close (fd);
+
+ return path;
+}
+
+
+static void
+email_cb (SimpleScan *ui, const gchar *profile)
+{
+ gboolean saved = FALSE;
+ GError *error = NULL;
+ GString *command_line;
+
+ command_line = g_string_new ("xdg-email");
+
+ /* Save text files as PDFs */
+ if (strcmp (profile, "text") == 0) {
+ gchar *path;
+
+ /* Open a temporary file */
+ path = get_temporary_filename ("scanned-document", "pdf");
+ if (path) {
+ GFile *file;
+
+ file = g_file_new_for_path (path);
+ saved = book_save (book, "pdf", file, &error);
+ g_string_append_printf (command_line, " --attach %s", path);
+ g_free (path);
+ g_object_unref (file);
+ }
+ }
+ else {
+ gint i;
+
+ for (i = 0; i < book_get_n_pages (book); i++) {
+ gchar *path;
+ GFile *file;
+
+ path = get_temporary_filename ("scanned-document", "jpg");
+ if (!path) {
+ saved = FALSE;
+ break;
+ }
+
+ file = g_file_new_for_path (path);
+ saved = page_save (book_get_page (book, i), "jpeg", file, &error);
+ g_string_append_printf (command_line, " --attach %s", path);
+ g_free (path);
+ g_object_unref (file);
+
+ if (!saved)
+ break;
+ }
+ }
+
+ if (saved) {
+ g_debug ("Launchind email client: %s", command_line->str);
+ g_spawn_command_line_async (command_line->str, &error);
+
+ if (error) {
+ g_warning ("Unable to start email: %s", error->message);
+ g_clear_error (&error);
+ }
+ }
+ else {
+ g_warning ("Unable to save email file: %s", error->message);
+ g_clear_error (&error);
+ }
+
+ g_string_free (command_line, TRUE);
+}
+
+
+static void
+quit_cb (SimpleScan *ui)
+{
+ g_object_unref (book);
+ g_object_unref (ui);
+ g_object_unref (udev_client);
+ scanner_free (scanner);
+ gtk_main_quit ();
+}
+
+
+static void
+version()
+{
+ /* NOTE: Is not translated so can be easily parsed */
+ fprintf(stderr, "%1$s %2$s\n", SIMPLE_SCAN_BINARY, VERSION);
+}
+
+
+static void
+usage(int show_gtk)
+{
+ fprintf(stderr,
+ /* Description on how to use simple-scan displayed on command-line */
+ _("Usage:\n"
+ " %s [DEVICE...] - Scanning utility"), SIMPLE_SCAN_BINARY);
+
+ fprintf(stderr,
+ "\n\n");
+
+ fprintf(stderr,
+ /* Description on how to use simple-scan displayed on command-line */
+ _("Help Options:\n"
+ " -d, --debug Print debugging messages\n"
+ " -v, --version Show release version\n"
+ " -h, --help Show help options\n"
+ " --help-all Show all help options\n"
+ " --help-gtk Show GTK+ options"));
+ fprintf(stderr,
+ "\n\n");
+
+ if (show_gtk) {
+ fprintf(stderr,
+ /* Description on simple-scan command-line GTK+ options displayed on command-line */
+ _("GTK+ Options:\n"
+ " --class=CLASS Program class as used by the window manager\n"
+ " --name=NAME Program name as used by the window manager\n"
+ " --screen=SCREEN X screen to use\n"
+ " --sync Make X calls synchronous\n"
+ " --gtk-module=MODULES Load additional GTK+ modules\n"
+ " --g-fatal-warnings Make all warnings fatal"));
+ fprintf(stderr,
+ "\n\n");
+ }
+}
+
+
+static void
+log_cb (const gchar *log_domain, GLogLevelFlags log_level,
+ const gchar *message, gpointer data)
+{
+ /* Log everything to a file */
+ if (log_file) {
+ const gchar *prefix;
+
+ switch (log_level & G_LOG_LEVEL_MASK) {
+ case G_LOG_LEVEL_ERROR:
+ prefix = "ERROR:";
+ break;
+ case G_LOG_LEVEL_CRITICAL:
+ prefix = "CRITICAL:";
+ break;
+ case G_LOG_LEVEL_WARNING:
+ prefix = "WARNING:";
+ break;
+ case G_LOG_LEVEL_MESSAGE:
+ prefix = "MESSAGE:";
+ break;
+ case G_LOG_LEVEL_INFO:
+ prefix = "INFO:";
+ break;
+ case G_LOG_LEVEL_DEBUG:
+ prefix = "DEBUG:";
+ break;
+ default:
+ prefix = "LOG:";
+ break;
+ }
+
+ fprintf (log_file, "[%+.2fs] %s %s\n", g_timer_elapsed (log_timer, NULL), prefix, message);
+ }
+
+ /* Only show debug if requested */
+ if (log_level & G_LOG_LEVEL_DEBUG) {
+ if (debug)
+ g_log_default_handler (log_domain, log_level, message, data);
+ }
+ else
+ g_log_default_handler (log_domain, log_level, message, data);
+}
+
+
+static void
+get_options (int argc, char **argv)
+{
+ int i;
+
+ for (i = 1; i < argc; i++) {
+ char *arg = argv[i];
+
+ if (strcmp (arg, "-d") == 0 ||
+ strcmp (arg, "--debug") == 0) {
+ debug = TRUE;
+ }
+ else if (strcmp (arg, "-v") == 0 ||
+ strcmp (arg, "--version") == 0) {
+ version ();
+ exit (0);
+ }
+ else if (strcmp (arg, "-h") == 0 ||
+ strcmp (arg, "--help") == 0) {
+ usage (FALSE);
+ exit (0);
+ }
+ else if (strcmp (arg, "--help-all") == 0 ||
+ strcmp (arg, "--help-gtk") == 0) {
+ usage (TRUE);
+ exit (0);
+ }
+ else {
+ if (default_device) {
+ fprintf (stderr, "Unknown argument: '%s'\n", arg);
+ exit (1);
+ }
+ default_device = arg;
+ }
+ }
+}
+
+
+static void
+on_uevent (GUdevClient *client, const gchar *action, GUdevDevice *device)
+{
+ scanner_redetect (scanner);
+}
+
+
+int
+main (int argc, char **argv)
+{
+ const char *udev_subsystems[] = { "usb", NULL };
+ gchar *path;
+
+ g_thread_init (NULL);
+
+ /* Log to a file */
+ log_timer = g_timer_new ();
+ path = g_build_filename (g_get_user_cache_dir (), "simple-scan", NULL);
+ g_mkdir_with_parents (path, 0700);
+ g_free (path);
+ path = g_build_filename (g_get_user_cache_dir (), "simple-scan", "simple-scan.log", NULL);
+ log_file = fopen (path, "w");
+ g_free (path);
+ g_log_set_default_handler (log_cb, NULL);
+
+ bindtextdomain (GETTEXT_PACKAGE, LOCALE_DIR);
+ bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
+ textdomain (GETTEXT_PACKAGE);
+
+ gtk_init (&argc, &argv);
+
+ get_options (argc, argv);
+
+ g_debug ("Starting Simple Scan %s, PID=%i", VERSION, getpid ());
+
+ ui = ui_new ();
+ book = ui_get_book (ui);
+ g_signal_connect (ui, "start-scan", G_CALLBACK (scan_cb), NULL);
+ g_signal_connect (ui, "stop-scan", G_CALLBACK (cancel_cb), NULL);
+ g_signal_connect (ui, "save", G_CALLBACK (save_cb), NULL);
+ g_signal_connect (ui, "email", G_CALLBACK (email_cb), NULL);
+ g_signal_connect (ui, "quit", G_CALLBACK (quit_cb), NULL);
+
+ scanner = scanner_new ();
+ g_signal_connect (G_OBJECT (scanner), "update-devices", G_CALLBACK (update_scan_devices_cb), NULL);
+ g_signal_connect (G_OBJECT (scanner), "authorize", G_CALLBACK (authorize_cb), NULL);
+ g_signal_connect (G_OBJECT (scanner), "expect-page", G_CALLBACK (scanner_new_page_cb), NULL);
+ g_signal_connect (G_OBJECT (scanner), "got-page-info", G_CALLBACK (scanner_page_info_cb), NULL);
+ g_signal_connect (G_OBJECT (scanner), "got-line", G_CALLBACK (scanner_line_cb), NULL);
+ g_signal_connect (G_OBJECT (scanner), "page-done", G_CALLBACK (scanner_page_done_cb), NULL);
+ g_signal_connect (G_OBJECT (scanner), "document-done", G_CALLBACK (scanner_document_done_cb), NULL);
+ g_signal_connect (G_OBJECT (scanner), "scan-failed", G_CALLBACK (scanner_failed_cb), NULL);
+ g_signal_connect (G_OBJECT (scanner), "scanning-changed", G_CALLBACK (scanner_scanning_changed_cb), NULL);
+
+ udev_client = g_udev_client_new (udev_subsystems);
+ g_signal_connect (udev_client, "uevent", G_CALLBACK (on_uevent), NULL);
+
+ if (default_device)
+ ui_set_selected_device (ui, default_device);
+
+ ui_start (ui);
+ scanner_start (scanner);
+
+ gtk_main ();
+
+ return 0;
+}
diff --git a/src/ui.c b/src/ui.c
new file mode 100644
index 0000000..92e83c7
--- /dev/null
+++ b/src/ui.c
@@ -0,0 +1,1648 @@
+/*
+ * Copyright (C) 2009 Canonical Ltd.
+ * Author: Robert Ancell <robert.ancell@canonical.com>
+ *
+ * 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. See http://www.gnu.org/copyleft/gpl.html the full text of the
+ * license.
+ */
+
+#include <stdlib.h>
+#include <string.h>
+#include <glib/gi18n.h>
+#include <gtk/gtk.h>
+#include <gconf/gconf-client.h>
+#include <math.h>
+#include <unistd.h> // TEMP: Needed for close() in get_temporary_filename()
+
+#include "ui.h"
+#include "book-view.h"
+
+
+#define DEFAULT_TEXT_DPI 150
+#define DEFAULT_PHOTO_DPI 300
+
+
+enum {
+ START_SCAN,
+ STOP_SCAN,
+ SAVE,
+ EMAIL,
+ QUIT,
+ LAST_SIGNAL
+};
+static guint signals[LAST_SIGNAL] = { 0, };
+
+
+struct SimpleScanPrivate
+{
+ GConfClient *client;
+
+ GtkBuilder *builder;
+
+ GtkWidget *window;
+ GtkWidget *preview_box, *preview_area, *preview_scroll;
+ GtkWidget *page_delete_menuitem, *crop_rotate_menuitem;
+ GtkWidget *stop_menuitem, *stop_toolbutton;
+
+ GtkWidget *text_toolbar_menuitem, *text_menu_menuitem;
+ GtkWidget *photo_toolbar_menuitem, *photo_menu_menuitem;
+
+ GtkWidget *authorize_dialog;
+ GtkWidget *authorize_label;
+ GtkWidget *username_entry, *password_entry;
+
+ GtkWidget *preferences_dialog;
+ GtkWidget *device_combo, *text_dpi_combo, *photo_dpi_combo, *page_side_combo, *paper_size_combo;
+ GtkTreeModel *device_model, *text_dpi_model, *photo_dpi_model, *page_side_model, *paper_size_model;
+ gboolean setting_devices, user_selected_device;
+
+ Book *book;
+ BookView *book_view;
+ gboolean updating_page_menu;
+ gint default_page_width, default_page_height, default_page_dpi;
+ Orientation default_page_orientation;
+
+ gboolean have_device_list;
+
+ gchar *document_hint;
+
+ gchar *default_file_name;
+ gboolean scanning;
+
+ gint window_width, window_height;
+ gboolean window_is_maximized;
+};
+
+G_DEFINE_TYPE (SimpleScan, ui, G_TYPE_OBJECT);
+
+static struct
+{
+ const gchar *key;
+ Orientation orientation;
+} orientation_keys[] =
+{
+ { "top-to-bottom", TOP_TO_BOTTOM },
+ { "bottom-to-top", BOTTOM_TO_TOP },
+ { "left-to-right", LEFT_TO_RIGHT },
+ { "right-to-left", RIGHT_TO_LEFT },
+ { NULL, 0 }
+};
+
+
+static gboolean
+find_scan_device (SimpleScan *ui, const char *device, GtkTreeIter *iter)
+{
+ gboolean have_iter = FALSE;
+
+ if (gtk_tree_model_get_iter_first (ui->priv->device_model, iter)) {
+ do {
+ gchar *d;
+ gtk_tree_model_get (ui->priv->device_model, iter, 0, &d, -1);
+ if (strcmp (d, device) == 0)
+ have_iter = TRUE;
+ g_free (d);
+ } while (!have_iter && gtk_tree_model_iter_next (ui->priv->device_model, iter));
+ }
+
+ return have_iter;
+}
+
+
+void
+ui_set_default_file_name (SimpleScan *ui, const gchar *default_file_name)
+{
+ g_free (ui->priv->default_file_name);
+ ui->priv->default_file_name = g_strdup (default_file_name);
+}
+
+
+void
+ui_authorize (SimpleScan *ui, const gchar *resource, gchar **username, gchar **password)
+{
+ GString *description;
+
+ description = g_string_new ("");
+ g_string_printf (description,
+ /* Label in authorization dialog. '%s' is replaced with the name of the resource requesting authorization */
+ _("Username and password required to access '%s'"),
+ resource);
+
+ gtk_entry_set_text (GTK_ENTRY (ui->priv->username_entry), *username ? *username : "");
+ gtk_entry_set_text (GTK_ENTRY (ui->priv->password_entry), "");
+ gtk_label_set_text (GTK_LABEL (ui->priv->authorize_label), description->str);
+ g_string_free (description, TRUE);
+
+ gtk_widget_show (ui->priv->authorize_dialog);
+ gtk_dialog_run (GTK_DIALOG (ui->priv->authorize_dialog));
+ gtk_widget_hide (ui->priv->authorize_dialog);
+
+ *username = g_strdup (gtk_entry_get_text (GTK_ENTRY (ui->priv->username_entry)));
+ *password = g_strdup (gtk_entry_get_text (GTK_ENTRY (ui->priv->password_entry)));
+}
+
+
+void device_combo_changed_cb (GtkWidget *widget, SimpleScan *ui);
+G_MODULE_EXPORT
+void
+device_combo_changed_cb (GtkWidget *widget, SimpleScan *ui)
+{
+ if (ui->priv->setting_devices)
+ return;
+ ui->priv->user_selected_device = TRUE;
+}
+
+
+void
+ui_set_scan_devices (SimpleScan *ui, GList *devices)
+{
+ GList *d;
+ gboolean have_selection = FALSE;
+ gint index;
+ GtkTreeIter iter;
+
+ ui->priv->setting_devices = TRUE;
+
+ /* If the user hasn't chosen a scanner choose the best available one */
+ if (ui->priv->user_selected_device)
+ have_selection = gtk_combo_box_get_active (GTK_COMBO_BOX (ui->priv->device_combo)) >= 0;
+
+ /* Add new devices */
+ index = 0;
+ for (d = devices; d; d = d->next) {
+ ScanDevice *device = (ScanDevice *) d->data;
+ gint n_delete = -1;
+
+ /* Find if already exists */
+ if (gtk_tree_model_iter_nth_child (ui->priv->device_model, &iter, NULL, index)) {
+ gint i = 0;
+ do {
+ gchar *name;
+ gboolean matched;
+
+ gtk_tree_model_get (ui->priv->device_model, &iter, 0, &name, -1);
+ matched = strcmp (name, device->name) == 0;
+ g_free (name);
+
+ if (matched) {
+ n_delete = i;
+ break;
+ }
+ i++;
+ } while (gtk_tree_model_iter_next (ui->priv->device_model, &iter));
+ }
+
+ /* If exists, remove elements up to this one */
+ if (n_delete >= 0) {
+ gint i;
+
+ for (i = 0; i < n_delete; i++) {
+ gtk_tree_model_iter_nth_child (ui->priv->device_model, &iter, NULL, index);
+ gtk_list_store_remove (GTK_LIST_STORE (ui->priv->device_model), &iter);
+ }
+ }
+ else {
+ gtk_list_store_insert (GTK_LIST_STORE (ui->priv->device_model), &iter, index);
+ gtk_list_store_set (GTK_LIST_STORE (ui->priv->device_model), &iter, 0, device->name, 1, device->label, -1);
+ }
+ index++;
+ }
+
+ /* Remove any remaining devices */
+ while (gtk_tree_model_iter_nth_child (ui->priv->device_model, &iter, NULL, index))
+ gtk_list_store_remove (GTK_LIST_STORE (ui->priv->device_model), &iter);
+
+ /* Select the first available device */
+ if (!have_selection && devices != NULL)
+ gtk_combo_box_set_active (GTK_COMBO_BOX (ui->priv->device_combo), 0);
+
+ ui->priv->setting_devices = FALSE;
+
+ if (!ui->priv->have_device_list) {
+ ui->priv->have_device_list = TRUE;
+
+ if (!devices) {
+ ui_show_error (ui,
+ /* Warning displayed when no scanners are detected */
+ _("No scanners detected"),
+ /* Hint to user on why there are no scanners detected */
+ _("Please check your scanner is connected and powered on"),
+ FALSE);
+ }
+ }
+}
+
+
+static gchar *
+get_selected_device (SimpleScan *ui)
+{
+ GtkTreeIter iter;
+
+ if (gtk_combo_box_get_active_iter (GTK_COMBO_BOX (ui->priv->device_combo), &iter)) {
+ gchar *device;
+ gtk_tree_model_get (ui->priv->device_model, &iter, 0, &device, -1);
+ return device;
+ }
+
+ return NULL;
+}
+
+
+void
+ui_set_selected_device (SimpleScan *ui, const gchar *device)
+{
+ GtkTreeIter iter;
+
+ /* If doesn't exist add with label set to device name */
+ if (!find_scan_device (ui, device, &iter)) {
+ gtk_list_store_append (GTK_LIST_STORE (ui->priv->device_model), &iter);
+ gtk_list_store_set (GTK_LIST_STORE (ui->priv->device_model), &iter, 0, device, 1, device, -1);
+ }
+
+ gtk_combo_box_set_active_iter (GTK_COMBO_BOX (ui->priv->device_combo), &iter);
+}
+
+
+static void
+add_default_page (SimpleScan *ui)
+{
+ Page *page;
+
+ page = book_append_page (ui->priv->book,
+ ui->priv->default_page_width,
+ ui->priv->default_page_height,
+ ui->priv->default_page_dpi,
+ ui->priv->default_page_orientation);
+ book_view_select_page (ui->priv->book_view, page);
+}
+
+
+void new_button_clicked_cb (GtkWidget *widget, SimpleScan *ui);
+G_MODULE_EXPORT
+void
+new_button_clicked_cb (GtkWidget *widget, SimpleScan *ui)
+{
+ book_clear (ui->priv->book);
+ add_default_page (ui);
+}
+
+
+static void
+set_document_hint (SimpleScan *ui, const gchar *document_hint)
+{
+ g_free (ui->priv->document_hint);
+ ui->priv->document_hint = g_strdup (document_hint);
+
+ if (strcmp (document_hint, "text") == 0) {
+ gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (ui->priv->text_toolbar_menuitem), TRUE);
+ gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (ui->priv->text_menu_menuitem), TRUE);
+ }
+ else if (strcmp (document_hint, "photo") == 0) {
+ gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (ui->priv->photo_toolbar_menuitem), TRUE);
+ gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (ui->priv->photo_menu_menuitem), TRUE);
+ }
+}
+
+
+void text_menuitem_toggled_cb (GtkWidget *widget, SimpleScan *ui);
+G_MODULE_EXPORT
+void
+text_menuitem_toggled_cb (GtkWidget *widget, SimpleScan *ui)
+{
+ if (gtk_check_menu_item_get_active (GTK_CHECK_MENU_ITEM (widget)))
+ set_document_hint (ui, "text");
+}
+
+
+void photo_menuitem_toggled_cb (GtkWidget *widget, SimpleScan *ui);
+G_MODULE_EXPORT
+void
+photo_menuitem_toggled_cb (GtkWidget *widget, SimpleScan *ui)
+{
+ if (gtk_check_menu_item_get_active (GTK_CHECK_MENU_ITEM (widget)))
+ set_document_hint (ui, "photo");
+}
+
+
+static void
+set_page_side (SimpleScan *ui, const gchar *document_hint)
+{
+ GtkTreeIter iter;
+
+ if (gtk_tree_model_get_iter_first (ui->priv->page_side_model, &iter)) {
+ do {
+ gchar *d;
+ gboolean have_match;
+
+ gtk_tree_model_get (ui->priv->page_side_model, &iter, 0, &d, -1);
+ have_match = strcmp (d, document_hint) == 0;
+ g_free (d);
+
+ if (have_match) {
+ gtk_combo_box_set_active_iter (GTK_COMBO_BOX (ui->priv->page_side_combo), &iter);
+ return;
+ }
+ } while (gtk_tree_model_iter_next (ui->priv->page_side_model, &iter));
+ }
+}
+
+
+static void
+set_paper_size (SimpleScan *ui, gint width, gint height)
+{
+ GtkTreeIter iter;
+ gboolean have_iter;
+
+ for (have_iter = gtk_tree_model_get_iter_first (ui->priv->paper_size_model, &iter);
+ have_iter;
+ have_iter = gtk_tree_model_iter_next (ui->priv->paper_size_model, &iter)) {
+ gint w, h;
+
+ gtk_tree_model_get (ui->priv->paper_size_model, &iter, 0, &w, 1, &h, -1);
+ if (w == width && h == height)
+ break;
+ }
+
+ if (!have_iter)
+ have_iter = gtk_tree_model_get_iter_first (ui->priv->paper_size_model, &iter);
+ if (have_iter)
+ gtk_combo_box_set_active_iter (GTK_COMBO_BOX (ui->priv->paper_size_combo), &iter);
+}
+
+
+static gint
+get_text_dpi (SimpleScan *ui)
+{
+ GtkTreeIter iter;
+ gint dpi = DEFAULT_TEXT_DPI;
+
+ if (gtk_combo_box_get_active_iter (GTK_COMBO_BOX (ui->priv->text_dpi_combo), &iter))
+ gtk_tree_model_get (ui->priv->text_dpi_model, &iter, 0, &dpi, -1);
+
+ return dpi;
+}
+
+
+static gint
+get_photo_dpi (SimpleScan *ui)
+{
+ GtkTreeIter iter;
+ gint dpi = DEFAULT_PHOTO_DPI;
+
+ if (gtk_combo_box_get_active_iter (GTK_COMBO_BOX (ui->priv->photo_dpi_combo), &iter))
+ gtk_tree_model_get (ui->priv->photo_dpi_model, &iter, 0, &dpi, -1);
+
+ return dpi;
+}
+
+
+static gchar *
+get_page_side (SimpleScan *ui)
+{
+ GtkTreeIter iter;
+ gchar *mode = NULL;
+
+ if (gtk_combo_box_get_active_iter (GTK_COMBO_BOX (ui->priv->page_side_combo), &iter))
+ gtk_tree_model_get (ui->priv->page_side_model, &iter, 0, &mode, -1);
+
+ return mode;
+}
+
+
+static gboolean
+get_paper_size (SimpleScan *ui, gint *width, gint *height)
+{
+ GtkTreeIter iter;
+
+ if (gtk_combo_box_get_active_iter (GTK_COMBO_BOX (ui->priv->paper_size_combo), &iter)) {
+ gtk_tree_model_get (ui->priv->paper_size_model, &iter, 0, width, 1, height, -1);
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+
+static ScanOptions *
+get_scan_options (SimpleScan *ui)
+{
+ struct {
+ const gchar *name;
+ ScanMode mode;
+ } profiles[] =
+ {
+ { "text", SCAN_MODE_LINEART },
+ { "photo", SCAN_MODE_COLOR },
+ { NULL, SCAN_MODE_COLOR }
+ };
+ gint i;
+ ScanOptions *options;
+
+ /* Find this profile */
+ // FIXME: Move this into scan-profile.c
+ for (i = 0; profiles[i].name && strcmp (profiles[i].name, ui->priv->document_hint) != 0; i++);
+
+ options = g_malloc0 (sizeof (ScanOptions));
+ options->scan_mode = profiles[i].mode;
+ options->depth = 8;
+ if (options->scan_mode == SCAN_MODE_COLOR)
+ options->dpi = get_photo_dpi (ui);
+ else
+ options->dpi = get_text_dpi (ui);
+ get_paper_size (ui, &options->paper_width, &options->paper_height);
+
+ return options;
+}
+
+
+void scan_button_clicked_cb (GtkWidget *widget, SimpleScan *ui);
+G_MODULE_EXPORT
+void
+scan_button_clicked_cb (GtkWidget *widget, SimpleScan *ui)
+{
+ gchar *device;
+ ScanOptions *options;
+
+ device = get_selected_device (ui);
+
+ options = get_scan_options (ui);
+ options->type = SCAN_SINGLE;
+ g_signal_emit (G_OBJECT (ui), signals[START_SCAN], 0, device, options);
+ g_free (device);
+ g_free (options);
+}
+
+
+void stop_scan_button_clicked_cb (GtkWidget *widget, SimpleScan *ui);
+G_MODULE_EXPORT
+void
+stop_scan_button_clicked_cb (GtkWidget *widget, SimpleScan *ui)
+{
+ g_signal_emit (G_OBJECT (ui), signals[STOP_SCAN], 0);
+}
+
+
+void continuous_scan_button_clicked_cb (GtkWidget *widget, SimpleScan *ui);
+G_MODULE_EXPORT
+void
+continuous_scan_button_clicked_cb (GtkWidget *widget, SimpleScan *ui)
+{
+ if (ui->priv->scanning) {
+ g_signal_emit (G_OBJECT (ui), signals[STOP_SCAN], 0);
+ } else {
+ gchar *device, *side;
+ ScanOptions *options;
+
+ device = get_selected_device (ui);
+ options = get_scan_options (ui);
+ side = get_page_side (ui);
+ if (strcmp (side, "front") == 0)
+ options->type = SCAN_ADF_FRONT;
+ else if (strcmp (side, "back") == 0)
+ options->type = SCAN_ADF_BACK;
+ else
+ options->type = SCAN_ADF_BOTH;
+
+ g_signal_emit (G_OBJECT (ui), signals[START_SCAN], 0, device, options);
+ g_free (device);
+ g_free (side);
+ g_free (options);
+ }
+}
+
+
+void preferences_button_clicked_cb (GtkWidget *widget, SimpleScan *ui);
+G_MODULE_EXPORT
+void
+preferences_button_clicked_cb (GtkWidget *widget, SimpleScan *ui)
+{
+ gtk_window_present (GTK_WINDOW (ui->priv->preferences_dialog));
+}
+
+
+gboolean preferences_dialog_delete_event_cb (GtkWidget *widget, SimpleScan *ui);
+G_MODULE_EXPORT
+gboolean
+preferences_dialog_delete_event_cb (GtkWidget *widget, SimpleScan *ui)
+{
+ return TRUE;
+}
+
+
+void preferences_dialog_response_cb (GtkWidget *widget, gint response_id, SimpleScan *ui);
+G_MODULE_EXPORT
+void
+preferences_dialog_response_cb (GtkWidget *widget, gint response_id, SimpleScan *ui)
+{
+ gtk_widget_hide (ui->priv->preferences_dialog);
+}
+
+
+static void
+page_selected_cb (BookView *view, Page *page, SimpleScan *ui)
+{
+ char *name = NULL;
+
+ if (page == NULL)
+ return;
+
+ ui->priv->updating_page_menu = TRUE;
+
+ if (page_has_crop (page)) {
+ char *crop_name;
+
+ // FIXME: Make more generic, move into page-size.c and reuse
+ crop_name = page_get_named_crop (page);
+ if (crop_name) {
+ if (strcmp (crop_name, "A4") == 0)
+ name = "a4_menuitem";
+ else if (strcmp (crop_name, "A5") == 0)
+ name = "a5_menuitem";
+ else if (strcmp (crop_name, "A6") == 0)
+ name = "a6_menuitem";
+ else if (strcmp (crop_name, "letter") == 0)
+ name = "letter_menuitem";
+ else if (strcmp (crop_name, "legal") == 0)
+ name = "legal_menuitem";
+ else if (strcmp (crop_name, "4x6") == 0)
+ name = "4x6_menuitem";
+ g_free (crop_name);
+ }
+ else
+ name = "custom_crop_menuitem";
+ }
+ else
+ name = "no_crop_menuitem";
+
+ gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (gtk_builder_get_object (ui->priv->builder, name)), TRUE);
+ gtk_toggle_tool_button_set_active (GTK_TOGGLE_TOOL_BUTTON (gtk_builder_get_object (ui->priv->builder, "crop_toolbutton")), page_has_crop (page));
+
+ ui->priv->updating_page_menu = FALSE;
+}
+
+
+// FIXME: Duplicated from simple-scan.c
+static gchar *
+get_temporary_filename (const gchar *prefix, const gchar *extension)
+{
+ gint fd;
+ gchar *filename, *path;
+ GError *error = NULL;
+
+ /* NOTE: I'm not sure if this is a 100% safe strategy to use g_file_open_tmp(), close and
+ * use the filename but it appears to work in practise */
+
+ filename = g_strdup_printf ("%s-XXXXXX.%s", prefix, extension);
+ fd = g_file_open_tmp (filename, &path, &error);
+ g_free (filename);
+ if (fd < 0) {
+ g_warning ("Error saving page for viewing: %s", error->message);
+ g_clear_error (&error);
+ return NULL;
+ }
+ close (fd);
+
+ return path;
+}
+
+
+static void
+show_page_cb (BookView *view, Page *page, SimpleScan *ui)
+{
+ gchar *path;
+ GFile *file;
+ GdkScreen *screen;
+ GError *error = NULL;
+
+ path = get_temporary_filename ("scanned-page", "tiff");
+ if (!path)
+ return;
+ file = g_file_new_for_path (path);
+ g_free (path);
+
+ screen = gtk_widget_get_screen (GTK_WIDGET (ui->priv->window));
+
+ if (page_save (page, "tiff", file, &error)) {
+ gchar *uri = g_file_get_uri (file);
+ gtk_show_uri (screen, uri, gtk_get_current_event_time (), &error);
+ g_free (uri);
+ }
+
+ g_object_unref (file);
+
+ if (error) {
+ ui_show_error (ui,
+ /* Error message display when unable to preview image */
+ _("Unable to open image preview application"),
+ error->message,
+ FALSE);
+ g_clear_error (&error);
+ }
+}
+
+
+void rotate_left_button_clicked_cb (GtkWidget *widget, SimpleScan *ui);
+G_MODULE_EXPORT
+void
+rotate_left_button_clicked_cb (GtkWidget *widget, SimpleScan *ui)
+{
+ Page *page;
+
+ if (ui->priv->updating_page_menu)
+ return;
+ page = book_view_get_selected (ui->priv->book_view);
+ page_rotate_left (page);
+}
+
+
+void rotate_right_button_clicked_cb (GtkWidget *widget, SimpleScan *ui);
+G_MODULE_EXPORT
+void
+rotate_right_button_clicked_cb (GtkWidget *widget, SimpleScan *ui)
+{
+ Page *page;
+
+ if (ui->priv->updating_page_menu)
+ return;
+ page = book_view_get_selected (ui->priv->book_view);
+ page_rotate_right (page);
+}
+
+
+static void
+set_crop (SimpleScan *ui, const gchar *crop_name)
+{
+ Page *page;
+
+ gtk_widget_set_sensitive (ui->priv->crop_rotate_menuitem, crop_name != NULL);
+
+ if (ui->priv->updating_page_menu)
+ return;
+
+ page = book_view_get_selected (ui->priv->book_view);
+ if (!page)
+ return;
+
+ if (!crop_name) {
+ page_set_no_crop (page);
+ return;
+ }
+
+ if (strcmp (crop_name, "custom") == 0) {
+ gint width, height, crop_width, crop_height;
+
+ width = page_get_width (page);
+ height = page_get_height (page);
+
+ crop_width = (int) (width * 0.8 + 0.5);
+ crop_height = (int) (height * 0.8 + 0.5);
+ page_set_custom_crop (page, crop_width, crop_height);
+ page_move_crop (page, (width - crop_width) / 2, (height - crop_height) / 2);
+
+ return;
+ }
+
+ page_set_named_crop (page, crop_name);
+}
+
+
+void no_crop_menuitem_toggled_cb (GtkWidget *widget, SimpleScan *ui);
+G_MODULE_EXPORT
+void
+no_crop_menuitem_toggled_cb (GtkWidget *widget, SimpleScan *ui)
+{
+ if (gtk_check_menu_item_get_active (GTK_CHECK_MENU_ITEM (widget)))
+ set_crop (ui, NULL);
+}
+
+
+void custom_crop_menuitem_toggled_cb (GtkWidget *widget, SimpleScan *ui);
+G_MODULE_EXPORT
+void
+custom_crop_menuitem_toggled_cb (GtkWidget *widget, SimpleScan *ui)
+{
+ if (gtk_check_menu_item_get_active (GTK_CHECK_MENU_ITEM (widget)))
+ set_crop (ui, "custom");
+}
+
+void crop_toolbutton_toggled_cb (GtkWidget *widget, SimpleScan *ui);
+G_MODULE_EXPORT
+void
+crop_toolbutton_toggled_cb (GtkWidget *widget, SimpleScan *ui)
+{
+ if (ui->priv->updating_page_menu)
+ return;
+
+ if (gtk_toggle_tool_button_get_active (GTK_TOGGLE_TOOL_BUTTON (widget)))
+ gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (gtk_builder_get_object (ui->priv->builder, "custom_crop_menuitem")), TRUE);
+ else
+ gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (gtk_builder_get_object (ui->priv->builder, "no_crop_menuitem")), TRUE);
+}
+
+
+void four_by_six_menuitem_toggled_cb (GtkWidget *widget, SimpleScan *ui);
+G_MODULE_EXPORT
+void
+four_by_six_menuitem_toggled_cb (GtkWidget *widget, SimpleScan *ui)
+{
+ if (gtk_check_menu_item_get_active (GTK_CHECK_MENU_ITEM (widget)))
+ set_crop (ui, "4x6");
+}
+
+
+void legal_menuitem_toggled_cb (GtkWidget *widget, SimpleScan *ui);
+G_MODULE_EXPORT
+void
+legal_menuitem_toggled_cb (GtkWidget *widget, SimpleScan *ui)
+{
+ if (gtk_check_menu_item_get_active (GTK_CHECK_MENU_ITEM (widget)))
+ set_crop (ui, "legal");
+}
+
+
+void letter_menuitem_toggled_cb (GtkWidget *widget, SimpleScan *ui);
+G_MODULE_EXPORT
+void
+letter_menuitem_toggled_cb (GtkWidget *widget, SimpleScan *ui)
+{
+ if (gtk_check_menu_item_get_active (GTK_CHECK_MENU_ITEM (widget)))
+ set_crop (ui, "letter");
+}
+
+
+void a6_menuitem_toggled_cb (GtkWidget *widget, SimpleScan *ui);
+G_MODULE_EXPORT
+void
+a6_menuitem_toggled_cb (GtkWidget *widget, SimpleScan *ui)
+{
+ if (gtk_check_menu_item_get_active (GTK_CHECK_MENU_ITEM (widget)))
+ set_crop (ui, "A6");
+}
+
+
+void a5_menuitem_toggled_cb (GtkWidget *widget, SimpleScan *ui);
+G_MODULE_EXPORT
+void
+a5_menuitem_toggled_cb (GtkWidget *widget, SimpleScan *ui)
+{
+ if (gtk_check_menu_item_get_active (GTK_CHECK_MENU_ITEM (widget)))
+ set_crop (ui, "A5");
+}
+
+
+void a4_menuitem_toggled_cb (GtkWidget *widget, SimpleScan *ui);
+G_MODULE_EXPORT
+void
+a4_menuitem_toggled_cb (GtkWidget *widget, SimpleScan *ui)
+{
+ if (gtk_check_menu_item_get_active (GTK_CHECK_MENU_ITEM (widget)))
+ set_crop (ui, "A4");
+}
+
+
+void crop_rotate_menuitem_activate_cb (GtkWidget *widget, SimpleScan *ui);
+G_MODULE_EXPORT
+void
+crop_rotate_menuitem_activate_cb (GtkWidget *widget, SimpleScan *ui)
+{
+ Page *page;
+
+ page = book_view_get_selected (ui->priv->book_view);
+ if (!page)
+ return;
+
+ page_rotate_crop (page);
+}
+
+
+void page_delete_menuitem_activate_cb (GtkWidget *widget, SimpleScan *ui);
+G_MODULE_EXPORT
+void
+page_delete_menuitem_activate_cb (GtkWidget *widget, SimpleScan *ui)
+{
+ book_delete_page (book_view_get_book (ui->priv->book_view),
+ book_view_get_selected (ui->priv->book_view));
+}
+
+
+static void
+on_file_type_changed (GtkTreeSelection *selection, GtkWidget *dialog)
+{
+ GtkTreeModel *model;
+ GtkTreeIter iter;
+ gchar *path, *filename, *extension, *new_filename;
+
+ if (!gtk_tree_selection_get_selected (selection, &model, &iter))
+ return;
+
+ gtk_tree_model_get (model, &iter, 1, &extension, -1);
+ path = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (dialog));
+ filename = g_path_get_basename (path);
+
+ /* Replace extension */
+ if (g_strrstr (filename, "."))
+ new_filename = g_strdup_printf ("%.*s%s", (int)(g_strrstr (filename, ".") - filename), filename, extension);
+ else
+ new_filename = g_strdup_printf ("%s%s", filename, extension);
+ gtk_file_chooser_set_current_name (GTK_FILE_CHOOSER (dialog), new_filename);
+
+ g_free (path);
+ g_free (filename);
+ g_free (new_filename);
+ g_free (extension);
+}
+
+
+void save_file_button_clicked_cb (GtkWidget *widget, SimpleScan *ui);
+G_MODULE_EXPORT
+void
+save_file_button_clicked_cb (GtkWidget *widget, SimpleScan *ui)
+{
+ GtkWidget *dialog;
+ gint response;
+ GtkFileFilter *filter;
+ GtkWidget *expander, *file_type_view;
+ GtkListStore *file_type_store;
+ GtkTreeIter iter;
+ GtkTreeViewColumn *column;
+ const gchar *extension;
+ gchar *directory;
+ gint i;
+
+ struct
+ {
+ gchar *label, *extension;
+ } file_types[] =
+ {
+ /* Save dialog: Label for saving in PDF format */
+ { _("PDF (multi-page document)"), ".pdf" },
+ /* Save dialog: Label for saving in JPEG format */
+ { _("JPEG (compressed)"), ".jpg" },
+ /* Save dialog: Label for saving in PNG format */
+ { _("PNG (lossless)"), ".png" },
+ { NULL, NULL }
+ };
+
+ /* Get directory to save to */
+ directory = gconf_client_get_string (ui->priv->client, GCONF_DIR "/save_directory", NULL);
+ if (!directory || directory[0] == '\0') {
+ g_free (directory);
+ directory = g_strdup (g_get_user_special_dir (G_USER_DIRECTORY_DOCUMENTS));
+ }
+
+ dialog = gtk_file_chooser_dialog_new (/* Save dialog: Dialog title */
+ _("Save As..."),
+ GTK_WINDOW (ui->priv->window),
+ GTK_FILE_CHOOSER_ACTION_SAVE,
+ GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
+ GTK_STOCK_SAVE, GTK_RESPONSE_ACCEPT,
+ NULL);
+ gtk_file_chooser_set_do_overwrite_confirmation (GTK_FILE_CHOOSER (dialog), TRUE);
+ gtk_file_chooser_set_local_only (GTK_FILE_CHOOSER (dialog), FALSE);
+ gtk_file_chooser_set_current_folder (GTK_FILE_CHOOSER (dialog), directory);
+ gtk_file_chooser_set_current_name (GTK_FILE_CHOOSER (dialog), ui->priv->default_file_name);
+ g_free (directory);
+
+ /* Filter to only show images by default */
+ filter = gtk_file_filter_new ();
+ gtk_file_filter_set_name (filter,
+ /* Save dialog: Filter name to show only image files */
+ _("Image Files"));
+ gtk_file_filter_add_pixbuf_formats (filter);
+ gtk_file_filter_add_mime_type (filter, "application/pdf");
+ gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (dialog), filter);
+ filter = gtk_file_filter_new ();
+ gtk_file_filter_set_name (filter,
+ /* Save dialog: Filter name to show all files */
+ _("All Files"));
+ gtk_file_filter_add_pattern (filter, "*");
+ gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (dialog), filter);
+
+ expander = gtk_expander_new_with_mnemonic (/* */
+ _("Select File _Type"));
+ gtk_expander_set_spacing (GTK_EXPANDER (expander), 5);
+ gtk_file_chooser_set_extra_widget (GTK_FILE_CHOOSER (dialog), expander);
+
+ extension = strstr (ui->priv->default_file_name, ".");
+ if (!extension)
+ extension = "";
+
+ file_type_store = gtk_list_store_new (2, G_TYPE_STRING, G_TYPE_STRING);
+ for (i = 0; file_types[i].label; i++) {
+ gtk_list_store_append (file_type_store, &iter);
+ gtk_list_store_set (file_type_store, &iter, 0, file_types[i].label, 1, file_types[i].extension, -1);
+ }
+
+ file_type_view = gtk_tree_view_new_with_model (GTK_TREE_MODEL (file_type_store));
+ gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (file_type_view), FALSE);
+ gtk_tree_view_set_rules_hint (GTK_TREE_VIEW (file_type_view), TRUE);
+ column = gtk_tree_view_column_new_with_attributes ("",
+ gtk_cell_renderer_text_new (),
+ "text", 0, NULL);
+ gtk_tree_view_append_column (GTK_TREE_VIEW (file_type_view), column);
+ gtk_container_add (GTK_CONTAINER (expander), file_type_view);
+
+ if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (file_type_store), &iter)) {
+ do {
+ gchar *e;
+ gtk_tree_model_get (GTK_TREE_MODEL (file_type_store), &iter, 1, &e, -1);
+ if (strcmp (extension, e) == 0)
+ gtk_tree_selection_select_iter (gtk_tree_view_get_selection (GTK_TREE_VIEW (file_type_view)), &iter);
+ g_free (e);
+ } while (gtk_tree_model_iter_next (GTK_TREE_MODEL (file_type_store), &iter));
+ }
+ g_signal_connect (gtk_tree_view_get_selection (GTK_TREE_VIEW (file_type_view)),
+ "changed",
+ G_CALLBACK (on_file_type_changed),
+ dialog);
+
+ gtk_widget_show_all (expander);
+
+ response = gtk_dialog_run (GTK_DIALOG (dialog));
+ if (response == GTK_RESPONSE_ACCEPT) {
+ gchar *uri;
+
+ uri = gtk_file_chooser_get_uri (GTK_FILE_CHOOSER (dialog));
+ g_signal_emit (G_OBJECT (ui), signals[SAVE], 0, uri);
+
+ g_free (uri);
+ }
+
+ gconf_client_set_string (ui->priv->client, GCONF_DIR "/save_directory",
+ gtk_file_chooser_get_current_folder (GTK_FILE_CHOOSER (dialog)),
+ NULL);
+
+ gtk_widget_destroy (dialog);
+}
+
+
+static void
+draw_page (GtkPrintOperation *operation,
+ GtkPrintContext *print_context,
+ gint page_number,
+ SimpleScan *ui)
+{
+ cairo_t *context;
+ Page *page;
+ GdkPixbuf *image;
+ gboolean is_landscape = FALSE;
+
+ context = gtk_print_context_get_cairo_context (print_context);
+
+ page = book_get_page (ui->priv->book, page_number);
+
+ /* Rotate to same aspect */
+ if (gtk_print_context_get_width (print_context) > gtk_print_context_get_height (print_context))
+ is_landscape = TRUE;
+ if (page_is_landscape (page) != is_landscape) {
+ cairo_translate (context, gtk_print_context_get_width (print_context), 0);
+ cairo_rotate (context, M_PI_2);
+ }
+
+ cairo_scale (context,
+ gtk_print_context_get_dpi_x (print_context) / page_get_dpi (page),
+ gtk_print_context_get_dpi_y (print_context) / page_get_dpi (page));
+
+ image = page_get_cropped_image (page);
+ gdk_cairo_set_source_pixbuf (context, image, 0, 0);
+ cairo_paint (context);
+
+ g_object_unref (image);
+}
+
+
+void email_button_clicked_cb (GtkWidget *widget, SimpleScan *ui);
+G_MODULE_EXPORT
+void
+email_button_clicked_cb (GtkWidget *widget, SimpleScan *ui)
+{
+ g_signal_emit (G_OBJECT (ui), signals[EMAIL], 0, ui->priv->document_hint);
+}
+
+
+void print_button_clicked_cb (GtkWidget *widget, SimpleScan *ui);
+G_MODULE_EXPORT
+void
+print_button_clicked_cb (GtkWidget *widget, SimpleScan *ui)
+{
+ GtkPrintOperation *print;
+ GtkPrintOperationResult result;
+ GError *error = NULL;
+
+ print = gtk_print_operation_new ();
+ gtk_print_operation_set_n_pages (print, book_get_n_pages (ui->priv->book));
+ g_signal_connect (print, "draw-page", G_CALLBACK (draw_page), ui);
+
+ result = gtk_print_operation_run (print, GTK_PRINT_OPERATION_ACTION_PRINT_DIALOG,
+ GTK_WINDOW (ui->priv->window), &error);
+
+ g_object_unref (print);
+}
+
+
+void help_contents_menuitem_activate_cb (GtkWidget *widget, SimpleScan *ui);
+G_MODULE_EXPORT
+void
+help_contents_menuitem_activate_cb (GtkWidget *widget, SimpleScan *ui)
+{
+ GdkScreen *screen;
+ GError *error = NULL;
+
+ screen = gtk_widget_get_screen (GTK_WIDGET (ui->priv->window));
+ gtk_show_uri (screen, "ghelp:simple-scan", gtk_get_current_event_time (), &error);
+
+ if (error)
+ {
+ ui_show_error (ui,
+ /* Error message displayed when unable to launch help browser */
+ _("Unable to open help file"),
+ error->message,
+ FALSE);
+ g_clear_error (&error);
+ }
+}
+
+
+void about_menuitem_activate_cb (GtkWidget *widget, SimpleScan *ui);
+G_MODULE_EXPORT
+void
+about_menuitem_activate_cb (GtkWidget *widget, SimpleScan *ui)
+{
+ const gchar *authors[] = { "Robert Ancell <robert.ancell@canonical.com>", NULL };
+
+ /* The license this software is under (GPL3+) */
+ const char *license = _("This program is free software: you can redistribute it and/or modify\n"
+ "it under the terms of the GNU General Public License as published by\n"
+ "the Free Software Foundation, either version 3 of the License, or\n"
+ "(at your option) any later version.\n"
+ "\n"
+ "This program is distributed in the hope that it will be useful,\n"
+ "but WITHOUT ANY WARRANTY; without even the implied warranty of\n"
+ "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n"
+ "GNU General Public License for more details.\n"
+ "\n"
+ "You should have received a copy of the GNU General Public License\n"
+ "along with this program. If not, see <http://www.gnu.org/licenses/>.");
+
+ /* Title of about dialog */
+ const char *title = _("About Simple Scan");
+
+ /* Description of program */
+ const char *description = _("Simple document scanning tool");
+
+ gtk_show_about_dialog (GTK_WINDOW (ui->priv->window),
+ "title", title,
+ "program-name", "Simple Scan",
+ "version", VERSION,
+ "comments", description,
+ "logo-icon-name", "scanner",
+ "authors", authors,
+ "translator-credits", _("translator-credits"),
+ "website", "https://launchpad.net/simple-scan",
+ "copyright", "Copyright © 2009 Canonical Ltd.",
+ "license", license,
+ "wrap-license", TRUE,
+ NULL);
+}
+
+
+static void
+quit (SimpleScan *ui)
+{
+ char *device;
+ gint paper_width = 0, paper_height = 0;
+ gint i;
+
+ // FIXME: Warn if document with unsaved changes
+
+ device = get_selected_device (ui);
+ if (device) {
+ gconf_client_set_string(ui->priv->client, GCONF_DIR "/selected_device", device, NULL);
+ g_free (device);
+ }
+
+ gconf_client_set_string (ui->priv->client, GCONF_DIR "/document_type", ui->priv->document_hint, NULL);
+ gconf_client_set_int (ui->priv->client, GCONF_DIR "/text_dpi", get_text_dpi (ui), NULL);
+ gconf_client_set_int (ui->priv->client, GCONF_DIR "/photo_dpi", get_photo_dpi (ui), NULL);
+ gconf_client_set_string (ui->priv->client, GCONF_DIR "/page_side", get_page_side (ui), NULL);
+ get_paper_size (ui, &paper_width, &paper_height);
+ gconf_client_set_int (ui->priv->client, GCONF_DIR "/paper_width", paper_width, NULL);
+ gconf_client_set_int (ui->priv->client, GCONF_DIR "/paper_height", paper_height, NULL);
+
+ gconf_client_set_int(ui->priv->client, GCONF_DIR "/window_width", ui->priv->window_width, NULL);
+ gconf_client_set_int(ui->priv->client, GCONF_DIR "/window_height", ui->priv->window_height, NULL);
+ gconf_client_set_bool(ui->priv->client, GCONF_DIR "/window_is_maximized", ui->priv->window_is_maximized, NULL);
+
+ for (i = 0; orientation_keys[i].key != NULL && orientation_keys[i].orientation != ui->priv->default_page_orientation; i++);
+ if (orientation_keys[i].key != NULL)
+ gconf_client_set_string(ui->priv->client, GCONF_DIR "/scan_direction", orientation_keys[i].key, NULL);
+ gconf_client_set_int (ui->priv->client, GCONF_DIR "/page_width", ui->priv->default_page_width, NULL);
+ gconf_client_set_int (ui->priv->client, GCONF_DIR "/page_height", ui->priv->default_page_height, NULL);
+ gconf_client_set_int (ui->priv->client, GCONF_DIR "/page_dpi", ui->priv->default_page_dpi, NULL);
+
+ g_signal_emit (G_OBJECT (ui), signals[QUIT], 0);
+}
+
+
+void quit_menuitem_activate_cb (GtkWidget *widget, SimpleScan *ui);
+G_MODULE_EXPORT
+void
+quit_menuitem_activate_cb (GtkWidget *widget, SimpleScan *ui)
+{
+ quit (ui);
+}
+
+
+gboolean simple_scan_window_configure_event_cb (GtkWidget *widget, GdkEventConfigure *event, SimpleScan *ui);
+G_MODULE_EXPORT
+gboolean
+simple_scan_window_configure_event_cb (GtkWidget *widget, GdkEventConfigure *event, SimpleScan *ui)
+{
+ if (!ui->priv->window_is_maximized) {
+ ui->priv->window_width = event->width;
+ ui->priv->window_height = event->height;
+ }
+
+ return FALSE;
+}
+
+
+gboolean simple_scan_window_window_state_event_cb (GtkWidget *widget, GdkEventWindowState *event, SimpleScan *ui);
+G_MODULE_EXPORT
+gboolean
+simple_scan_window_window_state_event_cb (GtkWidget *widget, GdkEventWindowState *event, SimpleScan *ui)
+{
+ if (event->changed_mask & GDK_WINDOW_STATE_MAXIMIZED)
+ ui->priv->window_is_maximized = (event->new_window_state & GDK_WINDOW_STATE_MAXIMIZED) != 0;
+ return FALSE;
+}
+
+
+gboolean window_delete_event_cb (GtkWidget *widget, GdkEvent *event, SimpleScan *ui);
+G_MODULE_EXPORT
+gboolean
+window_delete_event_cb (GtkWidget *widget, GdkEvent *event, SimpleScan *ui)
+{
+ quit (ui);
+ return TRUE;
+}
+
+
+static void
+page_size_changed_cb (Page *page, SimpleScan *ui)
+{
+ ui->priv->default_page_width = page_get_width (page);
+ ui->priv->default_page_height = page_get_height (page);
+ ui->priv->default_page_dpi = page_get_dpi (page);
+}
+
+
+static void
+page_orientation_changed_cb (Page *page, SimpleScan *ui)
+{
+ ui->priv->default_page_orientation = page_get_orientation (page);
+}
+
+
+static void
+page_added_cb (Book *book, Page *page, SimpleScan *ui)
+{
+ ui->priv->default_page_width = page_get_width (page);
+ ui->priv->default_page_height = page_get_height (page);
+ ui->priv->default_page_dpi = page_get_dpi (page);
+ ui->priv->default_page_orientation = page_get_orientation (page);
+ g_signal_connect (page, "size-changed", G_CALLBACK (page_size_changed_cb), ui);
+ g_signal_connect (page, "orientation-changed", G_CALLBACK (page_orientation_changed_cb), ui);
+}
+
+
+static void
+page_removed_cb (Book *book, Page *page, SimpleScan *ui)
+{
+ /* If this is the last page add a new blank one */
+ if (book_get_n_pages (ui->priv->book) == 1)
+ add_default_page (ui);
+}
+
+
+static void
+set_dpi_combo (GtkWidget *combo, gint default_dpi, gint current_dpi)
+{
+ struct
+ {
+ gint dpi;
+ const gchar *label;
+ } scan_resolutions[] =
+ {
+ /* Preferences dialog: Label for minimum resolution in resolution list */
+ { 75, _("%d dpi (draft)") },
+ /* Preferences dialog: Label for resolution value in resolution list (dpi = dots per inch) */
+ { 150, _("%d dpi") },
+ { 300, _("%d dpi") },
+ { 600, _("%d dpi") },
+ /* Preferences dialog: Label for maximum resolution in resolution list */
+ { 1200, _("%d dpi (high resolution)") },
+ { 2400, _("%d dpi") },
+ { -1, NULL }
+ };
+ GtkCellRenderer *renderer;
+ GtkTreeModel *model;
+ gint i;
+
+ renderer = gtk_cell_renderer_text_new();
+ gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (combo), renderer, TRUE);
+ gtk_cell_layout_add_attribute (GTK_CELL_LAYOUT (combo), renderer, "text", 1);
+
+ model = gtk_combo_box_get_model (GTK_COMBO_BOX (combo));
+ for (i = 0; scan_resolutions[i].dpi > 0; i++)
+ {
+ GtkTreeIter iter;
+ gchar *label;
+ gint dpi;
+
+ dpi = scan_resolutions[i].dpi;
+
+ if (dpi == default_dpi)
+ label = g_strdup_printf (/* Preferences dialog: Label for default resolution in resolution list */
+ _("%d dpi (default)"), dpi);
+ else
+ label = g_strdup_printf (scan_resolutions[i].label, dpi);
+
+ gtk_list_store_append (GTK_LIST_STORE (model), &iter);
+ gtk_list_store_set (GTK_LIST_STORE (model), &iter, 0, dpi, 1, label, -1);
+
+ if (dpi == current_dpi)
+ gtk_combo_box_set_active_iter (GTK_COMBO_BOX (combo), &iter);
+
+ g_free (label);
+ }
+}
+
+
+static void
+ui_load (SimpleScan *ui)
+{
+ GtkBuilder *builder;
+ GError *error = NULL;
+ GtkCellRenderer *renderer;
+ gchar *device, *document_type, *scan_direction, *page_side;
+ gint dpi, paper_width, paper_height;
+
+ gtk_icon_theme_append_search_path (gtk_icon_theme_get_default (), ICON_DIR);
+
+ gtk_window_set_default_icon_name ("scanner");
+
+ builder = ui->priv->builder = gtk_builder_new ();
+ gtk_builder_add_from_file (builder, UI_DIR "simple-scan.ui", &error);
+ if (error) {
+ g_critical ("Unable to load UI: %s\n", error->message);
+ ui_show_error (ui,
+ /* Title of dialog when cannot load required files */
+ _("Files missing"),
+ /* Description in dialog when cannot load required files */
+ _("Please check your installation"),
+ FALSE);
+ exit (1);
+ }
+ gtk_builder_connect_signals (builder, ui);
+
+ ui->priv->window = GTK_WIDGET (gtk_builder_get_object (builder, "simple_scan_window"));
+ ui->priv->preview_box = GTK_WIDGET (gtk_builder_get_object (builder, "preview_vbox"));
+ ui->priv->preview_area = GTK_WIDGET (gtk_builder_get_object (builder, "preview_area"));
+ ui->priv->preview_scroll = GTK_WIDGET (gtk_builder_get_object (builder, "preview_scrollbar"));
+ ui->priv->page_delete_menuitem = GTK_WIDGET (gtk_builder_get_object (builder, "page_delete_menuitem"));
+ ui->priv->crop_rotate_menuitem = GTK_WIDGET (gtk_builder_get_object (builder, "crop_rotate_menuitem"));
+ ui->priv->stop_menuitem = GTK_WIDGET (gtk_builder_get_object (builder, "stop_scan_menuitem"));
+ ui->priv->stop_toolbutton = GTK_WIDGET (gtk_builder_get_object (builder, "stop_toolbutton"));
+
+ ui->priv->text_toolbar_menuitem = GTK_WIDGET (gtk_builder_get_object (builder, "text_toolbutton_menuitem"));
+ ui->priv->text_menu_menuitem = GTK_WIDGET (gtk_builder_get_object (builder, "text_menuitem"));
+ ui->priv->photo_toolbar_menuitem = GTK_WIDGET (gtk_builder_get_object (builder, "photo_toolbutton_menuitem"));
+ ui->priv->photo_menu_menuitem = GTK_WIDGET (gtk_builder_get_object (builder, "photo_menuitem"));
+
+ ui->priv->authorize_dialog = GTK_WIDGET (gtk_builder_get_object (builder, "authorize_dialog"));
+ ui->priv->authorize_label = GTK_WIDGET (gtk_builder_get_object (builder, "authorize_label"));
+ ui->priv->username_entry = GTK_WIDGET (gtk_builder_get_object (builder, "username_entry"));
+ ui->priv->password_entry = GTK_WIDGET (gtk_builder_get_object (builder, "password_entry"));
+
+ ui->priv->preferences_dialog = GTK_WIDGET (gtk_builder_get_object (builder, "preferences_dialog"));
+ ui->priv->device_combo = GTK_WIDGET (gtk_builder_get_object (builder, "device_combo"));
+ ui->priv->device_model = gtk_combo_box_get_model (GTK_COMBO_BOX (ui->priv->device_combo));
+ ui->priv->text_dpi_combo = GTK_WIDGET (gtk_builder_get_object (builder, "text_dpi_combo"));
+ ui->priv->text_dpi_model = gtk_combo_box_get_model (GTK_COMBO_BOX (ui->priv->text_dpi_combo));
+ ui->priv->photo_dpi_combo = GTK_WIDGET (gtk_builder_get_object (builder, "photo_dpi_combo"));
+ ui->priv->photo_dpi_model = gtk_combo_box_get_model (GTK_COMBO_BOX (ui->priv->photo_dpi_combo));
+ ui->priv->page_side_combo = GTK_WIDGET (gtk_builder_get_object (builder, "page_side_combo"));
+ ui->priv->page_side_model = gtk_combo_box_get_model (GTK_COMBO_BOX (ui->priv->page_side_combo));
+ ui->priv->paper_size_combo = GTK_WIDGET (gtk_builder_get_object (builder, "paper_size_combo"));
+ ui->priv->paper_size_model = gtk_combo_box_get_model (GTK_COMBO_BOX (ui->priv->paper_size_combo));
+
+ GtkTreeIter iter;
+ gtk_list_store_append (GTK_LIST_STORE (ui->priv->paper_size_model), &iter);
+ gtk_list_store_set (GTK_LIST_STORE (ui->priv->paper_size_model), &iter, 0, 0, 1, 0, 2, "Automatic", -1);
+ gtk_list_store_append (GTK_LIST_STORE (ui->priv->paper_size_model), &iter);
+ gtk_list_store_set (GTK_LIST_STORE (ui->priv->paper_size_model), &iter, 0, 2050, 1, 1480, 2, "A6", -1);
+ gtk_list_store_append (GTK_LIST_STORE (ui->priv->paper_size_model), &iter);
+ gtk_list_store_set (GTK_LIST_STORE (ui->priv->paper_size_model), &iter, 0, 1480, 1, 2100, 2, "A5", -1);
+ gtk_list_store_append (GTK_LIST_STORE (ui->priv->paper_size_model), &iter);
+ gtk_list_store_set (GTK_LIST_STORE (ui->priv->paper_size_model), &iter, 0, 2100, 1, 1970, 2, "A4", -1);
+ gtk_list_store_append (GTK_LIST_STORE (ui->priv->paper_size_model), &iter);
+ gtk_list_store_set (GTK_LIST_STORE (ui->priv->paper_size_model), &iter, 0, 2159, 1, 2794, 2, "Letter", -1);
+ gtk_list_store_append (GTK_LIST_STORE (ui->priv->paper_size_model), &iter);
+ gtk_list_store_set (GTK_LIST_STORE (ui->priv->paper_size_model), &iter, 0, 2159, 1, 3556, 2, "Legal", -1);
+ gtk_list_store_append (GTK_LIST_STORE (ui->priv->paper_size_model), &iter);
+ gtk_list_store_set (GTK_LIST_STORE (ui->priv->paper_size_model), &iter, 0, 1016, 1, 1524, 2, "4×6", -1);
+
+ dpi = gconf_client_get_int (ui->priv->client, GCONF_DIR "/text_dpi", NULL);
+ if (dpi <= 0)
+ dpi = DEFAULT_TEXT_DPI;
+ set_dpi_combo (ui->priv->text_dpi_combo, DEFAULT_TEXT_DPI, dpi);
+ dpi = gconf_client_get_int (ui->priv->client, GCONF_DIR "/photo_dpi", NULL);
+ if (dpi <= 0)
+ dpi = DEFAULT_PHOTO_DPI;
+ set_dpi_combo (ui->priv->photo_dpi_combo, DEFAULT_PHOTO_DPI, dpi);
+
+ renderer = gtk_cell_renderer_text_new();
+ gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (ui->priv->device_combo), renderer, TRUE);
+ gtk_cell_layout_add_attribute (GTK_CELL_LAYOUT (ui->priv->device_combo), renderer, "text", 1);
+
+ renderer = gtk_cell_renderer_text_new();
+ gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (ui->priv->page_side_combo), renderer, TRUE);
+ gtk_cell_layout_add_attribute (GTK_CELL_LAYOUT (ui->priv->page_side_combo), renderer, "text", 1);
+ page_side = gconf_client_get_string (ui->priv->client, GCONF_DIR "/page_side", NULL);
+ if (page_side) {
+ set_page_side (ui, page_side);
+ g_free (page_side);
+ }
+
+ renderer = gtk_cell_renderer_text_new();
+ gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (ui->priv->paper_size_combo), renderer, TRUE);
+ gtk_cell_layout_add_attribute (GTK_CELL_LAYOUT (ui->priv->paper_size_combo), renderer, "text", 2);
+ paper_width = gconf_client_get_int (ui->priv->client, GCONF_DIR "/paper_width", NULL);
+ paper_height = gconf_client_get_int (ui->priv->client, GCONF_DIR "/paper_height", NULL);
+ set_paper_size (ui, paper_width, paper_height);
+
+ device = gconf_client_get_string (ui->priv->client, GCONF_DIR "/selected_device", NULL);
+ if (device) {
+ GtkTreeIter iter;
+ if (find_scan_device (ui, device, &iter))
+ gtk_combo_box_set_active_iter (GTK_COMBO_BOX (ui->priv->device_combo), &iter);
+ g_free (device);
+ }
+
+ document_type = gconf_client_get_string (ui->priv->client, GCONF_DIR "/document_type", NULL);
+ if (document_type) {
+ set_document_hint (ui, document_type);
+ g_free (document_type);
+ }
+
+ ui->priv->book_view = book_view_new ();
+ g_signal_connect (ui->priv->book_view, "page-selected", G_CALLBACK (page_selected_cb), ui);
+ g_signal_connect (ui->priv->book_view, "show-page", G_CALLBACK (show_page_cb), ui);
+ book_view_set_widgets (ui->priv->book_view,
+ ui->priv->preview_box,
+ ui->priv->preview_area,
+ ui->priv->preview_scroll,
+ GTK_WIDGET (gtk_builder_get_object (builder, "page_menu")));
+
+ /* Find default page details */
+ scan_direction = gconf_client_get_string(ui->priv->client, GCONF_DIR "/scan_direction", NULL);
+ ui->priv->default_page_orientation = TOP_TO_BOTTOM;
+ if (scan_direction) {
+ gint i;
+ for (i = 0; orientation_keys[i].key != NULL && strcmp (orientation_keys[i].key, scan_direction) != 0; i++);
+ if (orientation_keys[i].key != NULL)
+ ui->priv->default_page_orientation = orientation_keys[i].orientation;
+ g_free (scan_direction);
+ }
+ ui->priv->default_page_width = gconf_client_get_int (ui->priv->client, GCONF_DIR "/page_width", NULL);
+ if (ui->priv->default_page_width <= 0)
+ ui->priv->default_page_width = 595;
+ ui->priv->default_page_height = gconf_client_get_int (ui->priv->client, GCONF_DIR "/page_height", NULL);
+ if (ui->priv->default_page_height <= 0)
+ ui->priv->default_page_height = 842;
+ ui->priv->default_page_dpi = gconf_client_get_int (ui->priv->client, GCONF_DIR "/page_dpi", NULL);
+ if (ui->priv->default_page_dpi <= 0)
+ ui->priv->default_page_dpi = 72;
+
+ /* Restore window size */
+ ui->priv->window_width = gconf_client_get_int (ui->priv->client, GCONF_DIR "/window_width", NULL);
+ if (ui->priv->window_width <= 0)
+ ui->priv->window_width = 600;
+ ui->priv->window_height = gconf_client_get_int (ui->priv->client, GCONF_DIR "/window_height", NULL);
+ if (ui->priv->window_height <= 0)
+ ui->priv->window_height = 400;
+ g_debug ("Restoring window to %dx%d pixels", ui->priv->window_width, ui->priv->window_height);
+ gtk_window_set_default_size (GTK_WINDOW (ui->priv->window), ui->priv->window_width, ui->priv->window_height);
+ ui->priv->window_is_maximized = gconf_client_get_bool (ui->priv->client, GCONF_DIR "/window_is_maximized", NULL);
+ if (ui->priv->window_is_maximized) {
+ g_debug ("Restoring window to maximized");
+ gtk_window_maximize (GTK_WINDOW (ui->priv->window));
+ }
+
+ if (book_get_n_pages (ui->priv->book) == 0)
+ add_default_page (ui);
+ book_view_set_book (ui->priv->book_view, ui->priv->book);
+}
+
+
+SimpleScan *
+ui_new ()
+{
+ return g_object_new (SIMPLE_SCAN_TYPE, NULL);
+}
+
+
+Book *
+ui_get_book (SimpleScan *ui)
+{
+ return g_object_ref (ui->priv->book);
+}
+
+
+void
+ui_set_selected_page (SimpleScan *ui, Page *page)
+{
+ book_view_select_page (ui->priv->book_view, page);
+}
+
+
+Page *
+ui_get_selected_page (SimpleScan *ui)
+{
+ return book_view_get_selected (ui->priv->book_view);
+}
+
+
+void
+ui_set_scanning (SimpleScan *ui, gboolean scanning)
+{
+ ui->priv->scanning = scanning;
+ gtk_widget_set_sensitive (ui->priv->page_delete_menuitem, !scanning);
+ gtk_widget_set_sensitive (ui->priv->stop_menuitem, scanning);
+ gtk_widget_set_sensitive (ui->priv->stop_toolbutton, scanning);
+}
+
+
+void
+ui_show_error (SimpleScan *ui, const gchar *error_title, const gchar *error_text, gboolean change_scanner_hint)
+{
+ GtkWidget *dialog;
+
+ dialog = gtk_message_dialog_new (GTK_WINDOW (ui->priv->window),
+ GTK_DIALOG_MODAL,
+ GTK_MESSAGE_WARNING,
+ GTK_BUTTONS_NONE,
+ "%s", error_title);
+ if (change_scanner_hint)
+ gtk_dialog_add_button (GTK_DIALOG (dialog),
+ /* Button in error dialog to open prefereces dialog and change scanner */
+ _("Change _Scanner"),
+ 1);
+ gtk_dialog_add_button (GTK_DIALOG (dialog), GTK_STOCK_CLOSE, 0);
+ gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog),
+ "%s", error_text);
+
+ if (gtk_dialog_run (GTK_DIALOG (dialog)) == 1) {
+ gtk_widget_grab_focus (ui->priv->device_combo);
+ gtk_window_present (GTK_WINDOW (ui->priv->preferences_dialog));
+ }
+
+ gtk_widget_destroy (dialog);
+}
+
+
+void
+ui_start (SimpleScan *ui)
+{
+ gtk_widget_show (ui->priv->window);
+}
+
+
+/* Generated with glib-genmarshal */
+static void
+g_cclosure_user_marshal_VOID__STRING_POINTER (GClosure *closure,
+ GValue *return_value G_GNUC_UNUSED,
+ guint n_param_values,
+ const GValue *param_values,
+ gpointer invocation_hint G_GNUC_UNUSED,
+ gpointer marshal_data)
+{
+ typedef void (*GMarshalFunc_VOID__STRING_POINTER) (gpointer data1,
+ gconstpointer arg_1,
+ gconstpointer arg_2,
+ gpointer data2);
+ register GMarshalFunc_VOID__STRING_POINTER callback;
+ register GCClosure *cc = (GCClosure*) closure;
+ register gpointer data1, data2;
+
+ g_return_if_fail (n_param_values == 3);
+
+ if (G_CCLOSURE_SWAP_DATA (closure))
+ {
+ data1 = closure->data;
+ data2 = g_value_peek_pointer (param_values + 0);
+ }
+ else
+ {
+ data1 = g_value_peek_pointer (param_values + 0);
+ data2 = closure->data;
+ }
+ callback = (GMarshalFunc_VOID__STRING_POINTER) (marshal_data ? marshal_data : cc->callback);
+
+ callback (data1,
+ g_value_get_string (param_values + 1),
+ g_value_get_pointer (param_values + 2),
+ data2);
+}
+
+
+static void
+ui_finalize (GObject *object)
+{
+ SimpleScan *ui = SIMPLE_SCAN (object);
+
+ g_object_unref (ui->priv->client);
+ ui->priv->client = NULL;
+ g_object_unref (ui->priv->builder);
+ ui->priv->builder = NULL;
+ g_object_unref (ui->priv->book);
+ ui->priv->book = NULL;
+ g_object_unref (ui->priv->book_view);
+ ui->priv->book_view = NULL;
+
+ G_OBJECT_CLASS (ui_parent_class)->finalize (object);
+}
+
+
+static void
+ui_class_init (SimpleScanClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->finalize = ui_finalize;
+
+ signals[START_SCAN] =
+ g_signal_new ("start-scan",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (SimpleScanClass, start_scan),
+ NULL, NULL,
+ g_cclosure_user_marshal_VOID__STRING_POINTER,
+ G_TYPE_NONE, 2, G_TYPE_STRING, G_TYPE_POINTER);
+ signals[STOP_SCAN] =
+ g_signal_new ("stop-scan",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (SimpleScanClass, stop_scan),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE, 0);
+ signals[SAVE] =
+ g_signal_new ("save",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (SimpleScanClass, save),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__POINTER,
+ G_TYPE_NONE, 1, G_TYPE_POINTER);
+ signals[EMAIL] =
+ g_signal_new ("email",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (SimpleScanClass, email),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__STRING,
+ G_TYPE_NONE, 1, G_TYPE_STRING);
+ signals[QUIT] =
+ g_signal_new ("quit",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (SimpleScanClass, quit),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE, 0);
+
+ g_type_class_add_private (klass, sizeof (SimpleScanPrivate));
+}
+
+
+static void
+ui_init (SimpleScan *ui)
+{
+ ui->priv = G_TYPE_INSTANCE_GET_PRIVATE (ui, SIMPLE_SCAN_TYPE, SimpleScanPrivate);
+
+ ui->priv->book = book_new ();
+ g_signal_connect (ui->priv->book, "page-removed", G_CALLBACK (page_removed_cb), ui);
+ g_signal_connect (ui->priv->book, "page-added", G_CALLBACK (page_added_cb), ui);
+
+ ui->priv->client = gconf_client_get_default();
+ gconf_client_add_dir(ui->priv->client, GCONF_DIR, GCONF_CLIENT_PRELOAD_NONE, NULL);
+
+ ui->priv->document_hint = g_strdup ("photo");
+ ui->priv->default_file_name = g_strdup (_("Scanned Document.pdf"));
+ ui->priv->scanning = FALSE;
+ ui_load (ui);
+}
diff --git a/src/ui.h b/src/ui.h
new file mode 100644
index 0000000..41d551f
--- /dev/null
+++ b/src/ui.h
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2009 Canonical Ltd.
+ * Author: Robert Ancell <robert.ancell@canonical.com>
+ *
+ * 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. See http://www.gnu.org/copyleft/gpl.html the full text of the
+ * license.
+ */
+
+#ifndef _UI_H_
+#define _UI_H_
+
+#include <glib-object.h>
+#include "book.h"
+#include "scanner.h"
+
+G_BEGIN_DECLS
+
+#define SIMPLE_SCAN_TYPE (ui_get_type ())
+#define SIMPLE_SCAN(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), SIMPLE_SCAN_TYPE, SimpleScan))
+
+
+typedef struct SimpleScanPrivate SimpleScanPrivate;
+
+typedef struct
+{
+ GObject parent_instance;
+ SimpleScanPrivate *priv;
+} SimpleScan;
+
+typedef struct
+{
+ GObjectClass parent_class;
+
+ void (*start_scan) (SimpleScan *ui, ScanOptions *options);
+ void (*stop_scan) (SimpleScan *ui);
+ void (*save) (SimpleScan *ui, const gchar *format);
+ void (*email) (SimpleScan *ui, const gchar *profile);
+ void (*quit) (SimpleScan *ui);
+} SimpleScanClass;
+
+
+GType ui_get_type (void);
+
+SimpleScan *ui_new (void);
+
+Book *ui_get_book (SimpleScan *ui);
+
+void ui_set_selected_page (SimpleScan *ui, Page *page);
+
+Page *ui_get_selected_page (SimpleScan *ui);
+
+void ui_set_default_file_name (SimpleScan *ui, const gchar *default_file_name);
+
+void ui_authorize (SimpleScan *ui, const gchar *resource, gchar **username, gchar **password);
+
+void ui_set_scan_devices (SimpleScan *ui, GList *devices);
+
+gchar *ui_get_selected_device (SimpleScan *ui);
+
+void ui_set_selected_device (SimpleScan *ui, const gchar *device);
+
+void ui_set_scanning (SimpleScan *ui, gboolean scanning);
+
+void ui_show_error (SimpleScan *ui, const gchar *error_title, const gchar *error_text, gboolean change_scanner_hint);
+
+void ui_start (SimpleScan *ui);
+
+#endif /* _UI_H_ */