root / pykota / trunk / pykota / pdlanalyzer.py @ 1544

Revision 1544, 18.0 kB (checked in by jalet, 20 years ago)

Resets file pointer to start of file in all cases

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
Line 
1# PyKota
2# -*- coding: ISO-8859-15 -*-
3#
4# PyKota - Print Quotas for CUPS and LPRng
5#
6# (c) 2003-2004 Jerome Alet <alet@librelogiciel.com>
7# This program is free software; you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation; either version 2 of the License, or
10# (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program; if not, write to the Free Software
19# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
20#
21# $Id$
22#
23# $Log$
24# Revision 1.5  2004/06/18 10:09:05  jalet
25# Resets file pointer to start of file in all cases
26#
27# Revision 1.4  2004/06/18 06:16:14  jalet
28# Fixes PostScript detection code for incorrect drivers
29#
30# Revision 1.3  2004/05/21 20:40:08  jalet
31# All the code for pkpgcounter is now in pdlanalyzer.py
32#
33# Revision 1.2  2004/05/19 19:09:36  jalet
34# Speed improvement
35#
36# Revision 1.1  2004/05/18 09:59:54  jalet
37# pkpgcounter is now just a wrapper around the PDLAnalyzer class
38#
39#
40#
41
42import sys
43import struct
44import tempfile
45   
46KILOBYTE = 1024   
47MEGABYTE = 1024 * KILOBYTE   
48
49class PDLAnalyzerError(Exception):
50    """An exception for PDL Analyzer related stuff."""
51    def __init__(self, message = ""):
52        self.message = message
53        Exception.__init__(self, message)
54    def __repr__(self):
55        return self.message
56    __str__ = __repr__
57   
58class PostScriptAnalyzer :
59    def __init__(self, infile) :
60        """Initialize PostScript Analyzer."""
61        self.infile = infile
62       
63    def getJobSize(self) :   
64        """Count pages in a DSC compliant PostScript document."""
65        pagecount = 0
66        while 1 :
67            line = self.infile.readline()
68            if not line :
69                break
70            if line.startswith("%%Page: ") :
71                pagecount += 1
72        return pagecount
73       
74class PCLAnalyzer :
75    def __init__(self, infile) :
76        """Initialize PCL Analyzer."""
77        self.infile = infile
78       
79    def skip(self, nb) :   
80        """Reads a new datablock."""
81        newpos = self.pos + nb
82        if newpos >= self.len :
83            oldlen = self.len
84            self.data = self.infile.read(MEGABYTE)
85            self.len = len(self.data)
86            if not self.len :
87                return
88            self.pos = newpos - oldlen
89        else :   
90            self.pos = newpos
91       
92    def readone(self) :
93        """Reads a new byte."""
94        if self.pos < self.len :
95            char = self.data[self.pos]
96        else :   
97            self.data = self.infile.read(MEGABYTE)
98            self.len = len(self.data)
99            self.pos = 0
100            if not self.len :   
101                return
102            char = self.data[0]
103        self.pos += 1   
104        return char
105       
106    def getJobSize(self) :     
107        """Count pages in a PCL5 document."""
108        #
109        # Algorithm from pclcount
110        # (c) 2003, by Eduardo Gielamo Oliveira & Rodolfo Broco Manin
111        # published under the terms of the GNU General Public Licence v2.
112        #
113        # Backported from C to Python by Jerome Alet, then enhanced
114        # with more PCL tags detected. I think all the necessary PCL tags
115        # are recognized to correctly handle PCL5 files wrt their number
116        # of pages. The documentation used for this was :
117        #
118        # HP PCL/PJL Reference Set
119        # PCL5 Printer Language Technical Quick Reference Guide
120        # http://h20000.www2.hp.com/bc/docs/support/SupportManual/bpl13205/bpl13205.pdf
121        #
122        tagsends = { "&n" : "W", 
123                     "&b" : "W", 
124                     "*i" : "W", 
125                     "*l" : "W", 
126                     "*m" : "W", 
127                     "*v" : "W", 
128                     "*c" : "W", 
129                     "(f" : "W", 
130                     "*b" : "VW",
131                     "(s" : "W", 
132                     ")s" : "W", 
133                     "&p" : "X", 
134                     "&l" : "X" } 
135        self.data = []             
136        self.pos = self.len = 0
137        copies = 1
138        pagecount = resets = 0
139        tag = None
140        while 1 :
141            char = self.readone()
142            if not char :       # EOF ?
143                break   
144            if char == "\014" :   
145                pagecount += 1
146            elif char == "\033" :   
147                #
148                #     <ESC>*b###W -> Start of a raster data row/block
149                #     <ESC>*b###V -> Start of a raster data plane
150                #     <ESC>*c###W -> Start of a user defined pattern
151                #     <ESC>*i###W -> Start of a viewing illuminant block
152                #     <ESC>*l###W -> Start of a color lookup table
153                #     <ESC>*m###W -> Start of a download dither matrix block
154                #     <ESC>*v###W -> Start of a configure image data block
155                #     <ESC>(s###W -> Start of a characters description block
156                #     <ESC>)s###W -> Start of a fonts description block
157                #     <ESC>(f###W -> Start of a symbol set block
158                #     <ESC>&b###W -> Start of configuration data block
159                #     <ESC>&l###X -> Number of copies
160                #     <ESC>&n###W -> Starts an alphanumeric string ID block
161                #     <ESC>&p###X -> Start of a non printable characters block
162                #
163                tagstart = self.readone()
164                if tagstart in "E9=YZ" : # one byte PCL tag
165                    if tagstart == "E" :
166                        resets += 1
167                    continue             # skip to next tag
168                tag = tagstart + self.readone()
169                try :
170                    tagend = tagsends[tag]
171                except KeyError :   
172                    pass    # Unsupported PCL tag
173                else :   
174                    # Now read the numeric argument
175                    size = 0
176                    while 1 :
177                        char = self.readone()
178                        if not char.isdigit() :
179                            break
180                        size = (size * 10) + int(char)   
181                    if char in tagend :   
182                        if tag == "&l" :
183                            copies = size
184                        else :   
185                            # doing a read will prevent the seek
186                            # for unseekable streams.
187                            # we just ignore the block anyway.
188                            if tag == "&n" : 
189                                # we have to take care of the operation id byte
190                                # which is before the string itself
191                                size += 1
192                            self.skip(size)
193                           
194        # if pagecount is still 0, we will return the number
195        # of resets instead of the number of form feed characters.
196        # but the number of resets is always at least 2 with a valid
197        # pcl file : one at the very start and one at the very end
198        # of the job's data. So we substract 2 from the number of
199        # resets. And since on our test data we needed to substract
200        # 1 more, we finally substract 3, and will test several
201        # PCL files with this. If resets < 2, then the file is
202        # probably not a valid PCL file, so we return 0
203        return copies * (pagecount or ((resets - 3) * (resets > 2)))
204       
205class PCLXLAnalyzer :
206    def __init__(self, infile) :
207        """Initialize PCLXL Analyzer."""
208        raise PDLAnalyzerError, "PCLXL (aka PCL6) is not supported yet."
209        self.infile = infile
210        self.islittleendian = None
211        found = 0
212        while not found :
213            line = self.infile.readline()
214            if not line :
215                break
216            if line[1:12] == " HP-PCL XL;" :
217                found = 1
218                if line[0] == ")" :
219                    self.littleendian()
220                elif line[0] == "(" :   
221                    self.bigendian()
222        if not found :
223            raise PDLAnalyzerError, "This file doesn't seem to be PCLXL (aka PCL6)"
224        else :   
225            self.tags = [lambda: None] * 256   
226            self.tags[0x28] = self.bigendian    # big endian
227            self.tags[0x29] = self.littleendian # big endian
228            self.tags[0x43] = self.beginPage    # BeginPage
229            self.tags[0x44] = self.endPage      # EndPage
230           
231            self.tags[0xc0] = lambda: 1 # ubyte
232            self.tags[0xc1] = lambda: 2 # uint16
233            self.tags[0xc2] = lambda: 4 # uint32
234            self.tags[0xc3] = lambda: 2 # sint16
235            self.tags[0xc4] = lambda: 4 # sint32
236            self.tags[0xc5] = lambda: 4 # real32
237           
238            self.tags[0xc8] = self.array_8  # ubyte_array
239            self.tags[0xc9] = self.array_16 # uint16_array
240            self.tags[0xca] = self.array_32 # uint32_array
241            self.tags[0xcb] = self.array_16 # sint16_array
242            self.tags[0xcc] = self.array_32 # sint32_array
243            self.tags[0xcd] = self.array_32 # real32_array
244           
245            self.tags[0xd0] = lambda: 2 # ubyte_xy
246            self.tags[0xd1] = lambda: 4 # uint16_xy
247            self.tags[0xd2] = lambda: 8 # uint32_xy
248            self.tags[0xd3] = lambda: 4 # sint16_xy
249            self.tags[0xd4] = lambda: 8 # sint32_xy
250            self.tags[0xd5] = lambda: 8 # real32_xy
251           
252            self.tags[0xd0] = lambda: 4  # ubyte_box
253            self.tags[0xd1] = lambda: 8  # uint16_box
254            self.tags[0xd2] = lambda: 16 # uint32_box
255            self.tags[0xd3] = lambda: 8  # sint16_box
256            self.tags[0xd4] = lambda: 16 # sint32_box
257            self.tags[0xd5] = lambda: 16 # real32_box
258           
259            self.tags[0xf8] = lambda: 1 # attr_ubyte
260            self.tags[0xf9] = lambda: 2 # attr_uint16
261           
262            self.tags[0xfa] = self.embeddedData      # dataLength
263            self.tags[0xfb] = self.embeddedDataSmall # dataLengthByte
264           
265    def debug(self, msg) :
266        """Outputs a debug message on stderr."""
267        sys.stderr.write("%s\n" % msg)
268        sys.stderr.flush()
269       
270    def beginPage(self) :
271        """Indicates the beginning of a new page."""
272        self.pagecount += 1
273        self.debug("Begin page %i at %s" % (self.pagecount, self.infile.tell()))
274       
275    def endPage(self) :
276        """Indicates the end of a page."""
277        self.debug("End page %i at %s" % (self.pagecount, self.infile.tell()))
278       
279    def handleArray(self, itemsize) :       
280        """Handles arrays."""
281        pos = self.infile.tell()
282        datatype = self.infile.read(1)
283        length = self.tags[ord(datatype)]()
284        if length is None :
285            self.debug("Bogus array length at %s" % pos)
286        else :   
287            sarraysize = self.infile.read(length)
288            if self.islittleendian :
289                fmt = "<"
290            else :   
291                fmt = ">"
292            if length == 1 :   
293                fmt += "B"
294            elif length == 2 :   
295                fmt += "H"
296            elif length == 4 :   
297                fmt += "I"
298            else :   
299                raise PDLAnalyzerError, "Error on array size at %s" % self.infile.tell()
300            arraysize = struct.unpack(fmt, sarraysize)[0]
301            self.debug("Array at %s, itemsize %s, datatype 0x%02x, size %s" % (pos, itemsize, ord(datatype), arraysize))
302            return arraysize * itemsize
303       
304    def array_8(self) :   
305        """Handles byte arrays."""
306        return self.handleArray(1)
307       
308    def array_16(self) :   
309        """Handles byte arrays."""
310        return self.handleArray(2)
311       
312    def array_32(self) :   
313        """Handles byte arrays."""
314        return self.handleArray(4)
315       
316    def embeddedDataSmall(self) :
317        """Handle small amounts of data."""
318        pos = self.infile.tell()
319        val = ord(self.infile.read(1))
320        self.debug("smalldatablock at %s (0x%02x)" % (pos, val))
321        return val
322       
323    def embeddedData(self) :
324        """Handle normal amounts of data."""
325        if self.islittleendian :
326            fmt = "<I"
327        else :   
328            fmt = ">I"
329        pos = self.infile.tell()
330        val = struct.unpack(fmt, self.infile.read(4))[0]
331        self.debug("datablock at %s (0x%08x)" % (pos, val))
332        return val
333       
334    def littleendian(self) :       
335        """Toggles to little endianness."""
336        self.islittleendian = 1 # little endian
337       
338    def bigendian(self) :   
339        """Toggles to big endianness."""
340        self.islittleendian = 0 # big endian
341   
342    def getJobSize(self) :
343        """Counts pages in a PCLXL (PCL6) document."""
344        self.pagecount = 0
345        while 1 :
346            char = self.infile.read(1)
347            if not char :
348                break
349            index = ord(char)   
350            length = self.tags[index]()
351            if length :   
352                self.infile.read(length)   
353        return self.pagecount
354       
355class PDLAnalyzer :   
356    """Generic PDL Analyzer class."""
357    def __init__(self, filename) :
358        """Initializes the PDL analyzer.
359       
360           filename is the name of the file or '-' for stdin.
361           filename can also be a file-like object which
362           supports read() and seek().
363        """
364        self.filename = filename
365       
366    def getJobSize(self) :   
367        """Returns the job's size."""
368        self.openFile()
369        try :
370            pdlhandler = self.detectPDLHandler()
371        except PDLAnalyzerError, msg :   
372            self.closeFile()
373            raise PDLAnalyzerError, "ERROR : Unknown file format for %s (%s)" % (self.filename, msg)
374        else :
375            try :
376                size = pdlhandler(self.infile).getJobSize()
377            finally :   
378                self.closeFile()
379            return size
380       
381    def openFile(self) :   
382        """Opens the job's data stream for reading."""
383        self.mustclose = 1
384        if hasattr(self.filename, "read") and hasattr(self.filename, "seek") :
385            # filename is in fact a file-like object
386            self.infile = self.filename
387            self.mustclose = 0  # we don't want to close this file when finished
388        elif self.filename == "-" :
389            # we must read from stdin
390            # but since stdin is not seekable, we have to use a temporary
391            # file instead.
392            self.infile = tempfile.TemporaryFile()
393            while 1 :
394                data = sys.stdin.read(MEGABYTE) 
395                if not data :
396                    break
397                self.infile.write(data)
398            self.infile.flush()   
399            self.infile.seek(0)
400        else :   
401            # normal file
402            self.infile = open(self.filename, "rb")
403           
404    def closeFile(self) :       
405        """Closes the job's data stream if we can close it."""
406        if self.mustclose :
407            self.infile.close()   
408        else :   
409            # if we don't have to close the file, then
410            # ensure the file pointer is reset to the
411            # start of the file in case the process wants
412            # to read the file again.
413            try :
414                self.infile.seek(0)
415            except :   
416                pass    # probably stdin, which is not seekable
417       
418    def isPostScript(self, data) :   
419        """Returns 1 if data is PostScript, else 0."""
420        if data.startswith("%!") or \
421           data.startswith("\004%!") or \
422           data.startswith("\033%-12345X%!PS") or \
423           ((data[:128].find("\033%-12345X") != -1) and \
424             ((data.find("LANGUAGE=POSTSCRIPT") != -1) or \
425              (data.find("LANGUAGE = POSTSCRIPT") != -1) or \
426              (data.find("LANGUAGE = Postscript") != -1))) or \
427              (data.find("%!PS-Adobe") != -1) :
428            return 1
429        else :   
430            return 0
431       
432    def isPCL(self, data) :   
433        """Returns 1 if data is PCL, else 0."""
434        if data.startswith("\033E\033") or \
435           ((data[:128].find("\033%-12345X") != -1) and \
436             ((data.find("LANGUAGE=PCL") != -1) or \
437              (data.find("LANGUAGE = PCL") != -1) or \
438              (data.find("LANGUAGE = Pcl") != -1))) :
439            return 1
440        else :   
441            return 0
442       
443    def isPCLXL(self, data) :   
444        """Returns 1 if data is PCLXL aka PCL6, else 0."""
445        if ((data[:128].find("\033%-12345X") != -1) and \
446             (data.find(" HP-PCL XL;") != -1) and \
447             ((data.find("LANGUAGE=PCLXL") != -1) or \
448              (data.find("LANGUAGE = PCLXL") != -1))) :
449            return 1
450        else :   
451            return 0
452           
453    def detectPDLHandler(self) :   
454        """Tries to autodetect the document format.
455       
456           Returns the correct PDL handler class or None if format is unknown
457        """   
458        # Try to detect file type by reading first block of datas   
459        self.infile.seek(0)
460        firstblock = self.infile.read(KILOBYTE)
461        self.infile.seek(0)
462        if self.isPostScript(firstblock) :
463            return PostScriptAnalyzer
464        elif self.isPCLXL(firstblock) :   
465            return PCLXLAnalyzer
466        elif self.isPCL(firstblock) :   
467            return PCLAnalyzer
468        else :   
469            raise PDLAnalyzerError, "Analysis of first data block failed."
470           
471def main() :   
472    """Entry point for PDL Analyzer."""
473    if (len(sys.argv) < 2) or ((not sys.stdin.isatty()) and ("-" not in sys.argv[1:])) :
474        sys.argv.append("-")
475       
476    totalsize = 0   
477    for arg in sys.argv[1:] :
478        try :
479            parser = PDLAnalyzer(arg)
480            totalsize += parser.getJobSize()
481        except PDLAnalyzerError, msg :   
482            sys.stderr.write("%s\n" % msg)
483            sys.stderr.flush()
484    print "%s" % totalsize
485   
486if __name__ == "__main__" :   
487    main()       
Note: See TracBrowser for help on using the browser.