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

Revision 1690, 29.6 kB (checked in by jalet, 20 years ago)

Some more work on ESC/P2 analyzer to avoid missing \r\n sequences. Not
exactly optimal though...

  • 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.36  2004/09/01 22:31:49  jalet
25# Some more work on ESC/P2 analyzer to avoid missing \r\n sequences. Not
26# exactly optimal though...
27#
28# Revision 1.35  2004/08/30 23:10:24  jalet
29# Improved the ESC/P2 analyzer so that more GhostScript devices are supported
30#
31# Revision 1.34  2004/08/27 09:08:22  jalet
32# Improvement in PostScript parser to avoid being fooled by clever "students"
33#
34# Revision 1.33  2004/08/27 09:02:34  jalet
35# Forgot to remove some special debugging code...
36#
37# Revision 1.32  2004/08/27 08:58:50  jalet
38# Relax checks for PCL5 header to accomodate strange printer drivers
39#
40# Revision 1.31  2004/08/22 08:25:33  jalet
41# Improved ESC/P2 miniparser thanks to Paulo Silva
42#
43# Revision 1.30  2004/08/21 23:16:57  jalet
44# First draft of ESC/P2 (mini-)parser.
45#
46# Revision 1.29  2004/08/11 16:25:38  jalet
47# Fixed index problem in PCLXL parser when retrieving number of copies for
48# each page
49#
50# Revision 1.28  2004/08/10 23:01:49  jalet
51# Fixed number of copies in PCL5 parser
52#
53# Revision 1.27  2004/08/09 18:14:22  jalet
54# Added workaround for number of copies and some PostScript drivers
55#
56# Revision 1.26  2004/07/22 13:49:51  jalet
57# Added support for binary PostScript through GhostScript if native DSC
58# compliant PostScript analyzer doesn't find any page. This is much
59# slower though, so native analyzer is tried first.
60#
61# Revision 1.25  2004/07/10 14:06:36  jalet
62# Fix for Python2.1 incompatibilities
63#
64# Revision 1.24  2004/07/05 21:00:39  jalet
65# Fix for number of copies for each page in PCLXL parser
66#
67# Revision 1.23  2004/07/03 08:21:59  jalet
68# Testsuite for PDL Analyzer added
69#
70# Revision 1.22  2004/06/29 14:21:41  jalet
71# Smallish optimization
72#
73# Revision 1.21  2004/06/28 23:11:26  jalet
74# Code de-factorization in PCLXL parser
75#
76# Revision 1.20  2004/06/28 22:38:41  jalet
77# Increased speed by a factor of 2 in PCLXL parser
78#
79# Revision 1.19  2004/06/28 21:20:30  jalet
80# PCLXL support now works !
81#
82# Revision 1.18  2004/06/27 22:59:37  jalet
83# More work on PCLXL parser
84#
85# Revision 1.17  2004/06/26 23:20:01  jalet
86# Additionnal speedup for GhostScript generated PCL5 files
87#
88# Revision 1.16  2004/06/26 15:31:00  jalet
89# mmap reintroduced in PCL5 parser
90#
91# Revision 1.15  2004/06/26 14:14:31  jalet
92# Now uses Psyco if it is available
93#
94# Revision 1.14  2004/06/25 09:50:28  jalet
95# More debug info in PCLXL parser
96#
97# Revision 1.13  2004/06/25 08:10:08  jalet
98# Another fix for PCL5 parser
99#
100# Revision 1.12  2004/06/24 23:09:53  jalet
101# Fix for number of copies in PCL5 parser
102#
103# Revision 1.11  2004/06/23 22:07:50  jalet
104# Fixed PCL5 parser according to the sources of rastertohp
105#
106# Revision 1.10  2004/06/18 22:24:03  jalet
107# Removed old comments
108#
109# Revision 1.9  2004/06/18 22:21:27  jalet
110# Native PDF parser greatly improved.
111# GhostScript based PDF parser completely removed because native code
112# is now portable across Python versions.
113#
114# Revision 1.8  2004/06/18 20:49:46  jalet
115# "ERROR:" prefix added
116#
117# Revision 1.7  2004/06/18 17:48:04  jalet
118# Added native fast PDF parsing method
119#
120# Revision 1.6  2004/06/18 14:00:16  jalet
121# Added PDF support in smart PDL analyzer (through GhostScript for now)
122#
123# Revision 1.5  2004/06/18 10:09:05  jalet
124# Resets file pointer to start of file in all cases
125#
126# Revision 1.4  2004/06/18 06:16:14  jalet
127# Fixes PostScript detection code for incorrect drivers
128#
129# Revision 1.3  2004/05/21 20:40:08  jalet
130# All the code for pkpgcounter is now in pdlanalyzer.py
131#
132# Revision 1.2  2004/05/19 19:09:36  jalet
133# Speed improvement
134#
135# Revision 1.1  2004/05/18 09:59:54  jalet
136# pkpgcounter is now just a wrapper around the PDLAnalyzer class
137#
138#
139#
140
141import sys
142import os
143import re
144from struct import unpack
145import tempfile
146import mmap
147import popen2
148   
149KILOBYTE = 1024   
150MEGABYTE = 1024 * KILOBYTE   
151
152class PDLAnalyzerError(Exception):
153    """An exception for PDL Analyzer related stuff."""
154    def __init__(self, message = ""):
155        self.message = message
156        Exception.__init__(self, message)
157    def __repr__(self):
158        return self.message
159    __str__ = __repr__
160   
161class PostScriptAnalyzer :
162    def __init__(self, infile) :
163        """Initialize PostScript Analyzer."""
164        self.infile = infile
165        self.copies = 1
166       
167    def throughGhostScript(self) :
168        """Get the count through GhostScript, useful for non-DSC compliant PS files."""
169        self.infile.seek(0)
170        command = 'gs -sDEVICE=bbox -dNOPAUSE -dBATCH -dQUIET - 2>&1 | grep -c "%%HiResBoundingBox:" 2>/dev/null'
171        child = popen2.Popen4(command)
172        try :
173            data = self.infile.read(MEGABYTE)   
174            while data :
175                child.tochild.write(data)
176                data = self.infile.read(MEGABYTE)
177            child.tochild.flush()
178            child.tochild.close()   
179        except (IOError, OSError), msg :   
180            raise PDLAnalyzerError, "Problem during analysis of Binary PostScript document."
181           
182        pagecount = 0
183        try :
184            pagecount = int(child.fromchild.readline().strip())
185        except (IOError, OSError, AttributeError, ValueError) :
186            raise PDLAnalyzerError, "Problem during analysis of Binary PostScript document."
187        child.fromchild.close()
188       
189        try :
190            retcode = child.wait()
191        except OSError, msg :   
192            raise PDLAnalyzerError, "Problem during analysis of Binary PostScript document."
193        return pagecount * self.copies
194       
195    def natively(self) :
196        """Count pages in a DSC compliant PostScript document."""
197        self.infile.seek(0)
198        pagecount = 0
199        for line in self.infile.xreadlines() : 
200            if line.startswith("%%Page: ") :
201                pagecount += 1
202            elif line.startswith("%%BeginNonPPDFeature: NumCopies ") :
203                # handle # of copies set by some Windows printer driver
204                try :
205                    number = int(line.strip().split()[2])
206                except :     
207                    pass
208                else :   
209                    if number > self.copies :
210                        self.copies = number
211            elif line.startswith("1 dict dup /NumCopies ") :
212                # handle # of copies set by mozilla/kprinter
213                try :
214                    number = int(line.strip().split()[4])
215                except :     
216                    pass
217                else :   
218                    if number > self.copies :
219                        self.copies = number
220        return pagecount * self.copies
221       
222    def getJobSize(self) :   
223        """Count pages in PostScript document."""
224        return self.natively() or self.throughGhostScript()
225       
226class PDFAnalyzer :
227    def __init__(self, infile) :
228        """Initialize PDF Analyzer."""
229        self.infile = infile
230               
231    def getJobSize(self) :   
232        """Counts pages in a PDF document."""
233        regexp = re.compile(r"(/Type) ?(/Page)[/ \t\r\n]")
234        pagecount = 0
235        for line in self.infile.xreadlines() : 
236            pagecount += len(regexp.findall(line))
237        return pagecount   
238       
239class ESCP2Analyzer :
240    def __init__(self, infile) :
241        """Initialize ESC/P2 Analyzer."""
242        self.infile = infile
243               
244    def getJobSize(self) :   
245        """Counts pages in an ESC/P2 document."""
246        # with Gimpprint, at least, for each page there
247        # are two Reset Printer sequences (ESC + @)
248        marker1 = "\033@"
249       
250        # with other software or printer driver, we
251        # may prefer to search for "\r\n\fESCAPE"
252        # or "\r\fESCAPE"
253        marker2r = "\r\f\033"
254        marker2rn = "\r\n\f\033"
255       
256        # and ghostscript's stcolor for example seems to
257        # output ESC + @ + \f for each page plus one
258        marker3 = "\033@\f"
259       
260        # while ghostscript's escp driver outputs instead
261        # \f + ESC + @
262        marker4 = "\f\033@"
263       
264        data = self.infile.read()
265        pagecount1 = data.count(marker1)
266        pagecount2 = max(data.count(marker2r), data.count(marker2rn))
267        pagecount3 = data.count(marker3)
268        pagecount4 = data.count(marker4)
269           
270        if pagecount2 :   
271            return pagecount2
272        elif pagecount3 > 1 :     
273            return pagecount3 - 1
274        elif pagecount4 :   
275            return pagecount4
276        else :   
277            return int(pagecount1 / 2)       
278       
279class PCLAnalyzer :
280    def __init__(self, infile) :
281        """Initialize PCL Analyzer."""
282        self.infile = infile
283       
284    def getJobSize(self) :     
285        """Count pages in a PCL5 document.
286         
287           Should also work for PCL3 and PCL4 documents.
288           
289           Algorithm from pclcount
290           (c) 2003, by Eduardo Gielamo Oliveira & Rodolfo Broco Manin
291           published under the terms of the GNU General Public Licence v2.
292         
293           Backported from C to Python by Jerome Alet, then enhanced
294           with more PCL tags detected. I think all the necessary PCL tags
295           are recognized to correctly handle PCL5 files wrt their number
296           of pages. The documentation used for this was :
297         
298           HP PCL/PJL Reference Set
299           PCL5 Printer Language Technical Quick Reference Guide
300           http://h20000.www2.hp.com/bc/docs/support/SupportManual/bpl13205/bpl13205.pdf
301        """
302        infileno = self.infile.fileno()
303        minfile = mmap.mmap(infileno, os.fstat(infileno)[6], prot=mmap.PROT_READ, flags=mmap.MAP_SHARED)
304        tagsends = { "&n" : "W", 
305                     "&b" : "W", 
306                     "*i" : "W", 
307                     "*l" : "W", 
308                     "*m" : "W", 
309                     "*v" : "W", 
310                     "*c" : "W", 
311                     "(f" : "W", 
312                     "(s" : "W", 
313                     ")s" : "W", 
314                     "&p" : "X", 
315                     "&l" : "XH",
316                     "&a" : "G",
317                     # "*b" : "VW", # treated specially because it occurs very often
318                   } 
319        pagecount = resets = ejects = backsides = 0
320        tag = None
321        copies = {}
322        pos = 0
323        try :
324            while 1 :
325                char = minfile[pos] ; pos += 1
326                if char == "\014" :   
327                    pagecount += 1
328                elif char == "\033" :   
329                    #
330                    #     <ESC>*b###W -> Start of a raster data row/block
331                    #     <ESC>*b###V -> Start of a raster data plane
332                    #     <ESC>*c###W -> Start of a user defined pattern
333                    #     <ESC>*i###W -> Start of a viewing illuminant block
334                    #     <ESC>*l###W -> Start of a color lookup table
335                    #     <ESC>*m###W -> Start of a download dither matrix block
336                    #     <ESC>*v###W -> Start of a configure image data block
337                    #     <ESC>(s###W -> Start of a characters description block
338                    #     <ESC>)s###W -> Start of a fonts description block
339                    #     <ESC>(f###W -> Start of a symbol set block
340                    #     <ESC>&b###W -> Start of configuration data block
341                    #     <ESC>&l###X -> Number of copies for current page
342                    #     <ESC>&n###W -> Starts an alphanumeric string ID block
343                    #     <ESC>&p###X -> Start of a non printable characters block
344                    #     <ESC>&a2G -> Back side when duplex mode as generated by rastertohp
345                    #     <ESC>&l0H -> Eject if NumPlanes > 1, as generated by rastertohp
346                    #
347                    tagstart = minfile[pos] ; pos += 1
348                    if tagstart in "E9=YZ" : # one byte PCL tag
349                        if tagstart == "E" :
350                            resets += 1
351                        continue             # skip to next tag
352                    tag = tagstart + minfile[pos] ; pos += 1
353                    if tag == "*b" : 
354                        tagend = "VW"
355                    else :   
356                        try :
357                            tagend = tagsends[tag]
358                        except KeyError :   
359                            continue # Unsupported PCL tag
360                    # Now read the numeric argument
361                    size = 0
362                    while 1 :
363                        char = minfile[pos] ; pos += 1
364                        if not char.isdigit() :
365                            break
366                        size = (size * 10) + int(char)   
367                    if char in tagend :   
368                        if (tag == "&l") and (char == "X") : # copies for current page
369                            copies[pagecount] = size
370                        elif (tag == "&l") and (char == "H") and (size == 0) :   
371                            ejects += 1         # Eject
372                        elif (tag == "&a") and (size == 2) :
373                            backsides += 1      # Back side in duplex mode
374                        else :   
375                            # we just ignore the block.
376                            if tag == "&n" : 
377                                # we have to take care of the operation id byte
378                                # which is before the string itself
379                                size += 1
380                            pos += size   
381        except IndexError : # EOF ?
382            minfile.close() # reached EOF
383                           
384        # if pagecount is still 0, we will use the number
385        # of resets instead of the number of form feed characters.
386        # but the number of resets is always at least 2 with a valid
387        # pcl file : one at the very start and one at the very end
388        # of the job's data. So we substract 2 from the number of
389        # resets. And since on our test data we needed to substract
390        # 1 more, we finally substract 3, and will test several
391        # PCL files with this. If resets < 2, then the file is
392        # probably not a valid PCL file, so we use 0
393        if not pagecount :
394            pagecount = (pagecount or ((resets - 3) * (resets > 2)))
395        else :   
396            # here we add counters for other ways new pages may have
397            # been printed and ejected by the printer
398            pagecount += ejects + backsides
399       
400        # now handle number of copies for each page (may differ).
401        # in duplex mode, number of copies may be sent only once.
402        for pnum in range(pagecount) :
403            # if no number of copies defined, take the preceding one else the one set before any page else 1.
404            nb = copies.get(pnum, copies.get(pnum-1, copies.get(0, 1)))
405            pagecount += (nb - 1)
406        return pagecount
407       
408class PCLXLAnalyzer :
409    def __init__(self, infile) :
410        """Initialize PCLXL Analyzer."""
411        self.infile = infile
412        self.endianness = None
413        found = 0
414        while not found :
415            line = self.infile.readline()
416            if not line :
417                break
418            if line[1:12] == " HP-PCL XL;" :
419                found = 1
420                endian = ord(line[0])
421                if endian == 0x29 :
422                    self.littleEndian()
423                elif endian == 0x28 :   
424                    self.bigEndian()
425                # elif endian == 0x27 : TODO : What can we do here ?   
426                #
427                else :   
428                    raise PDLAnalyzerError, "Unknown endianness marker 0x%02x at start !" % endian
429        if not found :
430            raise PDLAnalyzerError, "This file doesn't seem to be PCLXL (aka PCL6)"
431        else :   
432            # Initialize table of tags
433            self.tags = [ 0 ] * 256   
434           
435            # GhostScript's sources tell us that HP printers
436            # only accept little endianness, but we can handle both.
437            self.tags[0x28] = self.bigEndian    # BigEndian
438            self.tags[0x29] = self.littleEndian # LittleEndian
439           
440            self.tags[0x43] = self.beginPage    # BeginPage
441            self.tags[0x44] = self.endPage      # EndPage
442           
443            self.tags[0xc0] = 1 # ubyte
444            self.tags[0xc1] = 2 # uint16
445            self.tags[0xc2] = 4 # uint32
446            self.tags[0xc3] = 2 # sint16
447            self.tags[0xc4] = 4 # sint32
448            self.tags[0xc5] = 4 # real32
449           
450            self.tags[0xc8] = self.array_8  # ubyte_array
451            self.tags[0xc9] = self.array_16 # uint16_array
452            self.tags[0xca] = self.array_32 # uint32_array
453            self.tags[0xcb] = self.array_16 # sint16_array
454            self.tags[0xcc] = self.array_32 # sint32_array
455            self.tags[0xcd] = self.array_32 # real32_array
456           
457            self.tags[0xd0] = 2 # ubyte_xy
458            self.tags[0xd1] = 4 # uint16_xy
459            self.tags[0xd2] = 8 # uint32_xy
460            self.tags[0xd3] = 4 # sint16_xy
461            self.tags[0xd4] = 8 # sint32_xy
462            self.tags[0xd5] = 8 # real32_xy
463           
464            self.tags[0xe0] = 4  # ubyte_box
465            self.tags[0xe1] = 8  # uint16_box
466            self.tags[0xe2] = 16 # uint32_box
467            self.tags[0xe3] = 8  # sint16_box
468            self.tags[0xe4] = 16 # sint32_box
469            self.tags[0xe5] = 16 # real32_box
470           
471            self.tags[0xf8] = 1 # attr_ubyte
472            self.tags[0xf9] = 2 # attr_uint16
473           
474            self.tags[0xfa] = self.embeddedData      # dataLength
475            self.tags[0xfb] = self.embeddedDataSmall # dataLengthByte
476           
477    def beginPage(self) :
478        """Indicates the beginning of a new page."""
479        self.pagecount += 1
480        return 0
481       
482    def endPage(self) :   
483        """Indicates the end of a page."""
484        pos = self.pos
485        minfile = self.minfile
486        if (ord(minfile[pos-3]) == 0xf8) and (ord(minfile[pos-2]) == 0x31) :
487            # The EndPage operator is preceded by a PageCopies attribute
488            # So set number of copies for current page.
489            # From what I read in PCLXL documentation, the number
490            # of copies is an unsigned 16 bits integer
491            self.copies[self.pagecount] = unpack(self.endianness + "H", minfile[pos-5:pos-3])[0]
492        return 0
493       
494    def array_8(self) :   
495        """Handles byte arrays."""
496        pos = self.pos
497        datatype = self.minfile[pos]
498        pos += 1
499        length = self.tags[ord(datatype)]
500        if callable(length) :
501            self.pos = pos
502            length = length()
503            pos = self.pos
504        posl = pos + length
505        self.pos = posl
506        if length == 1 :   
507            return unpack("B", self.minfile[pos:posl])[0]
508        elif length == 2 :   
509            return unpack(self.endianness + "H", self.minfile[pos:posl])[0]
510        elif length == 4 :   
511            return unpack(self.endianness + "I", self.minfile[pos:posl])[0]
512        else :   
513            raise PDLAnalyzerError, "Error on array size at %s" % self.pos
514       
515    def array_16(self) :   
516        """Handles byte arrays."""
517        pos = self.pos
518        datatype = self.minfile[pos]
519        pos += 1
520        length = self.tags[ord(datatype)]
521        if callable(length) :
522            self.pos = pos
523            length = length()
524            pos = self.pos
525        posl = pos + length
526        self.pos = posl
527        if length == 1 :   
528            return 2 * unpack("B", self.minfile[pos:posl])[0]
529        elif length == 2 :   
530            return 2 * unpack(self.endianness + "H", self.minfile[pos:posl])[0]
531        elif length == 4 :   
532            return 2 * unpack(self.endianness + "I", self.minfile[pos:posl])[0]
533        else :   
534            raise PDLAnalyzerError, "Error on array size at %s" % self.pos
535       
536    def array_32(self) :   
537        """Handles byte arrays."""
538        pos = self.pos
539        datatype = self.minfile[pos]
540        pos += 1
541        length = self.tags[ord(datatype)]
542        if callable(length) :
543            self.pos = pos
544            length = length()
545            pos = self.pos
546        posl = pos + length
547        self.pos = posl
548        if length == 1 :   
549            return 4 * unpack("B", self.minfile[pos:posl])[0]
550        elif length == 2 :   
551            return 4 * unpack(self.endianness + "H", self.minfile[pos:posl])[0]
552        elif length == 4 :   
553            return 4 * unpack(self.endianness + "I", self.minfile[pos:posl])[0]
554        else :   
555            raise PDLAnalyzerError, "Error on array size at %s" % self.pos
556       
557    def embeddedDataSmall(self) :
558        """Handle small amounts of data."""
559        pos = self.pos
560        length = ord(self.minfile[pos])
561        self.pos = pos + 1
562        return length
563       
564    def embeddedData(self) :
565        """Handle normal amounts of data."""
566        pos = self.pos
567        pos4 = pos + 4
568        self.pos = pos4
569        return unpack(self.endianness + "I", self.minfile[pos:pos4])[0]
570       
571    def littleEndian(self) :       
572        """Toggles to little endianness."""
573        self.endianness = "<" # little endian
574        return 0
575       
576    def bigEndian(self) :   
577        """Toggles to big endianness."""
578        self.endianness = ">" # big endian
579        return 0
580   
581    def getJobSize(self) :
582        """Counts pages in a PCLXL (PCL6) document.
583       
584           Algorithm by Jerome Alet.
585           
586           The documentation used for this was :
587         
588           HP PCL XL Feature Reference
589           Protocol Class 2.0
590           http://www.hpdevelopersolutions.com/downloads/64/358/xl_ref20r22.pdf
591        """
592        infileno = self.infile.fileno()
593        self.copies = {}
594        self.minfile = minfile = mmap.mmap(infileno, os.fstat(infileno)[6], prot=mmap.PROT_READ, flags=mmap.MAP_SHARED)
595        tags = self.tags
596        self.pagecount = 0
597        self.pos = pos = self.infile.tell()
598        try :
599            while 1 :
600                char = minfile[pos]
601                pos += 1
602                length = tags[ord(char)]
603                if not length :
604                    continue
605                if callable(length) :   
606                    self.pos = pos
607                    length = length()
608                    pos = self.pos
609                pos += length   
610        except IndexError : # EOF ?
611            self.minfile.close() # reached EOF
612           
613        # now handle number of copies for each page (may differ).
614        for pnum in range(1, self.pagecount + 1) :
615            # if no number of copies defined, take 1, as explained
616            # in PCLXL documentation.
617            # NB : is number of copies is 0, the page won't be output
618            # but the formula below is still correct : we want
619            # to decrease the total number of pages in this case.
620            self.pagecount += (self.copies.get(pnum, 1) - 1)
621           
622        return self.pagecount
623       
624class PDLAnalyzer :   
625    """Generic PDL Analyzer class."""
626    def __init__(self, filename) :
627        """Initializes the PDL analyzer.
628       
629           filename is the name of the file or '-' for stdin.
630           filename can also be a file-like object which
631           supports read() and seek().
632        """
633        self.filename = filename
634        try :
635            import psyco 
636        except ImportError :   
637            pass # Psyco is not installed
638        else :   
639            # Psyco is installed, tell it to compile
640            # the CPU intensive methods : PCL and PCLXL
641            # parsing will greatly benefit from this,
642            # for PostScript and PDF the difference is
643            # barely noticeable since they are already
644            # almost optimal, and much more speedy anyway.
645            psyco.bind(PostScriptAnalyzer.getJobSize)
646            psyco.bind(PDFAnalyzer.getJobSize)
647            psyco.bind(ESCP2Analyzer.getJobSize)
648            psyco.bind(PCLAnalyzer.getJobSize)
649            psyco.bind(PCLXLAnalyzer.getJobSize)
650       
651    def getJobSize(self) :   
652        """Returns the job's size."""
653        self.openFile()
654        try :
655            pdlhandler = self.detectPDLHandler()
656        except PDLAnalyzerError, msg :   
657            self.closeFile()
658            raise PDLAnalyzerError, "ERROR : Unknown file format for %s (%s)" % (self.filename, msg)
659        else :
660            try :
661                size = pdlhandler(self.infile).getJobSize()
662            finally :   
663                self.closeFile()
664            return size
665       
666    def openFile(self) :   
667        """Opens the job's data stream for reading."""
668        self.mustclose = 0  # by default we don't want to close the file when finished
669        if hasattr(self.filename, "read") and hasattr(self.filename, "seek") :
670            # filename is in fact a file-like object
671            infile = self.filename
672        elif self.filename == "-" :
673            # we must read from stdin
674            infile = sys.stdin
675        else :   
676            # normal file
677            self.infile = open(self.filename, "rb")
678            self.mustclose = 1
679            return
680           
681        # Use a temporary file, always seekable contrary to standard input.
682        self.infile = tempfile.TemporaryFile(mode="w+b")
683        while 1 :
684            data = infile.read(MEGABYTE) 
685            if not data :
686                break
687            self.infile.write(data)
688        self.infile.flush()   
689        self.infile.seek(0)
690           
691    def closeFile(self) :       
692        """Closes the job's data stream if we can close it."""
693        if self.mustclose :
694            self.infile.close()   
695        else :   
696            # if we don't have to close the file, then
697            # ensure the file pointer is reset to the
698            # start of the file in case the process wants
699            # to read the file again.
700            try :
701                self.infile.seek(0)
702            except :   
703                pass    # probably stdin, which is not seekable
704       
705    def isPostScript(self, data) :   
706        """Returns 1 if data is PostScript, else 0."""
707        if data.startswith("%!") or \
708           data.startswith("\004%!") or \
709           data.startswith("\033%-12345X%!PS") or \
710           ((data[:128].find("\033%-12345X") != -1) and \
711             ((data.find("LANGUAGE=POSTSCRIPT") != -1) or \
712              (data.find("LANGUAGE = POSTSCRIPT") != -1) or \
713              (data.find("LANGUAGE = Postscript") != -1))) or \
714              (data.find("%!PS-Adobe") != -1) :
715            return 1
716        else :   
717            return 0
718       
719    def isPDF(self, data) :   
720        """Returns 1 if data is PDF, else 0."""
721        if data.startswith("%PDF-") or \
722           data.startswith("\033%-12345X%PDF-") or \
723           ((data[:128].find("\033%-12345X") != -1) and (data.upper().find("LANGUAGE=PDF") != -1)) or \
724           (data.find("%PDF-") != -1) :
725            return 1
726        else :   
727            return 0
728       
729    def isPCL(self, data) :   
730        """Returns 1 if data is PCL, else 0."""
731        if data.startswith("\033E\033") or \
732           (data[:128].find("\033%-12345X") != -1) :
733            return 1
734        else :   
735            return 0
736       
737    def isPCLXL(self, data) :   
738        """Returns 1 if data is PCLXL aka PCL6, else 0."""
739        if ((data[:128].find("\033%-12345X") != -1) and \
740             (data.find(" HP-PCL XL;") != -1) and \
741             ((data.find("LANGUAGE=PCLXL") != -1) or \
742              (data.find("LANGUAGE = PCLXL") != -1))) :
743            return 1
744        else :   
745            return 0
746           
747    def isESCP2(self, data) :       
748        """Returns 1 if data is ESC/P2, else 0."""
749        if data.startswith("\033@") or \
750           data.startswith("\033*") or \
751           data.startswith("\n\033@") :
752            return 1
753        else :   
754            return 0
755   
756    def detectPDLHandler(self) :   
757        """Tries to autodetect the document format.
758       
759           Returns the correct PDL handler class or None if format is unknown
760        """   
761        # Try to detect file type by reading first block of datas   
762        self.infile.seek(0)
763        firstblock = self.infile.read(KILOBYTE)
764        self.infile.seek(0)
765        if self.isPostScript(firstblock) :
766            return PostScriptAnalyzer
767        elif self.isPCLXL(firstblock) :   
768            return PCLXLAnalyzer
769        elif self.isPDF(firstblock) :   
770            return PDFAnalyzer
771        elif self.isPCL(firstblock) :   
772            return PCLAnalyzer
773        elif self.isESCP2(firstblock) :   
774            return ESCP2Analyzer
775        else :   
776            raise PDLAnalyzerError, "Analysis of first data block failed."
777           
778def main() :   
779    """Entry point for PDL Analyzer."""
780    if (len(sys.argv) < 2) or ((not sys.stdin.isatty()) and ("-" not in sys.argv[1:])) :
781        sys.argv.append("-")
782       
783    totalsize = 0   
784    for arg in sys.argv[1:] :
785        try :
786            parser = PDLAnalyzer(arg)
787            totalsize += parser.getJobSize()
788        except PDLAnalyzerError, msg :   
789            sys.stderr.write("ERROR: %s\n" % msg)
790            sys.stderr.flush()
791    print "%s" % totalsize
792   
793if __name__ == "__main__" :   
794    main()
Note: See TracBrowser for help on using the browser.