diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/Makefile.am | 35 | ||||
-rw-r--r-- | src/Makefile.in | 661 | ||||
-rw-r--r-- | src/book-view.c | 743 | ||||
-rw-r--r-- | src/book-view.h | 65 | ||||
-rw-r--r-- | src/book.c | 466 | ||||
-rw-r--r-- | src/book.h | 60 | ||||
-rw-r--r-- | src/page-view.c | 1170 | ||||
-rw-r--r-- | src/page-view.h | 79 | ||||
-rw-r--r-- | src/page.c | 951 | ||||
-rw-r--r-- | src/page.h | 121 | ||||
-rw-r--r-- | src/scanner.c | 1630 | ||||
-rw-r--r-- | src/scanner.h | 134 | ||||
-rw-r--r-- | src/simple-scan.c | 633 | ||||
-rw-r--r-- | src/ui.c | 1648 | ||||
-rw-r--r-- | src/ui.h | 71 |
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_ */ |