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

Revision 485, 10.8 kB (checked in by jerome, 16 years ago)

Almost ok for release.

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