Index: /pykota/trunk/conf/pykota.conf.sample
===================================================================
--- /pykota/trunk/conf/pykota.conf.sample (revision 2631)
+++ /pykota/trunk/conf/pykota.conf.sample (revision 2635)
@@ -563,4 +563,21 @@
 # and uses pkpgcounter's code internally.
 accounter: software()
+
+# What is the "pre"-accounter used for precomputing the job's size.
+#
+# Supported values are :
+#
+#  preaccounter: software()       
+#  preaccounter: software(/path/to/your/script)
+#
+# NB : the preaccounter directive doesn't support hardware() for obvious reasons.
+# If unset, "software()" is assumed. If you use your own script, ensure that it
+# only prints the job's number of pages (or an estimation of it) on its standard output.
+#
+# This value can be set either globally or on a per printer basis
+# If both are defined, the printer option has priority.
+#
+preaccounter: software()
+
 
 # What should we do if the accounter's subprocess doesn't return
Index: /pykota/trunk/pykota/accounters/software.py
===================================================================
--- /pykota/trunk/pykota/accounters/software.py (revision 2622)
+++ /pykota/trunk/pykota/accounters/software.py (revision 2635)
@@ -30,56 +30,97 @@
     def computeJobSize(self) :    
         """Feeds an external command with our datas to let it compute the job size, and return its value."""
+        if (not self.isPreAccounter) and \
+            (self.filter.accounter.arguments == self.filter.preaccounter.arguments) :
+            # if precomputing has been done and both accounter and preaccounter are
+            # configured the same, no need to launch a second pass since we already
+            # know the result.
+            self.filter.logdebug("Precomputing pass told us that job is %s pages long." % self.filter.softwareJobSize)
+            return self.filter.softwareJobSize   # Optimize : already computed !
+            
+        if self.arguments :
+            self.filter.logdebug("Using external script %s to compute job's size." % self.arguments)
+            return self.withExternalScript()
+        else :    
+            self.filter.logdebug("Using internal parser to compute job's size.")
+            return self.withInternalParser()
+        
+    def withInternalParser(self) :    
+        """Does software accounting through an external script."""
+        jobsize = 0
+        if self.filter.JobSizeBytes :
+            try :
+                from pkpgpdls import analyzer, pdlparser
+            except ImportError :    
+                self.filter.printInfo("pkpgcounter is now distributed separately, please grab it from http://www.librelogiciel.com/software/pkpgcounter/action_Download", "error")
+                self.filter.printInfo("Precomputed job size will be forced to 0 pages.", "error")
+            else :     
+                infile = open(self.filter.DataFile, "rb")
+                try :
+                    parser = analyzer.PDLAnalyzer(infile)
+                    jobsize = parser.getJobSize()
+                except pdlparser.PDLParserError, msg :    
+                    # Here we just log the failure, but
+                    # we finally ignore it and return 0 since this
+                    # computation is just an indication of what the
+                    # job's size MAY be.
+                    self.filter.printInfo(_("Unable to precompute the job's size with the generic PDL analyzer : %s") % msg, "warn")
+                else :    
+                    if self.filter.InputFile is not None :
+                        # when a filename is passed as an argument, the backend 
+                        # must generate the correct number of copies.
+                        jobsize *= self.filter.Copies
+                infile.close()        
+        return jobsize        
+                
+    def withExternalScript(self) :    
+        """Does software accounting through an external script."""
         self.filter.printInfo(_("Launching SOFTWARE(%s)...") % self.arguments)
-        if not self.arguments :
-            pagecounter = self.filter.softwareJobSize   # Optimize : already computed !
-            self.filter.logdebug("Internal software accounter said job is %s pages long." % repr(pagecounter))
-        else :
-            MEGABYTE = 1024*1024
-            infile = open(self.filter.DataFile, "rb")
-            child = popen2.Popen4(self.arguments)
-            try :
-                data = infile.read(MEGABYTE)    
-                while data :
-                    child.tochild.write(data)
-                    data = infile.read(MEGABYTE)
-                child.tochild.flush()
-                child.tochild.close()    
-            except (IOError, OSError), msg :    
-                msg = "%s : %s" % (self.arguments, msg) 
-                self.filter.printInfo(_("Unable to compute job size with accounter %s") % msg)
-            infile.close()
-            pagecounter = None
-            try :
-                answer = child.fromchild.read()
-            except (IOError, OSError), msg :    
-                msg = "%s : %s" % (self.arguments, msg) 
-                self.filter.printInfo(_("Unable to compute job size with accounter %s") % msg)
-            else :    
-                lines = [l.strip() for l in answer.split("\n")]
-                for i in range(len(lines)) : 
-                    try :
-                        pagecounter = int(lines[i])
-                    except (AttributeError, ValueError) :
-                        self.filter.printInfo(_("Line [%s] skipped in accounter's output. Trying again...") % lines[i])
-                    else :    
-                        break
-            child.fromchild.close()
+        MEGABYTE = 1024*1024
+        infile = open(self.filter.DataFile, "rb")
+        child = popen2.Popen4(self.arguments)
+        try :
+            data = infile.read(MEGABYTE)    
+            while data :
+                child.tochild.write(data)
+                data = infile.read(MEGABYTE)
+            child.tochild.flush()
+            child.tochild.close()    
+        except (IOError, OSError), msg :    
+            msg = "%s : %s" % (self.arguments, msg) 
+            self.filter.printInfo(_("Unable to compute job size with accounter %s") % msg)
+        infile.close()
+        pagecounter = None
+        try :
+            answer = child.fromchild.read()
+        except (IOError, OSError), msg :    
+            msg = "%s : %s" % (self.arguments, msg) 
+            self.filter.printInfo(_("Unable to compute job size with accounter %s") % msg)
+        else :    
+            lines = [l.strip() for l in answer.split("\n")]
+            for i in range(len(lines)) : 
+                try :
+                    pagecounter = int(lines[i])
+                except (AttributeError, ValueError) :
+                    self.filter.printInfo(_("Line [%s] skipped in accounter's output. Trying again...") % lines[i])
+                else :    
+                    break
+        child.fromchild.close()
+        
+        try :
+            status = child.wait()
+        except OSError, msg :    
+            self.filter.printInfo(_("Problem while waiting for software accounter pid %s to exit : %s") % (child.pid, msg))
+        else :    
+            if os.WIFEXITED(status) :
+                status = os.WEXITSTATUS(status)
+            self.filter.printInfo(_("Software accounter %s exit code is %s") % (self.arguments, str(status)))
             
