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

Revision 1482, 14.6 kB (checked in by jalet, 20 years ago)

pkpgcounter is now just a wrapper around the PDLAnalyzer class

  • 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.1  2004/05/18 09:59:54  jalet
25# pkpgcounter is now just a wrapper around the PDLAnalyzer class
26#
27#
28#
29
30import sys
31import os
32import struct
33import tempfile
34   
35class PostScriptAnalyzer :
36    def __init__(self, infile) :
37        """Initialize PostScript Analyzer."""
38        self.infile = infile
39       
40    def getJobSize(self) :   
41        """Count pages in a DSC compliant PostScript document."""
42        pagecount = 0
43        pagenum = None
44        while 1 :
45            line = self.infile.readline()
46            if not line :
47                break
48            if line.startswith("%%Page: ") :
49                pagecount += 1
50        return pagecount
51       
52class PCLAnalyzer :
53    def __init__(self, infile) :
54        """Initialize PCL Analyzer."""
55        self.infile = infile
56       
57    def getJobSize(self) :     
58        """Count pages in a PCL5 document."""
59        #
60        # Algorithm from pclcount
61        # (c) 2003, by Eduardo Gielamo Oliveira & Rodolfo Broco Manin
62        # published under the terms of the GNU General Public Licence v2.
63        #
64        # Backported from C to Python by Jerome Alet, then enhanced
65        # with more PCL tags detected. I think all the necessary PCL tags
66        # are recognized to correctly handle PCL5 files wrt their number
67        # of pages. The documentation used for this was :
68        #
69        # HP PCL/PJL Reference Set
70        # PCL5 Printer Language Technical Quick Reference Guide
71        # http://h20000.www2.hp.com/bc/docs/support/SupportManual/bpl13205/bpl13205.pdf
72        #
73        tagsends = { "&n" : "W", 
74                     "&b" : "W", 
75                     "*i" : "W", 
76                     "*l" : "W", 
77                     "*m" : "W", 
78                     "*v" : "W", 
79                     "*c" : "W", 
80                     "(f" : "W", 
81                     "*b" : "VW",
82                     "(s" : "W", 
83                     ")s" : "W", 
84                     "&p" : "X", 
85                     "&l" : "X" } 
86        copies = 1
87        pagecount = resets = 0
88        tag = None
89        while 1 :
90            char = self.infile.read(1)
91            if not char :       # EOF ?
92                break   
93            if char == "\014" :   
94                pagecount += 1
95            elif char == "\033" :   
96                #
97                #     <ESC>*b###W -> Start of a raster data row/block
98                #     <ESC>*b###V -> Start of a raster data plane
99                #     <ESC>*c###W -> Start of a user defined pattern
100                #     <ESC>*i###W -> Start of a viewing illuminant block
101                #     <ESC>*l###W -> Start of a color lookup table
102                #     <ESC>*m###W -> Start of a download dither matrix block
103                #     <ESC>*v###W -> Start of a configure image data block
104                #     <ESC>(s###W -> Start of a characters description block
105                #     <ESC>)s###W -> Start of a fonts description block
106                #     <ESC>(f###W -> Start of a symbol set block
107                #     <ESC>&b###W -> Start of configuration data block
108                #     <ESC>&l###X -> Number of copies
109                #     <ESC>&n###W -> Starts an alphanumeric string ID block
110                #     <ESC>&p###X -> Start of a non printable characters block
111                #
112                tagstart = self.infile.read(1)
113                if tagstart in "E9=YZ" : # one byte PCL tag
114                    if tagstart == "E" :
115                        resets += 1
116                    continue             # skip to next tag
117                tag = tagstart + self.infile.read(1)
118                try :
119                    tagend = tagsends[tag]
120                except KeyError :   
121                    pass    # Unsupported PCL tag
122                else :   
123                    # Now read the numeric argument
124                    size = 0
125                    while 1 :
126                        char = self.infile.read(1)
127                        if not char.isdigit() :
128                            break
129                        size = (size * 10) + int(char)   
130                    if char in tagend :   
131                        if tag == "&l" :
132                            copies = size
133                        else :   
134                            # doing a read will prevent the seek
135                            # for unseekable streams.
136                            # we just ignore the block anyway.
137                            if tag == "&n" : 
138                                # we have to take care of the operation id byte
139                                # which is before the string itself
140                                size += 1
141                            self.infile.read(size) # skips block, while avoiding seek()
142                           
143        # if pagecount is still 0, we will return the number
144        # of resets instead of the number of form feed characters.
145        # but the number of resets is always at least 2 with a valid
146        # pcl file : one at the very start and one at the very end
147        # of the job's data. So we substract 2 from the number of
148        # resets. And since on our test data we needed to substract
149        # 1 more, we finally substract 3, and will test several
150        # PCL files with this. If resets < 2, then the file is
151        # probably not a valid PCL file, so we return 0
152        if not pagecount :
153            return copies * (resets - 3) * (resets > 2)
154        else :
155            return copies * pagecount
156       
157class PCLXLAnalyzer :
158    def __init__(self, infile) :
159        """Initialize PCLXL Analyzer."""
160        raise TypeError, "PCLXL (aka PCL6) is not supported yet."
161        self.infile = infile
162        self.islittleendian = None
163        found = 0
164        while not found :
165            line = self.infile.readline()
166            if not line :
167                break
168            if line[1:12] == " HP-PCL XL;" :
169                found = 1
170                if line[0] == ")" :
171                    self.littleendian()
172                elif line[0] == "(" :   
173                    self.bigendian()
174        if not found :
175            raise TypeError, "This file doesn't seem to be PCLXL (aka PCL6)"
176        else :   
177            self.tags = [None] * 256   
178            self.tags[0x28] = self.bigendian    # big endian
179            self.tags[0x29] = self.littleendian # big endian
180            self.tags[0x43] = self.beginPage    # BeginPage
181            self.tags[0x44] = self.endPage      # EndPage
182           
183            self.tags[0xc0] = 1 # ubyte
184            self.tags[0xc1] = 2 # uint16
185            self.tags[0xc2] = 4 # uint32
186            self.tags[0xc3] = 2 # sint16
187            self.tags[0xc4] = 4 # sint32
188            self.tags[0xc5] = 4 # real32
189           
190            self.tags[0xc8] = self.array_8  # ubyte_array
191            self.tags[0xc9] = self.array_16 # uint16_array
192            self.tags[0xca] = self.array_32 # uint32_array
193            self.tags[0xcb] = self.array_16 # sint16_array
194            self.tags[0xcc] = self.array_32 # sint32_array
195            self.tags[0xcd] = self.array_32 # real32_array
196           
197            self.tags[0xd0] = 2 # ubyte_xy
198            self.tags[0xd1] = 4 # uint16_xy
199            self.tags[0xd2] = 8 # uint32_xy
200            self.tags[0xd3] = 4 # sint16_xy
201            self.tags[0xd4] = 8 # sint32_xy
202            self.tags[0xd5] = 8 # real32_xy
203           
204            self.tags[0xd0] = 4  # ubyte_box
205            self.tags[0xd1] = 8  # uint16_box
206            self.tags[0xd2] = 16 # uint32_box
207            self.tags[0xd3] = 8  # sint16_box
208            self.tags[0xd4] = 16 # sint32_box
209            self.tags[0xd5] = 16 # real32_box
210           
211            self.tags[0xf8] = 1 # attr_ubyte
212            self.tags[0xf9] = 2 # attr_uint16
213           
214            self.tags[0xfa] = self.embeddedData      # dataLength
215            self.tags[0xfb] = self.embeddedDataSmall # dataLengthByte
216           
217    def debug(self, msg) :
218        """Outputs a debug message on stderr."""
219        sys.stderr.write("%s\n" % msg)
220        sys.stderr.flush()
221       
222    def beginPage(self) :
223        """Indicates the beginning of a new page."""
224        self.pagecount += 1
225        self.debug("Begin page %i at %s" % (self.pagecount, self.infile.tell()))
226       
227    def endPage(self) :
228        """Indicates the end of a page."""
229        self.debug("End page %i at %s" % (self.pagecount, self.infile.tell()))
230       
231    def handleArray(self, itemsize) :       
232        """Handles arrays."""
233        datatype = self.infile.read(1)
234        length = self.tags[ord(datatype)]
235        sarraysize = self.infile.read(length)
236        if self.islittleendian :
237            fmt = "<"
238        else :   
239            fmt = ">"
240        if length == 1 :   
241            fmt += "B"
242        elif length == 2 :   
243            fmt += "H"
244        elif length == 4 :   
245            fmt += "I"
246        else :   
247            raise TypeError, "Error on array size at %s" % self.infile.tell()
248        arraysize = struct.unpack(fmt, sarraysize)[0]
249        return arraysize * itemsize
250       
251    def array_8(self) :   
252        """Handles byte arrays."""
253        return self.handleArray(1)
254       
255    def array_16(self) :   
256        """Handles byte arrays."""
257        return self.handleArray(2)
258       
259    def array_32(self) :   
260        """Handles byte arrays."""
261        return self.handleArray(4)
262       
263    def embeddedDataSmall(self) :
264        """Handle small amounts of data."""
265        return ord(self.infile.read(1))
266       
267    def embeddedData(self) :
268        """Handle normal amounts of data."""
269        if self.islittleendian :
270            fmt = "<I"
271        else :   
272            fmt = ">I"
273        return struct.unpack(fmt, self.infile.read(4))[0]
274       
275    def littleendian(self) :       
276        """Toggles to little endianness."""
277        self.islittleendian = 1 # little endian
278       
279    def bigendian(self) :   
280        """Toggles to big endianness."""
281        self.islittleendian = 0 # big endian
282   
283    def getJobSize(self) :
284        """Counts pages in a PCLXL (PCL6) document."""
285        self.pagecount = 0
286        while 1 :
287            pos = self.infile.tell()
288            char = self.infile.read(1)
289            if not char :
290                break
291            index = ord(char)   
292            length = self.tags[index]
293            if length is not None :
294                if not length :
295                    self.debug("Unrecognized tag 0x%02x at %s\n" % (index, self.infile.tell()))
296                elif callable(length) :   
297                    length = length()
298                if length :   
299                    self.infile.read(length)   
300        return self.pagecount
301   
302class PDLAnalyzer :   
303    """Generic PDL Analyzer class."""
304    def __init__(self, filename) :
305        """Initializes the PDL analyzer."""
306        self.filename = filename
307       
308    def getJobSize(self) :   
309        """Returns the job's size."""
310        self.openFile()
311        pdlhandler = self.detectPDLHandler()
312        if pdlhandler is not None :
313            try :
314                size = pdlhandler(self.infile).getJobSize()
315            finally :   
316                self.closeFile()
317            return size
318        else :       
319            self.closeFile()
320            raise TypeError, "ERROR : Unknown file format for %s" % self.filename
321       
322    def openFile(self) :   
323        """Opens the job's data stream for reading."""
324        if self.filename == "-" :
325            # we must read from stdin
326            # but since stdin is not seekable, we have to use a temporary
327            # file instead.
328            self.infile = tempfile.TemporaryFile()
329            while 1 :
330                data = sys.stdin.read(256 * 1024) 
331                if not data :
332                    break
333                self.infile.write(data)
334            self.infile.flush()   
335            self.infile.seek(0)
336        else :   
337            # normal file
338            self.infile = open(self.filename, "rb")
339           
340    def closeFile(self) :       
341        """Closes the job's data stream."""
342        self.infile.close()   
343       
344    def isPostScript(self, data) :   
345        """Returns 1 if data is PostScript, else 0."""
346        if data.startswith("%!") or \
347           data.startswith("\004%!") or \
348           data.startswith("\033%-12345X%!PS") or \
349           ((data[:128].find("\033%-12345X") != -1) and \
350             ((data.find("LANGUAGE=POSTSCRIPT") != -1) or \
351              (data.find("LANGUAGE = POSTSCRIPT") != -1) or \
352              (data.find("LANGUAGE = Postscript") != -1))) :
353            return 1
354        else :   
355            return 0
356       
357    def isPCL(self, data) :   
358        """Returns 1 if data is PCL, else 0."""
359        if data.startswith("\033E\033") or \
360           ((data[:128].find("\033%-12345X") != -1) and \
361             ((data.find("LANGUAGE=PCL") != -1) or \
362              (data.find("LANGUAGE = PCL") != -1) or \
363              (data.find("LANGUAGE = Pcl") != -1))) :
364            return 1
365        else :   
366            return 0
367       
368    def isPCLXL(self, data) :   
369        """Returns 1 if data is PCLXL aka PCL6, else 0."""
370        if ((data[:128].find("\033%-12345X") != -1) and \
371             (data.find(" HP-PCL XL;") != -1) and \
372             ((data.find("LANGUAGE=PCLXL") != -1) or \
373              (data.find("LANGUAGE = PCLXL") != -1))) :
374            return 1
375        else :   
376            return 0
377           
378    def detectPDLHandler(self) :   
379        """Tries to autodetect the document format.
380       
381           Returns the correct PDL handler class or None if format is unknown
382        """   
383        # Try to detect file type by reading first block of datas   
384        self.infile.seek(0)
385        firstblock = self.infile.read(1024)
386        self.infile.seek(0)
387        if self.isPostScript(firstblock) :
388            return PostScriptAnalyzer
389        elif self.isPCLXL(firstblock) :   
390            return PCLXLAnalyzer
391        elif self.isPCL(firstblock) :   
392            return PCLAnalyzer
Note: See TracBrowser for help on using the browser.