| # Classes to read and write CMIF video files. |
| # (For a description of the CMIF video format, see cmif-file.ms.) |
| |
| |
| # Layers of functionality: |
| # |
| # VideoParams: maintain essential parameters of a video file |
| # Displayer: display a frame in a window (with some extra parameters) |
| # BasicVinFile: read a CMIF video file |
| # BasicVoutFile: write a CMIF video file |
| # VinFile: BasicVinFile + Displayer |
| # VoutFile: BasicVoutFile + Displayer |
| # |
| # XXX Future extension: |
| # BasicVinoutFile: supports overwriting of individual frames |
| |
| |
| # Imported modules |
| |
| import sys |
| try: |
| import gl |
| import GL |
| import GET |
| no_gl = 0 |
| except ImportError: |
| no_gl = 1 |
| import colorsys |
| import imageop |
| |
| |
| # Exception raised for various occasions |
| |
| Error = 'VFile.Error' # file format errors |
| CallError = 'VFile.CallError' # bad call |
| AssertError = 'VFile.AssertError' # internal malfunction |
| |
| |
| # Max nr. of colormap entries to use |
| |
| MAXMAP = 4096 - 256 |
| |
| |
| # Parametrizations of colormap handling based on color system. |
| # (These functions are used via eval with a constructed argument!) |
| |
| def conv_grey(l, x, y): |
| return colorsys.yiq_to_rgb(l, 0, 0) |
| |
| def conv_grey4(l, x, y): |
| return colorsys.yiq_to_rgb(l*17, 0, 0) |
| |
| def conv_mono(l, x, y): |
| return colorsys.yiq_to_rgb(l*255, 0, 0) |
| |
| def conv_yiq(y, i, q): |
| return colorsys.yiq_to_rgb(y, (i-0.5)*1.2, q-0.5) |
| |
| def conv_hls(l, h, s): |
| return colorsys.hls_to_rgb(h, l, s) |
| |
| def conv_hsv(v, h, s): |
| return colorsys.hsv_to_rgb(h, s, v) |
| |
| def conv_rgb(r, g, b): |
| raise Error, 'Attempt to make RGB colormap' |
| |
| def conv_rgb8(rgb, d1, d2): |
| rgb = int(rgb*255.0) |
| r = (rgb >> 5) & 0x07 |
| g = (rgb ) & 0x07 |
| b = (rgb >> 3) & 0x03 |
| return (r/7.0, g/7.0, b/3.0) |
| |
| def conv_jpeg(r, g, b): |
| raise Error, 'Attempt to make RGB colormap (jpeg)' |
| |
| conv_jpeggrey = conv_grey |
| conv_grey2 = conv_grey |
| |
| |
| # Choose one of the above based upon a color system name |
| |
| def choose_conversion(format): |
| try: |
| return eval('conv_' + format) |
| except: |
| raise Error, 'Unknown color system: ' + `format` |
| |
| |
| # Inverses of the above |
| |
| def inv_grey(r, g, b): |
| y, i, q = colorsys.rgb_to_yiq(r, g, b) |
| return y, 0, 0 |
| |
| def inv_yiq(r, g, b): |
| y, i, q = colorsys.rgb_to_yiq(r, g, b) |
| return y, i/1.2 + 0.5, q + 0.5 |
| |
| def inv_hls(r, g, b): |
| h, l, s = colorsys.rgb_to_hls(r, g, b) |
| return l, h, s |
| |
| def inv_hsv(r, g, b): |
| h, s, v = colorsys.rgb_to_hsv(r, g, b) |
| return v, h, s |
| |
| def inv_rgb(r, g, b): |
| raise Error, 'Attempt to invert RGB colormap' |
| |
| def inv_rgb8(r, g, b): |
| r = int(r*7.0) |
| g = int(g*7.0) |
| b = int(b*7.0) |
| rgb = ((r&7) << 5) | ((b&3) << 3) | (g&7) |
| return rgb / 255.0, 0, 0 |
| |
| def inv_jpeg(r, g, b): |
| raise Error, 'Attempt to invert RGB colormap (jpeg)' |
| |
| inv_jpeggrey = inv_grey |
| |
| |
| # Choose one of the above based upon a color system name |
| |
| def choose_inverse(format): |
| try: |
| return eval('inv_' + format) |
| except: |
| raise Error, 'Unknown color system: ' + `format` |
| |
| |
| # Predicate to see whether this is an entry level (non-XS) Indigo. |
| # If so we can lrectwrite 8-bit wide pixels into a window in RGB mode |
| |
| def is_entry_indigo(): |
| # XXX hack, hack. We should call gl.gversion() but that doesn't |
| # exist in earlier Python versions. Therefore we check the number |
| # of bitplanes *and* the size of the monitor. |
| xmax = gl.getgdesc(GL.GD_XPMAX) |
| if xmax <> 1024: return 0 |
| ymax = gl.getgdesc(GL.GD_YPMAX) |
| if ymax != 768: return 0 |
| r = gl.getgdesc(GL.GD_BITS_NORM_SNG_RED) |
| g = gl.getgdesc(GL.GD_BITS_NORM_SNG_GREEN) |
| b = gl.getgdesc(GL.GD_BITS_NORM_SNG_BLUE) |
| return (r, g, b) == (3, 3, 2) |
| |
| |
| # Predicate to see whether this machine supports pixmode(PM_SIZE) with |
| # values 1 or 4. |
| # |
| # XXX Temporarily disabled, since it is unclear which machines support |
| # XXX which pixelsizes. |
| # |
| # XXX The XS appears to support 4 bit pixels, but (looking at osview) it |
| # XXX seems as if the conversion is done by the kernel (unpacking ourselves |
| # XXX is faster than using PM_SIZE=4) |
| |
| def support_packed_pixels(): |
| return 0 # To be architecture-dependent |
| |
| |
| |
| # Tables listing bits per pixel for some formats |
| |
| bitsperpixel = { \ |
| 'rgb': 32, \ |
| 'rgb8': 8, \ |
| 'grey': 8, \ |
| 'grey4': 4, \ |
| 'grey2': 2, \ |
| 'mono': 1, \ |
| 'compress': 32, \ |
| } |
| |
| bppafterdecomp = {'jpeg': 32, 'jpeggrey': 8} |
| |
| |
| # Base class to manage video format parameters |
| |
| class VideoParams: |
| |
| # Initialize an instance. |
| # Set all parameters to something decent |
| # (except width and height are set to zero) |
| |
| def __init__(self): |
| # Essential parameters |
| self.frozen = 0 # if set, can't change parameters |
| self.format = 'grey' # color system used |
| # Choose from: grey, rgb, rgb8, hsv, yiq, hls, jpeg, jpeggrey, |
| # mono, grey2, grey4 |
| self.width = 0 # width of frame |
| self.height = 0 # height of frame |
| self.packfactor = 1, 1 # expansion using rectzoom |
| # Colormap info |
| self.c0bits = 8 # bits in first color dimension |
| self.c1bits = 0 # bits in second color dimension |
| self.c2bits = 0 # bits in third color dimension |
| self.offset = 0 # colormap index offset (XXX ???) |
| self.chrompack = 0 # set if separate chrominance data |
| self.setderived() |
| self.decompressor = None |
| |
| # Freeze the parameters (disallow changes) |
| |
| def freeze(self): |
| self.frozen = 1 |
| |
| # Unfreeze the parameters (allow changes) |
| |
| def unfreeze(self): |
| self.frozen = 0 |
| |
| # Set some values derived from the standard info values |
| |
| def setderived(self): |
| if self.frozen: raise AssertError |
| if bitsperpixel.has_key(self.format): |
| self.bpp = bitsperpixel[self.format] |
| else: |
| self.bpp = 0 |
| xpf, ypf = self.packfactor |
| self.xpf = abs(xpf) |
| self.ypf = abs(ypf) |
| self.mirror_image = (xpf < 0) |
| self.upside_down = (ypf < 0) |
| self.realwidth = self.width / self.xpf |
| self.realheight = self.height / self.ypf |
| |
| # Set colormap info |
| |
| def setcmapinfo(self): |
| stuff = 0, 0, 0, 0, 0 |
| if self.format in ('rgb8', 'grey'): |
| stuff = 8, 0, 0, 0, 0 |
| if self.format == 'grey4': |
| stuff = 4, 0, 0, 0, 0 |
| if self.format == 'grey2': |
| stuff = 2, 0, 0, 0, 0 |
| if self.format == 'mono': |
| stuff = 1, 0, 0, 0, 0 |
| self.c0bits, self.c1bits, self.c2bits, \ |
| self.offset, self.chrompack = stuff |
| |
| # Set the frame width and height (e.g. from gl.getsize()) |
| |
| def setsize(self, width, height): |
| if self.frozen: raise CallError |
| width = (width/self.xpf)*self.xpf |
| height = (height/self.ypf)*self.ypf |
| self.width, self.height = width, height |
| self.setderived() |
| |
| # Retrieve the frame width and height (e.g. for gl.prefsize()) |
| |
| def getsize(self): |
| return (self.width, self.height) |
| |
| # Set the format |
| |
| def setformat(self, format): |
| if self.frozen: raise CallError |
| self.format = format |
| self.setderived() |
| self.setcmapinfo() |
| |
| # Get the format |
| |
| def getformat(self): |
| return self.format |
| |
| # Set the packfactor |
| |
| def setpf(self, pf): |
| if self.frozen: raise CallError |
| if type(pf) == type(1): |
| pf = (pf, pf) |
| if type(pf) is not type(()) or len(pf) <> 2: raise CallError |
| self.packfactor = pf |
| self.setderived() |
| |
| # Get the packfactor |
| |
| def getpf(self): |
| return self.packfactor |
| |
| # Set all parameters |
| |
| def setinfo(self, values): |
| if self.frozen: raise CallError |
| self.setformat(values[0]) |
| self.setpf(values[3]) |
| self.setsize(values[1], values[2]) |
| (self.c0bits, self.c1bits, self.c2bits, \ |
| self.offset, self.chrompack) = values[4:9] |
| if self.format == 'compress' and len(values) > 9: |
| self.compressheader = values[9] |
| self.setderived() |
| |
| # Retrieve all parameters in a format suitable for a subsequent |
| # call to setinfo() |
| |
| def getinfo(self): |
| return (self.format, self.width, self.height, self.packfactor,\ |
| self.c0bits, self.c1bits, self.c2bits, self.offset, \ |
| self.chrompack) |
| |
| def getcompressheader(self): |
| return self.compressheader |
| |
| def setcompressheader(self, ch): |
| self.compressheader = ch |
| |
| # Write the relevant bits to stdout |
| |
| def printinfo(self): |
| print 'Format: ', self.format |
| print 'Size: ', self.width, 'x', self.height |
| print 'Pack: ', self.packfactor, '; chrom:', self.chrompack |
| print 'Bpp: ', self.bpp |
| print 'Bits: ', self.c0bits, self.c1bits, self.c2bits |
| print 'Offset: ', self.offset |
| |
| # Calculate data size, if possible |
| # (Not counting frame header or cdata size) |
| |
| def calcframesize(self): |
| if not self.bpp: raise CallError |
| size = self.width/self.xpf * self.height/self.ypf |
| size = (size * self.bpp + 7) / 8 |
| return size |
| |
| # Decompress a possibly compressed frame. This method is here |
| # since you sometimes want to use it on a VFile instance and sometimes |
| # on a Displayer instance. |
| # |
| # XXXX This should also handle jpeg. Actually, the whole mechanism |
| # should be much more of 'ihave/iwant' style, also allowing you to |
| # read, say, greyscale images from a color movie. |
| |
| def decompress(self, data): |
| if self.format <> 'compress': |
| return data |
| if not self.decompressor: |
| import cl |
| scheme = cl.QueryScheme(self.compressheader) |
| self.decompressor = cl.OpenDecompressor(scheme) |
| headersize = self.decompressor.ReadHeader(self.compressheader) |
| width = self.decompressor.GetParam(cl.IMAGE_WIDTH) |
| height = self.decompressor.GetParam(cl.IMAGE_HEIGHT) |
| params = [cl.ORIGINAL_FORMAT, cl.RGBX, \ |
| cl.ORIENTATION, cl.BOTTOM_UP, \ |
| cl.FRAME_BUFFER_SIZE, width*height*cl.BytesPerPixel(cl.RGBX)] |
| self.decompressor.SetParams(params) |
| data = self.decompressor.Decompress(1, data) |
| return data |
| |
| |
| # Class to display video frames in a window. |
| # It is the caller's responsibility to ensure that the correct window |
| # is current when using showframe(), initcolormap(), clear() and clearto() |
| |
| class Displayer(VideoParams): |
| |
| # Initialize an instance. |
| # This does not need a current window |
| |
| def __init__(self): |
| if no_gl: |
| raise RuntimeError, \ |
| 'no gl module available, so cannot display' |
| VideoParams.__init__(self) |
| # User-settable parameters |
| self.magnify = 1.0 # frame magnification factor |
| self.xorigin = 0 # x frame offset |
| self.yorigin = 0 # y frame offset (from bottom) |
| self.quiet = 0 # if set, don't print messages |
| self.fallback = 1 # allow fallback to grey |
| # Internal flags |
| self.colormapinited = 0 # must initialize window |
| self.skipchrom = 0 # don't skip chrominance data |
| self.color0 = None # magic, used by clearto() |
| self.fixcolor0 = 0 # don't need to fix color0 |
| self.mustunpack = (not support_packed_pixels()) |
| |
| # setinfo() must reset some internal flags |
| |
| def setinfo(self, values): |
| VideoParams.setinfo(self, values) |
| self.colormapinited = 0 |
| self.skipchrom = 0 |
| self.color0 = None |
| self.fixcolor0 = 0 |
| |
| # Show one frame, initializing the window if necessary |
| |
| def showframe(self, data, chromdata): |
| self.showpartframe(data, chromdata, \ |
| (0,0,self.width,self.height)) |
| |
| def showpartframe(self, data, chromdata, (x,y,w,h)): |
| pmsize = self.bpp |
| xpf, ypf = self.xpf, self.ypf |
| if self.upside_down: |
| gl.pixmode(GL.PM_TTOB, 1) |
| if self.mirror_image: |
| gl.pixmode(GL.PM_RTOL, 1) |
| if self.format in ('jpeg', 'jpeggrey'): |
| import jpeg |
| data, width, height, bytes = jpeg.decompress(data) |
| pmsize = bytes*8 |
| elif self.format == 'compress': |
| data = self.decompress(data) |
| pmsize = 32 |
| elif self.format in ('mono', 'grey4'): |
| if self.mustunpack: |
| if self.format == 'mono': |
| data = imageop.mono2grey(data, \ |
| w/xpf, h/ypf, 0x20, 0xdf) |
| elif self.format == 'grey4': |
| data = imageop.grey42grey(data, \ |
| w/xpf, h/ypf) |
| pmsize = 8 |
| elif self.format == 'grey2': |
| data = imageop.grey22grey(data, w/xpf, h/ypf) |
| pmsize = 8 |
| if not self.colormapinited: |
| self.initcolormap() |
| if self.fixcolor0: |
| gl.mapcolor(self.color0) |
| self.fixcolor0 = 0 |
| xfactor = yfactor = self.magnify |
| xfactor = xfactor * xpf |
| yfactor = yfactor * ypf |
| if chromdata and not self.skipchrom: |
| cp = self.chrompack |
| cx = int(x*xfactor*cp) + self.xorigin |
| cy = int(y*yfactor*cp) + self.yorigin |
| cw = (w+cp-1)/cp |
| ch = (h+cp-1)/cp |
| gl.rectzoom(xfactor*cp, yfactor*cp) |
| gl.pixmode(GL.PM_SIZE, 16) |
| gl.writemask(self.mask - ((1 << self.c0bits) - 1)) |
| gl.lrectwrite(cx, cy, cx + cw - 1, cy + ch - 1, \ |
| chromdata) |
| # |
| if pmsize < 32: |
| gl.writemask((1 << self.c0bits) - 1) |
| gl.pixmode(GL.PM_SIZE, pmsize) |
| w = w/xpf |
| h = h/ypf |
| x = x/xpf |
| y = y/ypf |
| gl.rectzoom(xfactor, yfactor) |
| x = int(x*xfactor)+self.xorigin |
| y = int(y*yfactor)+self.yorigin |
| gl.lrectwrite(x, y, x + w - 1, y + h - 1, data) |
| gl.gflush() |
| |
| # Initialize the window: set RGB or colormap mode as required, |
| # fill in the colormap, and clear the window |
| |
| def initcolormap(self): |
| self.colormapinited = 1 |
| self.color0 = None |
| self.fixcolor0 = 0 |
| if self.format in ('rgb', 'jpeg', 'compress'): |
| self.set_rgbmode() |
| gl.RGBcolor(200, 200, 200) # XXX rather light grey |
| gl.clear() |
| return |
| # This only works on an Entry-level Indigo from IRIX 4.0.5 |
| if self.format == 'rgb8' and is_entry_indigo() and \ |
| gl.gversion() == 'GL4DLG-4.0.': # Note trailing '.'! |
| self.set_rgbmode() |
| gl.RGBcolor(200, 200, 200) # XXX rather light grey |
| gl.clear() |
| gl.pixmode(GL.PM_SIZE, 8) |
| return |
| self.set_cmode() |
| self.skipchrom = 0 |
| if self.offset == 0: |
| self.mask = 0x7ff |
| else: |
| self.mask = 0xfff |
| if not self.quiet: |
| sys.stderr.write('Initializing color map...') |
| self._initcmap() |
| gl.clear() |
| if not self.quiet: |
| sys.stderr.write(' Done.\n') |
| |
| # Set the window in RGB mode (may be overridden for Glx window) |
| |
| def set_rgbmode(self): |
| gl.RGBmode() |
| gl.gconfig() |
| |
| # Set the window in colormap mode (may be overridden for Glx window) |
| |
| def set_cmode(self): |
| gl.cmode() |
| gl.gconfig() |
| |
| # Clear the window to a default color |
| |
| def clear(self): |
| if not self.colormapinited: raise CallError |
| if gl.getdisplaymode() in (GET.DMRGB, GET.DMRGBDOUBLE): |
| gl.RGBcolor(200, 200, 200) # XXX rather light grey |
| gl.clear() |
| return |
| gl.writemask(0xffffffff) |
| gl.clear() |
| |
| # Clear the window to a given RGB color. |
| # This may steal the first color index used; the next call to |
| # showframe() will restore the intended mapping for that index |
| |
| def clearto(self, r, g, b): |
| if not self.colormapinited: raise CallError |
| if gl.getdisplaymode() in (GET.DMRGB, GET.DMRGBDOUBLE): |
| gl.RGBcolor(r, g, b) |
| gl.clear() |
| return |
| index = self.color0[0] |
| self.fixcolor0 = 1 |
| gl.mapcolor(index, r, g, b) |
| gl.writemask(0xffffffff) |
| gl.clear() |
| gl.gflush() |
| |
| # Do the hard work for initializing the colormap (internal). |
| # This also sets the current color to the first color index |
| # used -- the caller should never change this since it is used |
| # by clear() and clearto() |
| |
| def _initcmap(self): |
| map = [] |
| if self.format in ('mono', 'grey4') and self.mustunpack: |
| convcolor = conv_grey |
| else: |
| convcolor = choose_conversion(self.format) |
| maxbits = gl.getgdesc(GL.GD_BITS_NORM_SNG_CMODE) |
| if maxbits > 11: |
| maxbits = 11 |
| c0bits = self.c0bits |
| c1bits = self.c1bits |
| c2bits = self.c2bits |
| if c0bits+c1bits+c2bits > maxbits: |
| if self.fallback and c0bits < maxbits: |
| # Cannot display frames in this mode, use grey |
| self.skipchrom = 1 |
| c1bits = c2bits = 0 |
| convcolor = choose_conversion('grey') |
| else: |
| raise Error, 'Sorry, '+`maxbits`+ \ |
| ' bits max on this machine' |
| maxc0 = 1 << c0bits |
| maxc1 = 1 << c1bits |
| maxc2 = 1 << c2bits |
| if self.offset == 0 and maxbits == 11: |
| offset = 2048 |
| else: |
| offset = self.offset |
| if maxbits <> 11: |
| offset = offset & ((1<<maxbits)-1) |
| self.color0 = None |
| self.fixcolor0 = 0 |
| for c0 in range(maxc0): |
| c0v = c0/float(maxc0-1) |
| for c1 in range(maxc1): |
| if maxc1 == 1: |
| c1v = 0 |
| else: |
| c1v = c1/float(maxc1-1) |
| for c2 in range(maxc2): |
| if maxc2 == 1: |
| c2v = 0 |
| else: |
| c2v = c2/float(maxc2-1) |
| index = offset + c0 + (c1<<c0bits) + \ |
| (c2 << (c0bits+c1bits)) |
| if index < MAXMAP: |
| rv, gv, bv = \ |
| convcolor(c0v, c1v, c2v) |
| r, g, b = int(rv*255.0), \ |
| int(gv*255.0), \ |
| int(bv*255.0) |
| map.append((index, r, g, b)) |
| if self.color0 == None: |
| self.color0 = \ |
| index, r, g, b |
| self.install_colormap(map) |
| # Permanently make the first color index current |
| gl.color(self.color0[0]) |
| |
| # Install the colormap in the window (may be overridden for Glx window) |
| |
| def install_colormap(self, map): |
| if not self.quiet: |
| sys.stderr.write(' Installing ' + `len(map)` + \ |
| ' entries...') |
| for irgb in map: |
| gl.mapcolor(irgb) |
| gl.gflush() # send the colormap changes to the X server |
| |
| |
| # Read a CMIF video file header. |
| # Return (version, values) where version is 0.0, 1.0, 2.0 or 3.[01], |
| # and values is ready for setinfo(). |
| # Raise Error if there is an error in the info |
| |
| def readfileheader(fp, filename): |
| # |
| # Get identifying header |
| # |
| line = fp.readline(20) |
| if line == 'CMIF video 0.0\n': |
| version = 0.0 |
| elif line == 'CMIF video 1.0\n': |
| version = 1.0 |
| elif line == 'CMIF video 2.0\n': |
| version = 2.0 |
| elif line == 'CMIF video 3.0\n': |
| version = 3.0 |
| elif line == 'CMIF video 3.1\n': |
| version = 3.1 |
| else: |
| # XXX Could be version 0.0 without identifying header |
| raise Error, \ |
| filename + ': Unrecognized file header: ' + `line`[:20] |
| compressheader = None |
| # |
| # Get color encoding info |
| # (The format may change to 'rgb' later when packfactor == 0) |
| # |
| if version <= 1.0: |
| format = 'grey' |
| c0bits, c1bits, c2bits = 8, 0, 0 |
| chrompack = 0 |
| offset = 0 |
| elif version == 2.0: |
| line = fp.readline() |
| try: |
| c0bits, c1bits, c2bits, chrompack = eval(line[:-1]) |
| except: |
| raise Error, filename + ': Bad 2.0 color info' |
| if c1bits or c2bits: |
| format = 'yiq' |
| else: |
| format = 'grey' |
| offset = 0 |
| elif version in (3.0, 3.1): |
| line = fp.readline() |
| try: |
| format, rest = eval(line[:-1]) |
| except: |
| raise Error, filename + ': Bad 3.[01] color info' |
| if format in ('rgb', 'jpeg'): |
| c0bits = c1bits = c2bits = 0 |
| chrompack = 0 |
| offset = 0 |
| elif format == 'compress': |
| c0bits = c1bits = c2bits = 0 |
| chrompack = 0 |
| offset = 0 |
| compressheader = rest |
| elif format in ('grey', 'jpeggrey', 'mono', 'grey2', 'grey4'): |
| c0bits = rest |
| c1bits = c2bits = 0 |
| chrompack = 0 |
| offset = 0 |
| else: |
| # XXX ought to check that the format is valid |
| try: |
| c0bits, c1bits, c2bits, chrompack, offset = rest |
| except: |
| raise Error, filename + ': Bad 3.[01] color info' |
| if format == 'xrgb8': |
| format = 'rgb8' # rgb8 upside-down, for X |
| upside_down = 1 |
| else: |
| upside_down = 0 |
| # |
| # Get frame geometry info |
| # |
| line = fp.readline() |
| try: |
| x = eval(line[:-1]) |
| except: |
| raise Error, filename + ': Bad (w,h,pf) info' |
| if type(x) <> type(()): |
| raise Error, filename + ': Bad (w,h,pf) info' |
| if len(x) == 3: |
| width, height, packfactor = x |
| if packfactor == 0 and version < 3.0: |
| format = 'rgb' |
| c0bits = 0 |
| elif len(x) == 2 and version <= 1.0: |
| width, height = x |
| packfactor = 2 |
| else: |
| raise Error, filename + ': Bad (w,h,pf) info' |
| if type(packfactor) is type(0): |
| if packfactor == 0: packfactor = 1 |
| xpf = ypf = packfactor |
| else: |
| xpf, ypf = packfactor |
| if upside_down: |
| ypf = -ypf |
| packfactor = (xpf, ypf) |
| xpf = abs(xpf) |
| ypf = abs(ypf) |
| width = (width/xpf) * xpf |
| height = (height/ypf) * ypf |
| # |
| # Return (version, values) |
| # |
| values = (format, width, height, packfactor, \ |
| c0bits, c1bits, c2bits, offset, chrompack, compressheader) |
| return (version, values) |
| |
| |
| # Read a *frame* header -- separate functions per version. |
| # Return (timecode, datasize, chromdatasize). |
| # Raise EOFError if end of data is reached. |
| # Raise Error if data is bad. |
| |
| def readv0frameheader(fp): |
| line = fp.readline() |
| if not line or line == '\n': raise EOFError |
| try: |
| t = eval(line[:-1]) |
| except: |
| raise Error, 'Bad 0.0 frame header' |
| return (t, 0, 0) |
| |
| def readv1frameheader(fp): |
| line = fp.readline() |
| if not line or line == '\n': raise EOFError |
| try: |
| t, datasize = eval(line[:-1]) |
| except: |
| raise Error, 'Bad 1.0 frame header' |
| return (t, datasize, 0) |
| |
| def readv2frameheader(fp): |
| line = fp.readline() |
| if not line or line == '\n': raise EOFError |
| try: |
| t, datasize = eval(line[:-1]) |
| except: |
| raise Error, 'Bad 2.0 frame header' |
| return (t, datasize, 0) |
| |
| def readv3frameheader(fp): |
| line = fp.readline() |
| if not line or line == '\n': raise EOFError |
| try: |
| t, datasize, chromdatasize = x = eval(line[:-1]) |
| except: |
| raise Error, 'Bad 3.[01] frame header' |
| return x |
| |
| |
| # Write a CMIF video file header (always version 3.1) |
| |
| def writefileheader(fp, values): |
| (format, width, height, packfactor, \ |
| c0bits, c1bits, c2bits, offset, chrompack) = values |
| # |
| # Write identifying header |
| # |
| fp.write('CMIF video 3.1\n') |
| # |
| # Write color encoding info |
| # |
| if format in ('rgb', 'jpeg'): |
| data = (format, 0) |
| elif format in ('grey', 'jpeggrey', 'mono', 'grey2', 'grey4'): |
| data = (format, c0bits) |
| else: |
| data = (format, (c0bits, c1bits, c2bits, chrompack, offset)) |
| fp.write(`data`+'\n') |
| # |
| # Write frame geometry info |
| # |
| data = (width, height, packfactor) |
| fp.write(`data`+'\n') |
| |
| def writecompressfileheader(fp, cheader, values): |
| (format, width, height, packfactor, \ |
| c0bits, c1bits, c2bits, offset, chrompack) = values |
| # |
| # Write identifying header |
| # |
| fp.write('CMIF video 3.1\n') |
| # |
| # Write color encoding info |
| # |
| data = (format, cheader) |
| fp.write(`data`+'\n') |
| # |
| # Write frame geometry info |
| # |
| data = (width, height, packfactor) |
| fp.write(`data`+'\n') |
| |
| |
| # Basic class for reading CMIF video files |
| |
| class BasicVinFile(VideoParams): |
| |
| def __init__(self, filename): |
| if type(filename) != type(''): |
| fp = filename |
| filename = '???' |
| elif filename == '-': |
| fp = sys.stdin |
| else: |
| fp = open(filename, 'r') |
| self.initfp(fp, filename) |
| |
| def initfp(self, fp, filename): |
| VideoParams.__init__(self) |
| self.fp = fp |
| self.filename = filename |
| self.version, values = readfileheader(fp, filename) |
| self.setinfo(values) |
| self.freeze() |
| if self.version == 0.0: |
| w, h, pf = self.width, self.height, self.packfactor |
| if pf == 0: |
| self._datasize = w*h*4 |
| else: |
| self._datasize = (w/pf) * (h/pf) |
| self._readframeheader = self._readv0frameheader |
| elif self.version == 1.0: |
| self._readframeheader = readv1frameheader |
| elif self.version == 2.0: |
| self._readframeheader = readv2frameheader |
| elif self.version in (3.0, 3.1): |
| self._readframeheader = readv3frameheader |
| else: |
| raise Error, \ |
| filename + ': Bad version: ' + `self.version` |
| self.framecount = 0 |
| self.atframeheader = 1 |
| self.eofseen = 0 |
| self.errorseen = 0 |
| try: |
| self.startpos = self.fp.tell() |
| self.canseek = 1 |
| except IOError: |
| self.startpos = -1 |
| self.canseek = 0 |
| |
| def _readv0frameheader(self, fp): |
| t, ds, cs = readv0frameheader(fp) |
| ds = self._datasize |
| return (t, ds, cs) |
| |
| def close(self): |
| self.fp.close() |
| del self.fp |
| del self._readframeheader |
| |
| def rewind(self): |
| if not self.canseek: |
| raise Error, self.filename + ': can\'t seek' |
| self.fp.seek(self.startpos) |
| self.framecount = 0 |
| self.atframeheader = 1 |
| self.eofseen = 0 |
| self.errorseen = 0 |
| |
| def warmcache(self): |
| print '[BasicVinFile.warmcache() not implemented]' |
| |
| def printinfo(self): |
| print 'File: ', self.filename |
| print 'Size: ', getfilesize(self.filename) |
| print 'Version: ', self.version |
| VideoParams.printinfo(self) |
| |
| def getnextframe(self): |
| t, ds, cs = self.getnextframeheader() |
| data, cdata = self.getnextframedata(ds, cs) |
| return (t, data, cdata) |
| |
| def skipnextframe(self): |
| t, ds, cs = self.getnextframeheader() |
| self.skipnextframedata(ds, cs) |
| return t |
| |
| def getnextframeheader(self): |
| if self.eofseen: raise EOFError |
| if self.errorseen: raise CallError |
| if not self.atframeheader: raise CallError |
| self.atframeheader = 0 |
| try: |
| return self._readframeheader(self.fp) |
| except Error, msg: |
| self.errorseen = 1 |
| # Patch up the error message |
| raise Error, self.filename + ': ' + msg |
| except EOFError: |
| self.eofseen = 1 |
| raise EOFError |
| |
| def getnextframedata(self, ds, cs): |
| if self.eofseen: raise EOFError |
| if self.errorseen: raise CallError |
| if self.atframeheader: raise CallError |
| if ds: |
| data = self.fp.read(ds) |
| if len(data) < ds: |
| self.eofseen = 1 |
| raise EOFError |
| else: |
| data = '' |
| if cs: |
| cdata = self.fp.read(cs) |
| if len(cdata) < cs: |
| self.eofseen = 1 |
| raise EOFError |
| else: |
| cdata = '' |
| self.atframeheader = 1 |
| self.framecount = self.framecount + 1 |
| return (data, cdata) |
| |
| def skipnextframedata(self, ds, cs): |
| if self.eofseen: raise EOFError |
| if self.errorseen: raise CallError |
| if self.atframeheader: raise CallError |
| # Note that this won't raise EOFError for a partial frame |
| # since there is no easy way to tell whether a seek |
| # ended up beyond the end of the file |
| if self.canseek: |
| self.fp.seek(ds + cs, 1) # Relative seek |
| else: |
| dummy = self.fp.read(ds + cs) |
| del dummy |
| self.atframeheader = 1 |
| self.framecount = self.framecount + 1 |
| |
| |
| # Subroutine to return a file's size in bytes |
| |
| def getfilesize(filename): |
| import os, stat |
| try: |
| st = os.stat(filename) |
| return st[stat.ST_SIZE] |
| except os.error: |
| return 0 |
| |
| |
| # Derived class implementing random access and index cached in the file |
| |
| class RandomVinFile(BasicVinFile): |
| |
| def initfp(self, fp, filename): |
| BasicVinFile.initfp(self, fp, filename) |
| self.index = [] |
| |
| def warmcache(self): |
| if len(self.index) == 0: |
| try: |
| self.readcache() |
| except Error: |
| self.buildcache() |
| else: |
| print '[RandomVinFile.warmcache(): too late]' |
| self.rewind() |
| |
| def buildcache(self): |
| self.index = [] |
| self.rewind() |
| while 1: |
| try: dummy = self.skipnextframe() |
| except EOFError: break |
| self.rewind() |
| |
| def writecache(self): |
| # Raises IOerror if the file is not seekable & writable! |
| import marshal |
| if len(self.index) == 0: |
| self.buildcache() |
| if len(self.index) == 0: |
| raise Error, self.filename + ': No frames' |
| self.fp.seek(0, 2) |
| self.fp.write('\n/////CMIF/////\n') |
| pos = self.fp.tell() |
| data = `pos` |
| data = '\n-*-*-CMIF-*-*-\n' + data + ' '*(15-len(data)) + '\n' |
| try: |
| marshal.dump(self.index, self.fp) |
| self.fp.write(data) |
| self.fp.flush() |
| finally: |
| self.rewind() |
| |
| def readcache(self): |
| # Raises Error if there is no cache in the file |
| import marshal |
| if len(self.index) <> 0: |
| raise CallError |
| self.fp.seek(-32, 2) |
| data = self.fp.read() |
| if data[:16] <> '\n-*-*-CMIF-*-*-\n' or data[-1:] <> '\n': |
| self.rewind() |
| raise Error, self.filename + ': No cache' |
| pos = eval(data[16:-1]) |
| self.fp.seek(pos) |
| try: |
| self.index = marshal.load(self.fp) |
| except TypeError: |
| self.rewind() |
| raise Error, self.filename + ': Bad cache' |
| self.rewind() |
| |
| def getnextframeheader(self): |
| if self.framecount < len(self.index): |
| return self._getindexframeheader(self.framecount) |
| if self.framecount > len(self.index): |
| raise AssertError, \ |
| 'managed to bypass index?!?' |
| rv = BasicVinFile.getnextframeheader(self) |
| if self.canseek: |
| pos = self.fp.tell() |
| self.index.append((rv, pos)) |
| return rv |
| |
| def getrandomframe(self, i): |
| t, ds, cs = self.getrandomframeheader(i) |
| data, cdata = self.getnextframedata(ds, cs) |
| return t, data, cdata |
| |
| def getrandomframeheader(self, i): |
| if i < 0: raise ValueError, 'negative frame index' |
| if not self.canseek: |
| raise Error, self.filename + ': can\'t seek' |
| if i < len(self.index): |
| return self._getindexframeheader(i) |
| if len(self.index) > 0: |
| rv = self.getrandomframeheader(len(self.index)-1) |
| else: |
| self.rewind() |
| rv = self.getnextframeheader() |
| while i > self.framecount: |
| self.skipnextframedata() |
| rv = self.getnextframeheader() |
| return rv |
| |
| def _getindexframeheader(self, i): |
| (rv, pos) = self.index[i] |
| self.fp.seek(pos) |
| self.framecount = i |
| self.atframeheader = 0 |
| self.eofseen = 0 |
| self.errorseen = 0 |
| return rv |
| |
| |
| # Basic class for writing CMIF video files |
| |
| class BasicVoutFile(VideoParams): |
| |
| def __init__(self, filename): |
| if type(filename) != type(''): |
| fp = filename |
| filename = '???' |
| elif filename == '-': |
| fp = sys.stdout |
| else: |
| fp = open(filename, 'w') |
| self.initfp(fp, filename) |
| |
| def initfp(self, fp, filename): |
| VideoParams.__init__(self) |
| self.fp = fp |
| self.filename = filename |
| self.version = 3.1 # In case anyone inquries |
| |
| def flush(self): |
| self.fp.flush() |
| |
| def close(self): |
| self.fp.close() |
| del self.fp |
| |
| def prealloc(self, nframes): |
| if not self.frozen: raise CallError |
| data = '\xff' * (self.calcframesize() + 64) |
| pos = self.fp.tell() |
| for i in range(nframes): |
| self.fp.write(data) |
| self.fp.seek(pos) |
| |
| def writeheader(self): |
| if self.frozen: raise CallError |
| if self.format == 'compress': |
| writecompressfileheader(self.fp, self.compressheader, \ |
| self.getinfo()) |
| else: |
| writefileheader(self.fp, self.getinfo()) |
| self.freeze() |
| self.atheader = 1 |
| self.framecount = 0 |
| |
| def rewind(self): |
| self.fp.seek(0) |
| self.unfreeze() |
| self.atheader = 1 |
| self.framecount = 0 |
| |
| def printinfo(self): |
| print 'File: ', self.filename |
| VideoParams.printinfo(self) |
| |
| def writeframe(self, t, data, cdata): |
| if data: ds = len(data) |
| else: ds = 0 |
| if cdata: cs = len(cdata) |
| else: cs = 0 |
| self.writeframeheader(t, ds, cs) |
| self.writeframedata(data, cdata) |
| |
| def writeframeheader(self, t, ds, cs): |
| if not self.frozen: self.writeheader() |
| if not self.atheader: raise CallError |
| data = `(t, ds, cs)` |
| n = len(data) |
| if n < 63: data = data + ' '*(63-n) |
| self.fp.write(data + '\n') |
| self.atheader = 0 |
| |
| def writeframedata(self, data, cdata): |
| if not self.frozen or self.atheader: raise CallError |
| if data: self.fp.write(data) |
| if cdata: self.fp.write(cdata) |
| self.atheader = 1 |
| self.framecount = self.framecount + 1 |
| |
| |
| # Classes that combine files with displayers: |
| |
| class VinFile(RandomVinFile, Displayer): |
| |
| def initfp(self, fp, filename): |
| Displayer.__init__(self) |
| RandomVinFile.initfp(self, fp, filename) |
| |
| def shownextframe(self): |
| t, data, cdata = self.getnextframe() |
| self.showframe(data, cdata) |
| return t |
| |
| |
| class VoutFile(BasicVoutFile, Displayer): |
| |
| def initfp(self, fp, filename): |
| Displayer.__init__(self) |
| ## Grabber.__init__(self) # XXX not needed |
| BasicVoutFile.initfp(self, fp, filename) |
| |
| |
| # Simple test program (VinFile only) |
| |
| def test(): |
| import time |
| if sys.argv[1:]: filename = sys.argv[1] |
| else: filename = 'film.video' |
| vin = VinFile(filename) |
| vin.printinfo() |
| gl.foreground() |
| gl.prefsize(vin.getsize()) |
| wid = gl.winopen(filename) |
| vin.initcolormap() |
| t0 = time.time() |
| while 1: |
| try: t, data, cdata = vin.getnextframe() |
| except EOFError: break |
| dt = t0 + t - time.time() |
| if dt > 0: time.time(dt) |
| vin.showframe(data, cdata) |
| time.sleep(2) |