root / pkpgcounter / trunk / pkpgpdls / analyzer.py @ 413

Revision 413, 11.1 kB (checked in by jerome, 18 years ago)

Re-ordered parsers.

  • Property svn:keywords set to Auth Date Id Rev
Line 
1#
2# pkpgcounter : a generic Page Description Language parser
3#
4# (c) 2003, 2004, 2005, 2006 Jerome Alet <alet@librelogiciel.com>
5# This program is free software; you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation; either version 2 of the License, or
8# (at your option) any later version.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with this program; if not, write to the Free Software
17# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18#
19# $Id$
20#
21
22"""This is the main module of pkpgcounter.
23
24It defines the PDLAnalyzer class, which provides a generic way to parse
25input files, by automatically detecting the best parser to use."""
26
27import sys
28import os
29import tempfile
30
31import version, pdlparser, postscript, pdf, pcl345, pclxl, \
32       escp2, dvi, tiff, ooo, zjstream, qpdl, spl1, plain
33import inkcoverage
34
35class AnalyzerOptions :
36    """A class for use as the options parameter to PDLAnalyzer's constructor."""
37    def __init__(self, debug=None,
38                       colorspace=None,
39                       resolution=None) :
40        """Sets initial attributes."""
41        self.debug = debug
42        self.colorspace = colorspace
43        self.resolution = resolution
44   
45   
46class PDLAnalyzer :   
47    """Class for PDL autodetection."""
48    def __init__(self, filename, options=AnalyzerOptions()) :
49        """Initializes the PDL analyzer.
50       
51           filename is the name of the file or '-' for stdin.
52           filename can also be a file-like object which
53           supports read() and seek().
54        """
55        self.options = options
56        self.filename = filename
57        self.infile = None
58        self.mustclose = None
59       
60    def getJobSize(self) :   
61        """Returns the job's size."""
62        size = 0
63        self.openFile()
64        try :
65            try :
66                pdlhandler = self.detectPDLHandler()
67                size = pdlhandler.getJobSize()
68            except pdlparser.PDLParserError, msg :   
69                raise pdlparser.PDLParserError, "Unknown file format for %s (%s)" % (self.filename, msg)
70        finally :   
71            self.closeFile()
72        return size
73           
74    def getInkCoverage(self, colorspace=None, resolution=None) :
75        """Extracts the percents of ink coverage from the input file."""
76        result = None
77        cspace = colorspace or self.options.colorspace
78        res = resolution or self.options.resolution
79        if (not cspace) or (not res) :
80            raise ValueError, "Invalid colorspace (%s) or resolution (%s)" % (cspace, res)
81        self.openFile()
82        try :
83            try :
84                pdlhandler = self.detectPDLHandler()
85                tiffname = self.convertToTiffMultiPage24NC(pdlhandler)
86                result = inkcoverage.getInkCoverage(tiffname, cspace)
87                try :
88                    os.remove(tiffname)
89                except OSError :
90                    sys.stderr.write("Problem when trying to remove temporary file %s\n" % tiffname)
91            except pdlparser.PDLParserError, msg :   
92                raise pdlparser.PDLParserError, "Unknown file format for %s (%s)" % (self.filename, msg)
93        finally :   
94            self.closeFile()
95        return result
96       
97    def convertToTiffMultiPage24NC(self, handler) :   
98        """Converts the input file to TIFF format, X dpi, 24 bits per pixel, uncompressed.
99           Returns a temporary filename which names a file containing the TIFF datas.
100           The temporary file has to be deleted by the caller.
101        """   
102        self.infile.seek(0)
103        (handle, filename) = tempfile.mkstemp(".tmp", "pkpgcounter")   
104        os.close(handle)
105        handler.convertToTiffMultiPage24NC(filename, self.options.resolution)
106        return filename
107       
108    def openFile(self) :   
109        """Opens the job's data stream for reading."""
110        self.mustclose = 0  # by default we don't want to close the file when finished
111        if hasattr(self.filename, "read") and hasattr(self.filename, "seek") :
112            # filename is in fact a file-like object
113            infile = self.filename
114        elif self.filename == "-" :
115            # we must read from stdin
116            infile = sys.stdin
117        else :   
118            # normal file
119            self.infile = open(self.filename, "rb")
120            self.mustclose = 1
121            return
122           
123        # Use a temporary file, always seekable contrary to standard input.
124        self.infile = tempfile.TemporaryFile(mode="w+b")
125        while 1 :
126            data = infile.read(pdlparser.MEGABYTE) 
127            if not data :
128                break
129            self.infile.write(data)
130        self.infile.flush()   
131        self.infile.seek(0)
132           
133    def closeFile(self) :       
134        """Closes the job's data stream if we can close it."""
135        if self.mustclose :
136            self.infile.close()   
137        else :   
138            # if we don't have to close the file, then
139            # ensure the file pointer is reset to the
140            # start of the file in case the process wants
141            # to read the file again.
142            try :
143                self.infile.seek(0)
144            except IOError :   
145                pass    # probably stdin, which is not seekable
146       
147    def detectPDLHandler(self) :   
148        """Tries to autodetect the document format.
149       
150           Returns the correct PDL handler class or None if format is unknown
151        """   
152        # Try to detect file type by reading first and last blocks of datas   
153        # Each parser can read them automatically, but here we do this only once.
154        self.infile.seek(0)
155        firstblock = self.infile.read(pdlparser.FIRSTBLOCKSIZE)
156        try :
157            self.infile.seek(-pdlparser.LASTBLOCKSIZE, 2)
158            lastblock = self.infile.read(pdlparser.LASTBLOCKSIZE)
159        except IOError :   
160            lastblock = ""
161        self.infile.seek(0)
162        if not firstblock :
163            raise pdlparser.PDLParserError, "input file %s is empty !" % str(self.filename)
164        else :   
165            # IMPORTANT : the order is important below. FIXME.
166            for module in (postscript, \
167                           pclxl, \
168                           pdf, \
169                           qpdl, \
170                           spl1, \
171                           dvi, \
172                           tiff, \
173                           zjstream, \
174                           ooo, \
175                           pcl345, \    # TODO : this one could be confused with other ones
176                           escp2, \     # TODO : this one could be confused with other ones.
177                           plain) :     # IMPORTANT : don't move this one up !
178                try :               
179                    return module.Parser(self.infile, self.options.debug, firstblock, lastblock)
180                except pdlparser.PDLParserError :
181                    pass # try next parser
182        raise pdlparser.PDLParserError, "Analysis of first data block failed."
183           
184def main() :   
185    """Entry point for PDL Analyzer."""
186    import optparse
187    from copy import copy
188   
189    def check_cichoice(option, opt, value) :
190        """To add a CaseIgnore Choice option type."""
191        valower = value.lower()
192        if valower in [v.lower() for v in option.cichoices] :
193            return valower
194        else :   
195            choices = ", ".join([repr(o) for o in option.cichoices])
196            raise optparse.OptionValueError(
197                "option %s: invalid choice: %r (choose from %s)"
198                % (opt, value, choices))
199   
200    class MyOption(optparse.Option) :
201        """New Option class, with CaseIgnore Choice type."""
202        TYPES = optparse.Option.TYPES + ("cichoice",)
203        ATTRS = optparse.Option.ATTRS + ["cichoices"]
204        TYPE_CHECKER = copy(optparse.Option.TYPE_CHECKER)
205        TYPE_CHECKER["cichoice"] = check_cichoice
206       
207    parser = optparse.OptionParser(option_class=MyOption, 
208                                   usage="python analyzer.py [options] file1 [file2 ...]")
209    parser.add_option("-v", "--version", 
210                            action="store_true", 
211                            dest="version",
212                            help="Show pkpgcounter's version number and exit.")
213    parser.add_option("-d", "--debug", 
214                            action="store_true", 
215                            dest="debug",
216                            help="Activate debug mode.")
217    parser.add_option("-c", "--colorspace", 
218                            dest="colorspace",
219                            type="cichoice",
220                            cichoices=["bw", "rgb", "cmyk", "cmy"],
221                            help="Activate the computation of ink usage, and defines the colorspace to use. Supported values are 'BW', 'RGB', 'CMYK', and 'CMY'.")
222    parser.add_option("-r", "--resolution", 
223                            type="int", 
224                            default=72, 
225                            dest="resolution",
226                            help="The resolution in DPI to use when checking ink usage. Lower resolution is faster but less accurate. Default is 72 dpi.")
227    (options, arguments) = parser.parse_args()
228    if options.version :
229        print "%s" % version.__version__
230    elif not (72 <= options.resolution <= 1200) :   
231        sys.stderr.write("ERROR: the argument to the --resolution command line option must be between 72 and 1200.\n")
232        sys.stderr.flush()
233    else :
234        if (not arguments) or ((not sys.stdin.isatty()) and ("-" not in arguments)) :
235            arguments.append("-")
236        totalsize = 0   
237        lines = []
238        try :
239            for arg in arguments :
240                try :
241                    parser = PDLAnalyzer(arg, options)
242                    if not options.colorspace :
243                        totalsize += parser.getJobSize()
244                    else :
245                        (cspace, pages) = parser.getInkCoverage()
246                        for page in pages :
247                            lineparts = []
248                            for k in cspace : # NB : this way we preserve the order of the planes
249                                try :
250                                    lineparts.append("%s : %s%%" % (k, ("%f" % page[k]).rjust(10)))
251                                except KeyError :
252                                    pass
253                            lines.append("      ".join(lineparts))     
254                except (IOError, pdlparser.PDLParserError), msg :   
255                    sys.stderr.write("ERROR: %s\n" % msg)
256                    sys.stderr.flush()
257        except KeyboardInterrupt :           
258            sys.stderr.write("WARN: Aborted at user's request.\n")
259            sys.stderr.flush()
260        if not options.colorspace :   
261            print "%s" % totalsize
262        else :   
263            print "\n".join(lines)
264   
265if __name__ == "__main__" :   
266    main()
Note: See TracBrowser for help on using the browser.