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

Revision 3460, 10.7 kB (checked in by jerome, 14 years ago)

Ensures the temporary files are always closed (they should be anyway,
but it seems it's not the case for some people...)

  • Property svn:keywords set to Auth Date Id Rev
Line 
1# -*- coding: utf-8 -*-
2#
3# pkpgcounter : a generic Page Description Language parser
4#
5# (c) 2003, 2004, 2005, 2006, 2007, 2008 Jerome Alet <alet@librelogiciel.com>
6# This program is free software: you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation, either version 3 of the License, or
9# (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program.  If not, see <http://www.gnu.org/licenses/>.
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, hbp, \
32       pil, mscrap, cfax, lidil, escp2, dvi, tiff, ooo, zjstream, \
33       pnmascii, bj, qpdl, spl1, escpages03, plain
34import inkcoverage
35
36class AnalyzerOptions :
37    """A class for use as the options parameter to PDLAnalyzer's constructor."""
38    def __init__(self, debug=None,
39                       colorspace=None,
40                       resolution=None) :
41        """Sets initial attributes."""
42        self.debug = debug
43        self.colorspace = colorspace
44        self.resolution = resolution
45
46
47class PDLAnalyzer :
48    """Class for PDL autodetection."""
49    def __init__(self, filename, options=AnalyzerOptions()) :
50        """Initializes the PDL analyzer.
51
52           filename is the name of the file or '-' for stdin.
53           filename can also be a file-like object which
54           supports read() and seek().
55        """
56        self.options = options
57        self.filename = filename
58        self.workfile = 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, "Unsupported 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                dummyfile = tempfile.NamedTemporaryFile(mode="w+b",
86                                                        prefix="pkpgcounter_",
87                                                        suffix=".tiff",
88                                                        dir=os.environ.get("PYKOTADIRECTORY") or tempfile.gettempdir())
89                filename = dummyfile.name
90                try :
91                    pdlhandler.convertToTiffMultiPage24NC(filename, self.options.resolution)
92                    result = inkcoverage.getInkCoverage(filename, cspace)
93                finally :
94                    dummyfile.close()
95            except pdlparser.PDLParserError, msg :
96                raise pdlparser.PDLParserError, "Unsupported file format for %s (%s)" % (self.filename, msg)
97        finally :
98            self.closeFile()
99        return result
100
101    def openFile(self) :
102        """Opens the job's data stream for reading."""
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.workfile = open(self.filename, "rb")
112            return
113
114        # Use a temporary file, always seekable contrary to standard input.
115        self.workfile = tempfile.NamedTemporaryFile(mode="w+b",
116                                                    prefix="pkpgcounter_",
117                                                    suffix=".prn",
118                                                    dir=os.environ.get("PYKOTADIRECTORY") or tempfile.gettempdir())
119        self.filename = self.workfile.name
120        while True :
121            data = infile.read(pdlparser.MEGABYTE)
122            if not data :
123                break
124            self.workfile.write(data)
125        self.workfile.flush()
126        self.workfile.seek(0)
127
128    def closeFile(self) :
129        """Closes the job's data stream if we have to."""
130        self.workfile.close()
131
132    def readFirstAndLastBlocks(self, inputfile) :
133        """Reads the first and last blocks of data."""
134        # Now read first and last block of the input file
135        # to be able to detect the real file format and the parser to use.
136        firstblock = inputfile.read(pdlparser.FIRSTBLOCKSIZE)
137        try :
138            inputfile.seek(-pdlparser.LASTBLOCKSIZE, 2)
139            lastblock = inputfile.read(pdlparser.LASTBLOCKSIZE)
140        except IOError :
141            lastblock = ""
142        return (firstblock, lastblock)
143
144    def detectPDLHandler(self) :
145        """Tries to autodetect the document format.
146
147           Returns the correct PDL handler class or None if format is unknown
148        """
149        if not os.stat(self.filename).st_size :
150            raise pdlparser.PDLParserError, "input file %s is empty !" % str(self.filename)
151        (firstblock, lastblock) = self.readFirstAndLastBlocks(self.workfile)
152
153        # IMPORTANT : the order is important below. FIXME.
154        for module in (postscript, \
155                       pclxl, \
156                       pdf, \
157                       qpdl, \
158                       spl1, \
159                       dvi, \
160                       tiff, \
161                       cfax, \
162                       zjstream, \
163                       ooo, \
164                       hbp, \
165                       lidil, \
166                       pcl345, \
167                       escp2, \
168                       escpages03, \
169                       bj, \
170                       pnmascii, \
171                       pil, \
172                       mscrap, \
173                       plain) :     # IMPORTANT : don't move this one up !
174            try :
175                return module.Parser(self, self.filename,
176                                           (firstblock, lastblock))
177            except pdlparser.PDLParserError :
178                pass # try next parser
179        raise pdlparser.PDLParserError, "Analysis of first data block failed."
180
181def main() :
182    """Entry point for PDL Analyzer."""
183    import optparse
184    from copy import copy
185
186    def check_cichoice(option, opt, value) :
187        """To add a CaseIgnore Choice option type."""
188        valower = value.lower()
189        if valower in [v.lower() for v in option.cichoices] :
190            return valower
191        else :
192            choices = ", ".join([repr(o) for o in option.cichoices])
193            raise optparse.OptionValueError(
194                "option %s: invalid choice: %r (choose from %s)"
195                % (opt, value, choices))
196
197    class MyOption(optparse.Option) :
198        """New Option class, with CaseIgnore Choice type."""
199        TYPES = optparse.Option.TYPES + ("cichoice",)
200        ATTRS = optparse.Option.ATTRS + ["cichoices"]
201        TYPE_CHECKER = copy(optparse.Option.TYPE_CHECKER)
202        TYPE_CHECKER["cichoice"] = check_cichoice
203
204    parser = optparse.OptionParser(option_class=MyOption,
205                                   usage="python analyzer.py [options] file1 [file2 ...]")
206    parser.add_option("-v", "--version",
207                            action="store_true",
208                            dest="version",
209                            help="Show pkpgcounter's version number and exit.")
210    parser.add_option("-d", "--debug",
211                            action="store_true",
212                            dest="debug",
213                            help="Activate debug mode.")
214    parser.add_option("-c", "--colorspace",
215                            dest="colorspace",
216                            type="cichoice",
217                            cichoices=["bw", "rgb", "cmyk", "cmy", "gc"],
218                            help="Activate the computation of ink usage, and defines the colorspace to use. Supported values are 'BW', 'RGB', 'CMYK', 'CMY', and 'GC'.")
219    parser.add_option("-r", "--resolution",
220                            type="int",
221                            default=72,
222                            dest="resolution",
223                            help="The resolution in DPI to use when checking ink usage. Lower resolution is faster but less accurate. Default is 72 dpi.")
224    (options, arguments) = parser.parse_args()
225    if options.version :
226        sys.stdout.write("%s\n" % version.__version__)
227    elif not (72 <= options.resolution <= 1200) :
228        sys.stderr.write("ERROR: the argument to the --resolution command line option must be between 72 and 1200.\n")
229        sys.stderr.flush()
230    else :
231        if (not arguments) or ((not sys.stdin.isatty()) and ("-" not in arguments)) :
232            arguments.append("-")
233        totalsize = 0
234        lines = []
235        try :
236            for arg in arguments :
237                try :
238                    parser = PDLAnalyzer(arg, options)
239                    if not options.colorspace :
240                        totalsize += parser.getJobSize()
241                    else :
242                        (cspace, pages) = parser.getInkCoverage()
243                        for page in pages :
244                            lineparts = []
245                            for k in cspace : # NB : this way we preserve the order of the planes
246                                try :
247                                    lineparts.append("%s : %s%%" % (k, ("%f" % page[k]).rjust(10)))
248                                except KeyError :
249                                    pass
250                            lines.append("      ".join(lineparts))
251                except (IOError, pdlparser.PDLParserError), msg :
252                    sys.stderr.write("ERROR: %s\n" % msg)
253                    sys.stderr.flush()
254        except KeyboardInterrupt :
255            sys.stderr.write("WARN: Aborted at user's request.\n")
256            sys.stderr.flush()
257        if not options.colorspace :
258            sys.stdout.write("%i\n" % totalsize)
259        else :
260            sys.stdout.write("%s\n" % ("\n".join(lines)))
261
262if __name__ == "__main__" :
263    main()
Note: See TracBrowser for help on using the browser.