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

Revision 1550, 21.2 kB (checked in by jalet, 20 years ago)

Added native fast PDF parsing method

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