home *** CD-ROM | disk | FTP | other *** search
/ OS/2 Shareware BBS: 10 Tools / 10-Tools.zip / pyos2bin.zip / Demo / sgi / video / VFile.py < prev    next >
Text File  |  1994-12-21  |  30KB  |  1,194 lines

  1. # Classes to read and write CMIF video files.
  2. # (For a description of the CMIF video format, see cmif-file.ms.)
  3.  
  4.  
  5. # Layers of functionality:
  6. #
  7. # VideoParams: maintain essential parameters of a video file
  8. # Displayer: display a frame in a window (with some extra parameters)
  9. # BasicVinFile: read a CMIF video file
  10. # BasicVoutFile: write a CMIF video file
  11. # VinFile: BasicVinFile + Displayer
  12. # VoutFile: BasicVoutFile + Displayer
  13. #
  14. # XXX Future extension:
  15. # BasicVinoutFile: supports overwriting of individual frames
  16.  
  17.  
  18. # Imported modules
  19.  
  20. import sys
  21. try:
  22.     import gl
  23.     import GL
  24.     import GET
  25.     no_gl = 0
  26. except ImportError:
  27.     no_gl = 1
  28. import colorsys
  29. import imageop
  30.  
  31.  
  32. # Exception raised for various occasions
  33.  
  34. Error = 'VFile.Error'            # file format errors
  35. CallError = 'VFile.CallError'        # bad call
  36. AssertError = 'VFile.AssertError'    # internal malfunction
  37.  
  38.  
  39. # Max nr. of colormap entries to use
  40.  
  41. MAXMAP = 4096 - 256
  42.  
  43.  
  44. # Parametrizations of colormap handling based on color system.
  45. # (These functions are used via eval with a constructed argument!)
  46.  
  47. def conv_grey(l, x, y):
  48.     return colorsys.yiq_to_rgb(l, 0, 0)
  49.  
  50. def conv_grey4(l, x, y):
  51.     return colorsys.yiq_to_rgb(l*17, 0, 0)
  52.  
  53. def conv_mono(l, x, y):
  54.     return colorsys.yiq_to_rgb(l*255, 0, 0)
  55.  
  56. def conv_yiq(y, i, q):
  57.     return colorsys.yiq_to_rgb(y, (i-0.5)*1.2, q-0.5)
  58.  
  59. def conv_hls(l, h, s):
  60.     return colorsys.hls_to_rgb(h, l, s)
  61.  
  62. def conv_hsv(v, h, s):
  63.     return colorsys.hsv_to_rgb(h, s, v)
  64.  
  65. def conv_rgb(r, g, b):
  66.     raise Error, 'Attempt to make RGB colormap'
  67.  
  68. def conv_rgb8(rgb, d1, d2):
  69.     rgb = int(rgb*255.0)
  70.     r = (rgb >> 5) & 0x07
  71.     g = (rgb     ) & 0x07
  72.     b = (rgb >> 3) & 0x03
  73.     return (r/7.0, g/7.0, b/3.0)
  74.  
  75. def conv_jpeg(r, g, b):
  76.     raise Error, 'Attempt to make RGB colormap (jpeg)'
  77.  
  78. conv_jpeggrey = conv_grey
  79. conv_grey2 = conv_grey
  80.  
  81.  
  82. # Choose one of the above based upon a color system name
  83.  
  84. def choose_conversion(format):
  85.     try:
  86.         return eval('conv_' + format)
  87.     except:
  88.         raise Error, 'Unknown color system: ' + `format`
  89.  
  90.  
  91. # Inverses of the above
  92.  
  93. def inv_grey(r, g, b):
  94.     y, i, q = colorsys.rgb_to_yiq(r, g, b)
  95.     return y, 0, 0
  96.  
  97. def inv_yiq(r, g, b):
  98.     y, i, q = colorsys.rgb_to_yiq(r, g, b)
  99.     return y, i/1.2 + 0.5, q + 0.5
  100.  
  101. def inv_hls(r, g, b):
  102.     h, l, s = colorsys.rgb_to_hls(r, g, b)
  103.     return l, h, s
  104.  
  105. def inv_hsv(r, g, b):
  106.     h, s, v = colorsys.rgb_to_hsv(r, g, b)
  107.     return v, h, s
  108.  
  109. def inv_rgb(r, g, b):
  110.     raise Error, 'Attempt to invert RGB colormap'
  111.  
  112. def inv_rgb8(r, g, b):
  113.     r = int(r*7.0)
  114.     g = int(g*7.0)
  115.     b = int(b*7.0)
  116.     rgb = ((r&7) << 5) | ((b&3) << 3) | (g&7)
  117.     return rgb / 255.0, 0, 0
  118.  
  119. def inv_jpeg(r, g, b):
  120.     raise Error, 'Attempt to invert RGB colormap (jpeg)'
  121.  
  122. inv_jpeggrey = inv_grey
  123.  
  124.  
  125. # Choose one of the above based upon a color system name
  126.  
  127. def choose_inverse(format):
  128.     try:
  129.         return eval('inv_' + format)
  130.     except:
  131.         raise Error, 'Unknown color system: ' + `format`
  132.  
  133.  
  134. # Predicate to see whether this is an entry level (non-XS) Indigo.
  135. # If so we can lrectwrite 8-bit wide pixels into a window in RGB mode
  136.  
  137. def is_entry_indigo():
  138.     # XXX hack, hack.  We should call gl.gversion() but that doesn't
  139.     # exist in earlier Python versions.  Therefore we check the number
  140.     # of bitplanes *and* the size of the monitor.
  141.     xmax = gl.getgdesc(GL.GD_XPMAX)
  142.     if xmax <> 1024: return 0
  143.     ymax = gl.getgdesc(GL.GD_YPMAX)
  144.     if ymax != 768: return 0
  145.     r = gl.getgdesc(GL.GD_BITS_NORM_SNG_RED)
  146.     g = gl.getgdesc(GL.GD_BITS_NORM_SNG_GREEN)
  147.     b = gl.getgdesc(GL.GD_BITS_NORM_SNG_BLUE)
  148.     return (r, g, b) == (3, 3, 2)
  149.  
  150.  
  151. # Predicate to see whether this machine supports pixmode(PM_SIZE) with
  152. # values 1 or 4.
  153. #
  154. # XXX Temporarily disabled, since it is unclear which machines support
  155. # XXX which pixelsizes.
  156. #
  157. # XXX The XS appears to support 4 bit pixels, but (looking at osview) it
  158. # XXX seems as if the conversion is done by the kernel (unpacking ourselves
  159. # XXX is faster than using PM_SIZE=4)
  160.  
  161. def support_packed_pixels():
  162.     return 0   # To be architecture-dependent
  163.  
  164.  
  165.  
  166. # Tables listing bits per pixel for some formats
  167.  
  168. bitsperpixel = { \
  169.       'rgb': 32, \
  170.       'rgb8': 8, \
  171.       'grey': 8, \
  172.       'grey4': 4, \
  173.       'grey2': 2, \
  174.       'mono': 1, \
  175.       'compress': 32, \
  176. }
  177.  
  178. bppafterdecomp = {'jpeg': 32, 'jpeggrey': 8}
  179.  
  180.  
  181. # Base class to manage video format parameters
  182.  
  183. class VideoParams:
  184.  
  185.     # Initialize an instance.
  186.     # Set all parameters to something decent
  187.     # (except width and height are set to zero)
  188.  
  189.     def __init__(self):
  190.         # Essential parameters
  191.         self.frozen = 0        # if set, can't change parameters
  192.         self.format = 'grey'    # color system used
  193.         # Choose from: grey, rgb, rgb8, hsv, yiq, hls, jpeg, jpeggrey,
  194.         #              mono, grey2, grey4
  195.         self.width = 0        # width of frame
  196.         self.height = 0        # height of frame
  197.         self.packfactor = 1, 1    # expansion using rectzoom
  198.         # Colormap info
  199.         self.c0bits = 8        # bits in first color dimension
  200.         self.c1bits = 0        # bits in second color dimension
  201.         self.c2bits = 0        # bits in third color dimension
  202.         self.offset = 0        # colormap index offset (XXX ???)
  203.         self.chrompack = 0    # set if separate chrominance data
  204.         self.setderived()
  205.         self.decompressor = None
  206.  
  207.     # Freeze the parameters (disallow changes)
  208.  
  209.     def freeze(self):
  210.         self.frozen = 1
  211.  
  212.     # Unfreeze the parameters (allow changes)
  213.  
  214.     def unfreeze(self):
  215.         self.frozen = 0
  216.  
  217.     # Set some values derived from the standard info values
  218.  
  219.     def setderived(self):
  220.         if self.frozen: raise AssertError
  221.         if bitsperpixel.has_key(self.format):
  222.             self.bpp = bitsperpixel[self.format]
  223.         else:
  224.             self.bpp = 0
  225.         xpf, ypf = self.packfactor
  226.         self.xpf = abs(xpf)
  227.         self.ypf = abs(ypf)
  228.         self.mirror_image = (xpf < 0)
  229.         self.upside_down = (ypf < 0)
  230.         self.realwidth = self.width / self.xpf
  231.         self.realheight = self.height / self.ypf
  232.  
  233.     # Set colormap info
  234.  
  235.     def setcmapinfo(self):
  236.         stuff = 0, 0, 0, 0, 0
  237.         if self.format in ('rgb8', 'grey'):
  238.             stuff = 8, 0, 0, 0, 0
  239.         if self.format == 'grey4':
  240.             stuff = 4, 0, 0, 0, 0
  241.         if self.format == 'grey2':
  242.             stuff = 2, 0, 0, 0, 0
  243.         if self.format == 'mono':
  244.             stuff = 1, 0, 0, 0, 0
  245.         self.c0bits, self.c1bits, self.c2bits, \
  246.               self.offset, self.chrompack = stuff
  247.  
  248.     # Set the frame width and height (e.g. from gl.getsize())
  249.  
  250.     def setsize(self, width, height):
  251.         if self.frozen: raise CallError
  252.         width = (width/self.xpf)*self.xpf
  253.         height = (height/self.ypf)*self.ypf
  254.         self.width, self.height = width, height
  255.         self.setderived()
  256.  
  257.     # Retrieve the frame width and height (e.g. for gl.prefsize())
  258.  
  259.     def getsize(self):
  260.         return (self.width, self.height)
  261.  
  262.     # Set the format
  263.  
  264.     def setformat(self, format):
  265.         if self.frozen: raise CallError
  266.         self.format = format
  267.         self.setderived()
  268.         self.setcmapinfo()
  269.  
  270.     # Get the format
  271.  
  272.     def getformat(self):
  273.         return self.format
  274.  
  275.     # Set the packfactor
  276.  
  277.     def setpf(self, pf):
  278.         if self.frozen: raise CallError
  279.         if type(pf) == type(1):
  280.             pf = (pf, pf)
  281.         if type(pf) is not type(()) or len(pf) <> 2: raise CallError
  282.         self.packfactor = pf
  283.         self.setderived()
  284.  
  285.     # Get the packfactor
  286.  
  287.     def getpf(self):
  288.         return self.packfactor
  289.  
  290.     # Set all parameters
  291.  
  292.     def setinfo(self, values):
  293.         if self.frozen: raise CallError
  294.         self.setformat(values[0])
  295.         self.setpf(values[3])
  296.         self.setsize(values[1], values[2])
  297.         (self.c0bits, self.c1bits, self.c2bits, \
  298.               self.offset, self.chrompack) = values[4:9]
  299.         if self.format == 'compress' and len(values) > 9:
  300.             self.compressheader = values[9]
  301.         self.setderived()
  302.  
  303.     # Retrieve all parameters in a format suitable for a subsequent
  304.     # call to setinfo()
  305.  
  306.     def getinfo(self):
  307.         return (self.format, self.width, self.height, self.packfactor,\
  308.             self.c0bits, self.c1bits, self.c2bits, self.offset, \
  309.             self.chrompack)
  310.  
  311.     def getcompressheader(self):
  312.         return self.compressheader
  313.  
  314.     def setcompressheader(self, ch):
  315.         self.compressheader = ch
  316.  
  317.     # Write the relevant bits to stdout
  318.  
  319.     def printinfo(self):
  320.         print 'Format:  ', self.format
  321.         print 'Size:    ', self.width, 'x', self.height
  322.         print 'Pack:    ', self.packfactor, '; chrom:', self.chrompack
  323.         print 'Bpp:     ', self.bpp
  324.         print 'Bits:    ', self.c0bits, self.c1bits, self.c2bits
  325.         print 'Offset:  ', self.offset
  326.  
  327.     # Calculate data size, if possible
  328.     # (Not counting frame header or cdata size)
  329.  
  330.     def calcframesize(self):
  331.         if not self.bpp: raise CallError
  332.         size = self.width/self.xpf * self.height/self.ypf
  333.         size = (size * self.bpp + 7) / 8
  334.         return size
  335.  
  336.     # Decompress a possibly compressed frame. This method is here
  337.     # since you sometimes want to use it on a VFile instance and sometimes
  338.     # on a Displayer instance.
  339.     #
  340.     # XXXX This should also handle jpeg. Actually, the whole mechanism
  341.     # should be much more of 'ihave/iwant' style, also allowing you to
  342.     # read, say, greyscale images from a color movie.
  343.     
  344.     def decompress(self, data):
  345.         if self.format <> 'compress':
  346.             return data
  347.         if not self.decompressor:
  348.             import cl, CL
  349.             scheme = cl.QueryScheme(self.compressheader)
  350.             self.decompressor = cl.OpenDecompressor(scheme)
  351.             headersize = self.decompressor.ReadHeader(self.compressheader)
  352.             width = self.decompressor.GetParam(CL.IMAGE_WIDTH)
  353.             height = self.decompressor.GetParam(CL.IMAGE_HEIGHT)
  354.             params = [CL.ORIGINAL_FORMAT, CL.RGBX, \
  355.                   CL.ORIENTATION, CL.BOTTOM_UP, \
  356.                   CL.FRAME_BUFFER_SIZE, width*height*CL.BytesPerPixel(CL.RGBX)]
  357.             self.decompressor.SetParams(params)
  358.         data = self.decompressor.Decompress(1, data)
  359.         return data
  360.  
  361.  
  362. # Class to display video frames in a window.
  363. # It is the caller's responsibility to ensure that the correct window
  364. # is current when using showframe(), initcolormap(), clear() and clearto()
  365.  
  366. class Displayer(VideoParams):
  367.  
  368.     # Initialize an instance.
  369.     # This does not need a current window
  370.  
  371.     def __init__(self):
  372.         if no_gl:
  373.             raise RuntimeError, \
  374.                   'no gl module available, so cannot display'
  375.         VideoParams.__init__(self)
  376.         # User-settable parameters
  377.         self.magnify = 1.0    # frame magnification factor
  378.         self.xorigin = 0    # x frame offset
  379.         self.yorigin = 0    # y frame offset (from bottom)
  380.         self.quiet = 0        # if set, don't print messages
  381.         self.fallback = 1    # allow fallback to grey
  382.         # Internal flags
  383.         self.colormapinited = 0    # must initialize window
  384.         self.skipchrom = 0    # don't skip chrominance data
  385.         self.color0 = None    # magic, used by clearto()
  386.         self.fixcolor0 = 0    # don't need to fix color0
  387.         self.mustunpack = (not support_packed_pixels())
  388.  
  389.     # setinfo() must reset some internal flags
  390.  
  391.     def setinfo(self, values):
  392.         VideoParams.setinfo(self, values)
  393.         self.colormapinited = 0
  394.         self.skipchrom = 0
  395.         self.color0 = None
  396.         self.fixcolor0 = 0
  397.  
  398.     # Show one frame, initializing the window if necessary
  399.  
  400.     def showframe(self, data, chromdata):
  401.         self.showpartframe(data, chromdata, \
  402.               (0,0,self.width,self.height))
  403.  
  404.     def showpartframe(self, data, chromdata, (x,y,w,h)):
  405.         pmsize = self.bpp
  406.         xpf, ypf = self.xpf, self.ypf
  407.         if self.upside_down:
  408.             gl.pixmode(GL.PM_TTOB, 1)
  409.         if self.mirror_image:
  410.             gl.pixmode(GL.PM_RTOL, 1)
  411.         if self.format in ('jpeg', 'jpeggrey'):
  412.             import jpeg
  413.             data, width, height, bytes = jpeg.decompress(data)
  414.             pmsize = bytes*8
  415.         elif self.format == 'compress':
  416.             data = self.decompress(data)
  417.             pmsize = 32
  418.         elif self.format in ('mono', 'grey4'):
  419.             if self.mustunpack:
  420.                 if self.format == 'mono':
  421.                     data = imageop.mono2grey(data, \
  422.                           w/xpf, h/ypf, 0x20, 0xdf)
  423.                 elif self.format == 'grey4':
  424.                     data = imageop.grey42grey(data, \
  425.                           w/xpf, h/ypf)
  426.                 pmsize = 8
  427.         elif self.format == 'grey2':
  428.             data = imageop.grey22grey(data, w/xpf, h/ypf)
  429.             pmsize = 8
  430.         if not self.colormapinited:
  431.             self.initcolormap()
  432.         if self.fixcolor0:
  433.             gl.mapcolor(self.color0)
  434.             self.fixcolor0 = 0
  435.         xfactor = yfactor = self.magnify
  436.         xfactor = xfactor * xpf
  437.         yfactor = yfactor * ypf
  438.         if chromdata and not self.skipchrom:
  439.             cp = self.chrompack
  440.             cx = int(x*xfactor*cp) + self.xorigin
  441.             cy = int(y*yfactor*cp) + self.yorigin
  442.             cw = (w+cp-1)/cp
  443.             ch = (h+cp-1)/cp
  444.             gl.rectzoom(xfactor*cp, yfactor*cp)
  445.             gl.pixmode(GL.PM_SIZE, 16)
  446.             gl.writemask(self.mask - ((1 << self.c0bits) - 1))
  447.             gl.lrectwrite(cx, cy, cx + cw - 1, cy + ch - 1, \
  448.                   chromdata)
  449.         #
  450.         if pmsize < 32:
  451.             gl.writemask((1 << self.c0bits) - 1)
  452.         gl.pixmode(GL.PM_SIZE, pmsize)
  453.         w = w/xpf
  454.         h = h/ypf
  455.         x = x/xpf
  456.         y = y/ypf
  457.         gl.rectzoom(xfactor, yfactor)
  458.         x = int(x*xfactor)+self.xorigin
  459.         y = int(y*yfactor)+self.yorigin
  460.         gl.lrectwrite(x, y, x + w - 1, y + h - 1, data)
  461.         gl.gflush()
  462.  
  463.     # Initialize the window: set RGB or colormap mode as required,
  464.     # fill in the colormap, and clear the window
  465.  
  466.     def initcolormap(self):
  467.         self.colormapinited = 1
  468.         self.color0 = None
  469.         self.fixcolor0 = 0
  470.         if self.format in ('rgb', 'jpeg', 'compress'):
  471.             self.set_rgbmode()
  472.             gl.RGBcolor(200, 200, 200) # XXX rather light grey
  473.             gl.clear()
  474.             return
  475.         # This only works on an Entry-level Indigo from IRIX 4.0.5
  476.         if self.format == 'rgb8' and is_entry_indigo() and \
  477.               gl.gversion() == 'GL4DLG-4.0.': # Note trailing '.'!
  478.             self.set_rgbmode()
  479.             gl.RGBcolor(200, 200, 200) # XXX rather light grey
  480.             gl.clear()
  481.             gl.pixmode(GL.PM_SIZE, 8)
  482.             return
  483.         self.set_cmode()
  484.         self.skipchrom = 0
  485.         if self.offset == 0:
  486.             self.mask = 0x7ff
  487.         else:
  488.             self.mask = 0xfff
  489.         if not self.quiet:
  490.             sys.stderr.write('Initializing color map...')
  491.         self._initcmap()
  492.         gl.clear()
  493.         if not self.quiet:
  494.             sys.stderr.write(' Done.\n')
  495.  
  496.     # Set the window in RGB mode (may be overridden for Glx window)
  497.  
  498.     def set_rgbmode(self):
  499.         gl.RGBmode()
  500.         gl.gconfig()
  501.  
  502.     # Set the window in colormap mode (may be overridden for Glx window)
  503.  
  504.     def set_cmode(self):
  505.         gl.cmode()
  506.         gl.gconfig()
  507.  
  508.     # Clear the window to a default color
  509.  
  510.     def clear(self):
  511.         if not self.colormapinited: raise CallError
  512.         if gl.getdisplaymode() in (GET.DMRGB, GET.DMRGBDOUBLE):
  513.             gl.RGBcolor(200, 200, 200) # XXX rather light grey
  514.             gl.clear()
  515.             return
  516.         gl.writemask(0xffffffff)
  517.         gl.clear()
  518.  
  519.     # Clear the window to a given RGB color.
  520.     # This may steal the first color index used; the next call to
  521.     # showframe() will restore the intended mapping for that index
  522.  
  523.     def clearto(self, r, g, b):
  524.         if not self.colormapinited: raise CallError
  525.         if gl.getdisplaymode() in (GET.DMRGB, GET.DMRGBDOUBLE):
  526.             gl.RGBcolor(r, g, b)
  527.             gl.clear()
  528.             return
  529.         index = self.color0[0]
  530.         self.fixcolor0 = 1
  531.         gl.mapcolor(index, r, g, b)
  532.         gl.writemask(0xffffffff)
  533.         gl.clear()
  534.         gl.gflush()
  535.  
  536.     # Do the hard work for initializing the colormap (internal).
  537.     # This also sets the current color to the first color index
  538.     # used -- the caller should never change this since it is used
  539.     # by clear() and clearto()
  540.  
  541.     def _initcmap(self):
  542.         map = []
  543.         if self.format in ('mono', 'grey4') and self.mustunpack:
  544.             convcolor = conv_grey
  545.         else:
  546.             convcolor = choose_conversion(self.format)
  547.         maxbits = gl.getgdesc(GL.GD_BITS_NORM_SNG_CMODE)
  548.         if maxbits > 11:
  549.             maxbits = 11
  550.         c0bits = self.c0bits
  551.         c1bits = self.c1bits
  552.         c2bits = self.c2bits
  553.         if c0bits+c1bits+c2bits > maxbits:
  554.             if self.fallback and c0bits < maxbits:
  555.                 # Cannot display frames in this mode, use grey
  556.                 self.skipchrom = 1
  557.                 c1bits = c2bits = 0
  558.                 convcolor = choose_conversion('grey')
  559.             else:
  560.                 raise Error, 'Sorry, '+`maxbits`+ \
  561.                   ' bits max on this machine'
  562.         maxc0 = 1 << c0bits
  563.         maxc1 = 1 << c1bits
  564.         maxc2 = 1 << c2bits
  565.         if self.offset == 0 and maxbits == 11:
  566.             offset = 2048
  567.         else:
  568.             offset = self.offset
  569.         if maxbits <> 11:
  570.             offset = offset & ((1<<maxbits)-1)
  571.         self.color0 = None
  572.         self.fixcolor0 = 0
  573.         for c0 in range(maxc0):
  574.             c0v = c0/float(maxc0-1)
  575.             for c1 in range(maxc1):
  576.                 if maxc1 == 1:
  577.                     c1v = 0
  578.                 else:
  579.                     c1v = c1/float(maxc1-1)
  580.                 for c2 in range(maxc2):
  581.                     if maxc2 == 1:
  582.                         c2v = 0
  583.                     else:
  584.                         c2v = c2/float(maxc2-1)
  585.                     index = offset + c0 + (c1<<c0bits) + \
  586.                         (c2 << (c0bits+c1bits))
  587.                     if index < MAXMAP:
  588.                         rv, gv, bv = \
  589.                           convcolor(c0v, c1v, c2v)
  590.                         r, g, b = int(rv*255.0), \
  591.                               int(gv*255.0), \
  592.                               int(bv*255.0)
  593.                         map.append(index, r, g, b)
  594.                         if self.color0 == None:
  595.                             self.color0 = \
  596.                                 index, r, g, b
  597.         self.install_colormap(map)
  598.         # Permanently make the first color index current
  599.         gl.color(self.color0[0])
  600.  
  601.     # Install the colormap in the window (may be overridden for Glx window)
  602.  
  603.     def install_colormap(self, map):
  604.         if not self.quiet:
  605.             sys.stderr.write(' Installing ' + `len(map)` + \
  606.                   ' entries...')
  607.         for irgb in map:
  608.             gl.mapcolor(irgb)
  609.         gl.gflush() # send the colormap changes to the X server
  610.  
  611.  
  612. # Read a CMIF video file header.
  613. # Return (version, values) where version is 0.0, 1.0, 2.0 or 3.[01],
  614. # and values is ready for setinfo().
  615. # Raise Error if there is an error in the info
  616.  
  617. def readfileheader(fp, filename):
  618.     #
  619.     # Get identifying header
  620.     #
  621.     line = fp.readline(20)
  622.     if   line == 'CMIF video 0.0\n':
  623.         version = 0.0
  624.     elif line == 'CMIF video 1.0\n':
  625.         version = 1.0
  626.     elif line == 'CMIF video 2.0\n':
  627.         version = 2.0
  628.     elif line == 'CMIF video 3.0\n':
  629.         version = 3.0
  630.     elif line == 'CMIF video 3.1\n':
  631.         version = 3.1
  632.     else:
  633.         # XXX Could be version 0.0 without identifying header
  634.         raise Error, \
  635.             filename + ': Unrecognized file header: ' + `line`[:20]
  636.     compressheader = None
  637.     #
  638.     # Get color encoding info
  639.     # (The format may change to 'rgb' later when packfactor == 0)
  640.     #
  641.     if version <= 1.0:
  642.         format = 'grey'
  643.         c0bits, c1bits, c2bits = 8, 0, 0
  644.         chrompack = 0
  645.         offset = 0
  646.     elif version == 2.0:
  647.         line = fp.readline()
  648.         try:
  649.             c0bits, c1bits, c2bits, chrompack = eval(line[:-1])
  650.         except:
  651.             raise Error, filename + ': Bad 2.0 color info'
  652.         if c1bits or c2bits:
  653.             format = 'yiq'
  654.         else:
  655.             format = 'grey'
  656.         offset = 0
  657.     elif version in (3.0, 3.1):
  658.         line = fp.readline()
  659.         try:
  660.             format, rest = eval(line[:-1])
  661.         except:
  662.             raise Error, filename + ': Bad 3.[01] color info'
  663.         if format in ('rgb', 'jpeg'):
  664.             c0bits = c1bits = c2bits = 0
  665.             chrompack = 0
  666.             offset = 0
  667.         elif format == 'compress':
  668.             c0bits = c1bits = c2bits = 0
  669.             chrompack = 0
  670.             offset = 0
  671.             compressheader = rest
  672.         elif format in ('grey', 'jpeggrey', 'mono', 'grey2', 'grey4'):
  673.             c0bits = rest
  674.             c1bits = c2bits = 0
  675.             chrompack = 0
  676.             offset = 0
  677.         else:
  678.             # XXX ought to check that the format is valid
  679.             try:
  680.                 c0bits, c1bits, c2bits, chrompack, offset = rest
  681.             except:
  682.                 raise Error, filename + ': Bad 3.[01] color info'
  683.     if format == 'xrgb8':
  684.         format = 'rgb8' # rgb8 upside-down, for X
  685.         upside_down = 1
  686.     else:
  687.         upside_down = 0
  688.     #
  689.     # Get frame geometry info
  690.     #
  691.     line = fp.readline()
  692.     try:
  693.         x = eval(line[:-1])
  694.     except:
  695.         raise Error, filename + ': Bad (w,h,pf) info'
  696.     if type(x) <> type(()):
  697.         raise Error, filename + ': Bad (w,h,pf) info'
  698.     if len(x) == 3:
  699.         width, height, packfactor = x
  700.         if packfactor == 0 and version < 3.0:
  701.             format = 'rgb'
  702.             c0bits = 0
  703.     elif len(x) == 2 and version <= 1.0:
  704.         width, height = x
  705.         packfactor = 2
  706.     else:
  707.         raise Error, filename + ': Bad (w,h,pf) info'
  708.     if type(packfactor) is type(0):
  709.         if packfactor == 0: packfactor = 1
  710.         xpf = ypf = packfactor
  711.     else:
  712.         xpf, ypf = packfactor
  713.     if upside_down:
  714.         ypf = -ypf
  715.     packfactor = (xpf, ypf)
  716.     xpf = abs(xpf)
  717.     ypf = abs(ypf)
  718.     width = (width/xpf) * xpf
  719.     height = (height/ypf) * ypf
  720.     #
  721.     # Return (version, values)
  722.     #
  723.     values = (format, width, height, packfactor, \
  724.           c0bits, c1bits, c2bits, offset, chrompack, compressheader)
  725.     return (version, values)
  726.  
  727.  
  728. # Read a *frame* header -- separate functions per version.
  729. # Return (timecode, datasize, chromdatasize).
  730. # Raise EOFError if end of data is reached.
  731. # Raise Error if data is bad.
  732.  
  733. def readv0frameheader(fp):
  734.     line = fp.readline()
  735.     if not line or line == '\n': raise EOFError
  736.     try:
  737.         t = eval(line[:-1])
  738.     except:
  739.         raise Error, 'Bad 0.0 frame header'
  740.     return (t, 0, 0)
  741.  
  742. def readv1frameheader(fp):
  743.     line = fp.readline()
  744.     if not line or line == '\n': raise EOFError
  745.     try:
  746.         t, datasize = eval(line[:-1])
  747.     except:
  748.         raise Error, 'Bad 1.0 frame header'
  749.     return (t, datasize, 0)
  750.  
  751. def readv2frameheader(fp):
  752.     line = fp.readline()
  753.     if not line or line == '\n': raise EOFError
  754.     try:
  755.         t, datasize = eval(line[:-1])
  756.     except:
  757.         raise Error, 'Bad 2.0 frame header'
  758.     return (t, datasize, 0)
  759.  
  760. def readv3frameheader(fp):
  761.     line = fp.readline()
  762.     if not line or line == '\n': raise EOFError
  763.     try:
  764.         t, datasize, chromdatasize = x = eval(line[:-1])
  765.     except:
  766.         raise Error, 'Bad 3.[01] frame header'
  767.     return x
  768.  
  769.  
  770. # Write a CMIF video file header (always version 3.1)
  771.  
  772. def writefileheader(fp, values):
  773.     (format, width, height, packfactor, \
  774.         c0bits, c1bits, c2bits, offset, chrompack) = values
  775.     #
  776.     # Write identifying header
  777.     #
  778.     fp.write('CMIF video 3.1\n')
  779.     #
  780.     # Write color encoding info
  781.     #
  782.     if format in ('rgb', 'jpeg'):
  783.         data = (format, 0)
  784.     elif format in ('grey', 'jpeggrey', 'mono', 'grey2', 'grey4'):
  785.         data = (format, c0bits)
  786.     else:
  787.         data = (format, (c0bits, c1bits, c2bits, chrompack, offset))
  788.     fp.write(`data`+'\n')
  789.     #
  790.     # Write frame geometry info
  791.     #
  792.     data = (width, height, packfactor)
  793.     fp.write(`data`+'\n')
  794.     
  795. def writecompressfileheader(fp, cheader, values):
  796.     (format, width, height, packfactor, \
  797.         c0bits, c1bits, c2bits, offset, chrompack) = values
  798.     #
  799.     # Write identifying header
  800.     #
  801.     fp.write('CMIF video 3.1\n')
  802.     #
  803.     # Write color encoding info
  804.     #
  805.     data = (format, cheader)
  806.     fp.write(`data`+'\n')
  807.     #
  808.     # Write frame geometry info
  809.     #
  810.     data = (width, height, packfactor)
  811.     fp.write(`data`+'\n')
  812.  
  813.  
  814. # Basic class for reading CMIF video files
  815.  
  816. class BasicVinFile(VideoParams):
  817.  
  818.     def __init__(self, filename):
  819.         if type(filename) != type(''):
  820.             fp = filename
  821.             filename = '???'
  822.         elif filename == '-':
  823.             fp = sys.stdin
  824.         else:
  825.             fp = open(filename, 'r')
  826.         self.initfp(fp, filename)
  827.  
  828.     def initfp(self, fp, filename):
  829.         VideoParams.__init__(self)
  830.         self.fp = fp
  831.         self.filename = filename
  832.         self.version, values = readfileheader(fp, filename)
  833.         self.setinfo(values)
  834.         self.freeze()
  835.         if self.version == 0.0:
  836.             w, h, pf = self.width, self.height, self.packfactor
  837.             if pf == 0:
  838.                 self._datasize = w*h*4
  839.             else:
  840.                 self._datasize = (w/pf) * (h/pf)
  841.             self._readframeheader = self._readv0frameheader
  842.         elif self.version == 1.0:
  843.             self._readframeheader = readv1frameheader
  844.         elif self.version == 2.0:
  845.             self._readframeheader = readv2frameheader
  846.         elif self.version in (3.0, 3.1):
  847.             self._readframeheader = readv3frameheader
  848.         else:
  849.             raise Error, \
  850.                 filename + ': Bad version: ' + `self.version`
  851.         self.framecount = 0
  852.         self.atframeheader = 1
  853.         self.eofseen = 0
  854.         self.errorseen = 0
  855.         try:
  856.             self.startpos = self.fp.tell()
  857.             self.canseek = 1
  858.         except IOError:
  859.             self.startpos = -1
  860.             self.canseek = 0
  861.  
  862.     def _readv0frameheader(self, fp):
  863.         t, ds, cs = readv0frameheader(fp)
  864.         ds = self._datasize
  865.         return (t, ds, cs)
  866.  
  867.     def close(self):
  868.         self.fp.close()
  869.         del self.fp
  870.         del self._readframeheader
  871.  
  872.     def rewind(self):
  873.         if not self.canseek:
  874.             raise Error, self.filename + ': can\'t seek'
  875.         self.fp.seek(self.startpos)
  876.         self.framecount = 0
  877.         self.atframeheader = 1
  878.         self.eofseen = 0
  879.         self.errorseen = 0
  880.  
  881.     def warmcache(self):
  882.         print '[BasicVinFile.warmcache() not implemented]'
  883.  
  884.     def printinfo(self):
  885.         print 'File:    ', self.filename
  886.         print 'Size:    ', getfilesize(self.filename)
  887.         print 'Version: ', self.version
  888.         VideoParams.printinfo(self)
  889.  
  890.     def getnextframe(self):
  891.         t, ds, cs = self.getnextframeheader()
  892.         data, cdata = self.getnextframedata(ds, cs)
  893.         return (t, data, cdata)
  894.  
  895.     def skipnextframe(self):
  896.         t, ds, cs = self.getnextframeheader()
  897.         self.skipnextframedata(ds, cs)
  898.         return t
  899.  
  900.     def getnextframeheader(self):
  901.         if self.eofseen: raise EOFError
  902.         if self.errorseen: raise CallError
  903.         if not self.atframeheader: raise CallError
  904.         self.atframeheader = 0
  905.         try:
  906.             return self._readframeheader(self.fp)
  907.         except Error, msg:
  908.             self.errorseen = 1
  909.             # Patch up the error message
  910.             raise Error, self.filename + ': ' + msg
  911.         except EOFError:
  912.             self.eofseen = 1
  913.             raise EOFError
  914.  
  915.     def getnextframedata(self, ds, cs):
  916.         if self.eofseen: raise EOFError
  917.         if self.errorseen: raise CallError
  918.         if self.atframeheader: raise CallError
  919.         if ds:
  920.             data = self.fp.read(ds)
  921.             if len(data) < ds:
  922.                 self.eofseen = 1
  923.                 raise EOFError
  924.         else:
  925.             data = ''
  926.         if cs:
  927.             cdata = self.fp.read(cs)
  928.             if len(cdata) < cs:
  929.                 self.eofseen = 1
  930.                 raise EOFError
  931.         else:
  932.             cdata = ''
  933.         self.atframeheader = 1
  934.         self.framecount = self.framecount + 1
  935.         return (data, cdata)
  936.  
  937.     def skipnextframedata(self, ds, cs):
  938.         if self.eofseen: raise EOFError
  939.         if self.errorseen: raise CallError
  940.         if self.atframeheader: raise CallError
  941.         # Note that this won't raise EOFError for a partial frame
  942.         # since there is no easy way to tell whether a seek
  943.         # ended up beyond the end of the file
  944.         if self.canseek:
  945.             self.fp.seek(ds + cs, 1) # Relative seek
  946.         else:
  947.             dummy = self.fp.read(ds + cs)
  948.             del dummy
  949.         self.atframeheader = 1
  950.         self.framecount = self.framecount + 1
  951.  
  952.  
  953. # Subroutine to return a file's size in bytes
  954.  
  955. def getfilesize(filename):
  956.     import os, stat
  957.     try:
  958.         st = os.stat(filename)
  959.         return st[stat.ST_SIZE]
  960.     except os.error:
  961.         return 0
  962.  
  963.  
  964. # Derived class implementing random access and index cached in the file
  965.  
  966. class RandomVinFile(BasicVinFile):
  967.  
  968.     def initfp(self, fp, filename):
  969.         BasicVinFile.initfp(self, fp, filename)
  970.         self.index = []
  971.  
  972.     def warmcache(self):
  973.         if len(self.index) == 0:
  974.             try:
  975.                 self.readcache()
  976.             except Error:
  977.                 self.buildcache()
  978.         else:
  979.             print '[RandomVinFile.warmcache(): too late]'
  980.             self.rewind()
  981.  
  982.     def buildcache(self):
  983.         self.index = []
  984.         self.rewind()
  985.         while 1:
  986.             try: dummy = self.skipnextframe()
  987.             except EOFError: break
  988.         self.rewind()
  989.  
  990.     def writecache(self):
  991.         # Raises IOerror if the file is not seekable & writable!
  992.         import marshal
  993.         if len(self.index) == 0:
  994.             self.buildcache()
  995.             if len(self.index) == 0:
  996.                 raise Error, self.filename + ': No frames'
  997.         self.fp.seek(0, 2)
  998.         self.fp.write('\n/////CMIF/////\n')
  999.         pos = self.fp.tell()
  1000.         data = `pos`
  1001.         data = '\n-*-*-CMIF-*-*-\n' + data + ' '*(15-len(data)) + '\n'
  1002.         try:
  1003.             marshal.dump(self.index, self.fp)
  1004.             self.fp.write(data)
  1005.             self.fp.flush()
  1006.         finally:
  1007.             self.rewind()
  1008.  
  1009.     def readcache(self):
  1010.         # Raises Error if there is no cache in the file
  1011.         import marshal
  1012.         if len(self.index) <> 0:
  1013.             raise CallError
  1014.         self.fp.seek(-32, 2)
  1015.         data = self.fp.read()
  1016.         if data[:16] <> '\n-*-*-CMIF-*-*-\n' or data[-1:] <> '\n':
  1017.             self.rewind()
  1018.             raise Error, self.filename + ': No cache'
  1019.         pos = eval(data[16:-1])
  1020.         self.fp.seek(pos)
  1021.         try:
  1022.             self.index = marshal.load(self.fp)
  1023.         except TypeError:
  1024.             self.rewind()
  1025.             raise Error, self.filename + ': Bad cache'
  1026.         self.rewind()
  1027.  
  1028.     def getnextframeheader(self):
  1029.         if self.framecount < len(self.index):
  1030.             return self._getindexframeheader(self.framecount)
  1031.         if self.framecount > len(self.index):
  1032.             raise AssertError, \
  1033.                 'managed to bypass index?!?'
  1034.         rv = BasicVinFile.getnextframeheader(self)
  1035.         if self.canseek:
  1036.             pos = self.fp.tell()
  1037.             self.index.append(rv, pos)
  1038.         return rv
  1039.  
  1040.     def getrandomframe(self, i):
  1041.         t, ds, cs = self.getrandomframeheader(i)
  1042.         data, cdata = self.getnextframedata(ds, cs)
  1043.         return t, data, cdata
  1044.  
  1045.     def getrandomframeheader(self, i):
  1046.         if i < 0: raise ValueError, 'negative frame index'
  1047.         if not self.canseek:
  1048.             raise Error, self.filename + ': can\'t seek'
  1049.         if i < len(self.index):
  1050.             return self._getindexframeheader(i)
  1051.         if len(self.index) > 0:
  1052.             rv = self.getrandomframeheader(len(self.index)-1)
  1053.         else:
  1054.             self.rewind()
  1055.             rv = self.getnextframeheader()
  1056.         while i > self.framecount:
  1057.             self.skipnextframedata()
  1058.             rv = self.getnextframeheader()
  1059.         return rv
  1060.  
  1061.     def _getindexframeheader(self, i):
  1062.         (rv, pos) = self.index[i]
  1063.         self.fp.seek(pos)
  1064.         self.framecount = i
  1065.         self.atframeheader = 0
  1066.         self.eofseen = 0
  1067.         self.errorseen = 0
  1068.         return rv
  1069.  
  1070.  
  1071. # Basic class for writing CMIF video files
  1072.  
  1073. class BasicVoutFile(VideoParams):
  1074.  
  1075.     def __init__(self, filename):
  1076.         if type(filename) != type(''):
  1077.             fp = filename
  1078.             filename = '???'
  1079.         elif filename == '-':
  1080.             fp = sys.stdout
  1081.         else:
  1082.             fp = open(filename, 'w')
  1083.         self.initfp(fp, filename)
  1084.  
  1085.     def initfp(self, fp, filename):
  1086.         VideoParams.__init__(self)
  1087.         self.fp = fp
  1088.         self.filename = filename
  1089.         self.version = 3.1 # In case anyone inquries
  1090.  
  1091.     def flush(self):
  1092.         self.fp.flush()
  1093.  
  1094.     def close(self):
  1095.         self.fp.close()
  1096.         del self.fp
  1097.  
  1098.     def prealloc(self, nframes):
  1099.         if not self.frozen: raise CallError
  1100.         data = '\xff' * (self.calcframesize() + 64)
  1101.         pos = self.fp.tell()
  1102.         for i in range(nframes):
  1103.             self.fp.write(data)
  1104.         self.fp.seek(pos)
  1105.  
  1106.     def writeheader(self):
  1107.         if self.frozen: raise CallError
  1108.         if self.format == 'compress':
  1109.             writecompressfileheader(self.fp, self.compressheader, \
  1110.                   self.getinfo())
  1111.         else:
  1112.             writefileheader(self.fp, self.getinfo())
  1113.         self.freeze()
  1114.         self.atheader = 1
  1115.         self.framecount = 0
  1116.  
  1117.     def rewind(self):
  1118.         self.fp.seek(0)
  1119.         self.unfreeze()
  1120.         self.atheader = 1
  1121.         self.framecount = 0
  1122.  
  1123.     def printinfo(self):
  1124.         print 'File:    ', self.filename
  1125.         VideoParams.printinfo(self)
  1126.  
  1127.     def writeframe(self, t, data, cdata):
  1128.         if data: ds = len(data)
  1129.         else: ds = 0
  1130.         if cdata: cs = len(cdata)
  1131.         else: cs = 0
  1132.         self.writeframeheader(t, ds, cs)
  1133.         self.writeframedata(data, cdata)
  1134.  
  1135.     def writeframeheader(self, t, ds, cs):
  1136.         if not self.frozen: self.writeheader()
  1137.         if not self.atheader: raise CallError
  1138.         data = `(t, ds, cs)`
  1139.         n = len(data)
  1140.         if n < 63: data = data + ' '*(63-n)
  1141.         self.fp.write(data + '\n')
  1142.         self.atheader = 0
  1143.  
  1144.     def writeframedata(self, data, cdata):
  1145.         if not self.frozen or self.atheader: raise CallError
  1146.         if data: self.fp.write(data)
  1147.         if cdata: self.fp.write(cdata)
  1148.         self.atheader = 1
  1149.         self.framecount = self.framecount + 1
  1150.  
  1151.  
  1152. # Classes that combine files with displayers:
  1153.  
  1154. class VinFile(RandomVinFile, Displayer):
  1155.  
  1156.     def initfp(self, fp, filename):
  1157.         Displayer.__init__(self)
  1158.         RandomVinFile.initfp(self, fp, filename)
  1159.  
  1160.     def shownextframe(self):
  1161.         t, data, cdata = self.getnextframe()
  1162.         self.showframe(data, cdata)
  1163.         return t
  1164.  
  1165.  
  1166. class VoutFile(BasicVoutFile, Displayer):
  1167.  
  1168.     def initfp(self, fp, filename):
  1169.         Displayer.__init__(self)
  1170. ##        Grabber.__init__(self) # XXX not needed
  1171.         BasicVoutFile.initfp(self, fp, filename)
  1172.  
  1173.  
  1174. # Simple test program (VinFile only)
  1175.  
  1176. def test():
  1177.     import time
  1178.     if sys.argv[1:]: filename = sys.argv[1]
  1179.     else: filename = 'film.video'
  1180.     vin = VinFile(filename)
  1181.     vin.printinfo()
  1182.     gl.foreground()
  1183.     gl.prefsize(vin.getsize())
  1184.     wid = gl.winopen(filename)
  1185.     vin.initcolormap()
  1186.     t0 = time.time()
  1187.     while 1:
  1188.         try: t, data, cdata = vin.getnextframe()
  1189.         except EOFError: break
  1190.         dt = t0 + t - time.time()
  1191.         if dt > 0: time.time(dt)
  1192.         vin.showframe(data, cdata)
  1193.     time.sleep(2)
  1194.