-            try :
-                status = child.wait()
-            except OSError, msg :    
-                self.filter.printInfo(_("Problem while waiting for software accounter pid %s to exit : %s") % (child.pid, msg))
-            else :    
-                if os.WIFEXITED(status) :
-                    status = os.WEXITSTATUS(status)
-                self.filter.printInfo(_("Software accounter %s exit code is %s") % (self.arguments, str(status)))
-                
-            if pagecounter is None :    
-                message = _("Unable to compute job size with accounter %s") % self.arguments
-                if self.onerror == "CONTINUE" :
-                    self.filter.printInfo(message, "error")
-                else :
-                    raise PyKotaAccounterError, message
-            self.filter.logdebug("Software accounter %s said job is %s pages long." % (self.arguments, repr(pagecounter)))
+        if pagecounter is None :    
+            message = _("Unable to compute job size with accounter %s") % self.arguments
+            if self.onerror == "CONTINUE" :
+                self.filter.printInfo(message, "error")
+            else :
+                raise PyKotaAccounterError, message
+        self.filter.logdebug("Software accounter %s said job is %s pages long." % (self.arguments, repr(pagecounter)))
             
         return pagecounter or 0
Index: /pykota/trunk/pykota/accounters/hardware.py
===================================================================
--- /pykota/trunk/pykota/accounters/hardware.py (revision 2622)
+++ /pykota/trunk/pykota/accounters/hardware.py (revision 2635)
@@ -31,5 +31,5 @@
 
 class Accounter(AccounterBase) :
-    def __init__(self, kotabackend, arguments) :
+    def __init__(self, kotabackend, arguments, ispreaccounter=0) :
         """Initializes querying accounter."""
         AccounterBase.__init__(self, kotabackend, arguments)
Index: /pykota/trunk/pykota/accounter.py
===================================================================
--- /pykota/trunk/pykota/accounter.py (revision 2622)
+++ /pykota/trunk/pykota/accounter.py (revision 2635)
@@ -36,5 +36,5 @@
 class AccounterBase :    
     """A class to account print usage by querying printers."""
-    def __init__(self, kotafilter, arguments) :
+    def __init__(self, kotafilter, arguments, ispreaccounter=0) :
         """Sets instance vars depending on the current printer."""
         self.filter = kotafilter
@@ -42,4 +42,5 @@
         self.onerror = self.filter.config.getPrinterOnAccounterError(self.filter.PrinterName)
         self.isSoftware = 1 # by default software accounting
+        self.isPreAccounter = ispreaccounter 
         
     def getLastPageCounter(self) :    
@@ -58,12 +59,14 @@
         
         # get last job information for this printer
-        if not printer.LastJob.Exists :
-            # The printer hasn't been used yet, from PyKota's point of view
-            self.LastPageCounter = 0
-        else :    
-            # get last job size and page counter from Quota Storage
-            # Last lifetime page counter before actual job is 
-            # last page counter + last job size
-            self.LastPageCounter = int(printer.LastJob.PrinterPageCounter or 0) + int(printer.LastJob.JobSize or 0)
+        if not self.isPreAccounter :
+            # TODO : check if this code is still needed
+            if not printer.LastJob.Exists :
+                # The printer hasn't been used yet, from PyKota's point of view
+                self.LastPageCounter = 0
+            else :    
+                # get last job size and page counter from Quota Storage
+                # Last lifetime page counter before actual job is 
+                # last page counter + last job size
+                self.LastPageCounter = int(printer.LastJob.PrinterPageCounter or 0) + int(printer.LastJob.JobSize or 0)
         
     def fakeBeginJob(self) :    
@@ -86,7 +89,10 @@
         raise RuntimeError, "AccounterBase.computeJobSize() must be overriden !"
         
-def openAccounter(kotafilter) :
+def openAccounter(kotafilter, ispreaccounter=0) :
     """Returns a connection handle to the appropriate accounter."""
-    (backend, args) = kotafilter.config.getAccounterBackend(kotafilter.PrinterName)
+    if ispreaccounter :
+        (backend, args) = kotafilter.config.getPreAccounterBackend(kotafilter.PrinterName)
+    else :
+        (backend, args) = kotafilter.config.getAccounterBackend(kotafilter.PrinterName)
     try :
         exec "from pykota.accounters import %s as accounterbackend" % backend.lower()
@@ -94,3 +100,3 @@
         raise PyKotaAccounterError, _("Unsupported accounter backend %s") % backend
     else :    
-        return accounterbackend.Accounter(kotafilter, args)
+        return accounterbackend.Accounter(kotafilter, args, ispreaccounter)
Index: /pykota/trunk/pykota/config.py
===================================================================
--- /pykota/trunk/pykota/config.py (revision 2622)
+++ /pykota/trunk/pykota/config.py (revision 2635)
@@ -190,10 +190,26 @@
         return url.strip()           
     
+    def getPreAccounterBackend(self, printername) :    
+        """Returns the preaccounter backend to use for a given printer."""
+        validaccounters = [ "software" ]     
+        try :
+            fullaccounter = self.getPrinterOption(printername, "preaccounter").strip()
+        except PyKotaConfigError :    
+            return ("software", "")
+        else :    
+            flower = fullaccounter.lower()
+            if flower.startswith("software") :    
+                try :
+                    (accounter, args) = [x.strip() for x in fullaccounter.split('(', 1)]
+                except ValueError :    
+                    raise PyKotaConfigError, _("Invalid preaccounter %s for printer %s") % (fullaccounter, printername)
+                if args.endswith(')') :
+                    args = args[:-1].strip()
+                return ("software", args)
+            else :
+                raise PyKotaConfigError, _("Option preaccounter in section %s only supports values in %s") % (printername, str(validaccounters))
+        
     def getAccounterBackend(self, printername) :    
-        """Returns the accounter backend to use for a given printer.
-        
-           if it is not set, it defaults to 'hardware' which means ask printer
-           for its internal lifetime page counter.
-        """   
+        """Returns the accounter backend to use for a given printer."""
         validaccounters = [ "hardware", "software" ]     
         fullaccounter = self.getPrinterOption(printername, "accounter").strip()
Index: /pykota/trunk/bin/cupspykota
===================================================================
--- /pykota/trunk/bin/cupspykota (revision 2632)
+++ /pykota/trunk/bin/cupspykota (revision 2635)
@@ -380,29 +380,7 @@
         """Computes the job size with a software method."""
         self.logdebug("Precomputing job's size...")
-        jobsize = 0
-        if self.JobSizeBytes :
-            try :
-                from pkpgpdls import analyzer, pdlparser
-            except ImportError :    
-                self.printInfo("pkpgcounter is now distributed separately, please grab it from http://www.librelogiciel.com/software/pkpgcounter/action_Download", "error")
-                self.printInfo("Precomputed job size will be forced to 0 pages.", "error")
-            else :     
-                infile = open(self.DataFile, "rb")
-                try :
-                    parser = analyzer.PDLAnalyzer(infile)
-                    jobsize = parser.getJobSize()
-                except pdlparser.PDLParserError, msg :    
-                    # Here we just log the failure, but
-                    # we finally ignore it and return 0 since this
-                    # computation is just an indication of what the
-                    # job's size MAY be.
-                    self.printInfo(_("Unable to precompute the job's size with the generic PDL analyzer : %s") % msg, "warn")
-                else :    
-                    if self.InputFile is not None :
-                        # when a filename is passed as an argument, the backend 
-                        # must generate the correct number of copies.
-                        jobsize *= self.Copies
-                infile.close()        
-        self.softwareJobSize = jobsize
+        self.preaccounter.beginJob(None)
+        self.preaccounter.endJob(None)
+        self.softwareJobSize = self.preaccounter.getJobSize(None)
         self.logdebug("Precomputed job's size is %s pages." % self.softwareJobSize)
         
@@ -1178,4 +1156,5 @@
             wrapper.initBackendParameters()
             wrapper.saveDatasAndCheckSum()
+            wrapper.preaccounter = openAccounter(wrapper, ispreaccounter=1)
             wrapper.accounter = openAccounter(wrapper)
             wrapper.precomputeJobSize()
Index: /pykota/trunk/NEWS
===================================================================
--- /pykota/trunk/NEWS (revision 2631)
+++ /pykota/trunk/NEWS (revision 2635)
@@ -24,4 +24,9 @@
     - 1.24alpha8 :
     
+        - The 'preaccounter' configuration directive was introduced to control
+          which parser to use for precomputation of the job's size. When unset,
+          the internal parser is used just like it was previously. See
+          conf/pykota.conf.sample for more details.
+          
         - Now subprocesses launched through the overwrite_jobticket directive
           can also output 'CANCEL', to represent the user's own choice to
