#!/usr/bin/env python # $Id: pxsup2dast.py 448 2005-07-02 13:37:57Z too $ # Project X sup to dvdauthor subtitle xml file. # too ät iki piste fi # # This is currently wery picky what this expects of ProjectX .sup to contain. # Update: 2005/04/02 Not so picky anymore, but control sequence parsing is # not perfect (yet!?) # This program is released under GNU GPL. Check # http://www.fsf.org/licenses/licenses.html # to get your own copy of the GPL license. # There is much faster, C version of this available, look for pxsup2dast.c. import sys, struct, cStringIO, zlib from os import system, mkdir, rename, path def yuv2rgb(t): y, cr, cb = t r = int(y + 1.402 * (cr - 128)) g = int(y - 0.34414 * (cb - 128) - 0.71414 * (cr - 128)) b = int(y + 1.722 * (cb - 128)) if r < 0: r = 0 elif r > 255: r = 255 if g < 0: g = 0 elif g > 255: g = 255 if b < 0: b = 0 elif b > 255: b = 255 return (r, g, b) def ifopalette(ifofile): """ Extract color palette information from IFO file with VTS content""" # http://dvd.sourceforge.net/dvdinfo/ifo.html # http://dvd.sourceforge.net/dvdinfo/ifo_vts.html#pgci # http://dvd.sourceforge.net/dvdinfo/pgc.html f = file(ifofile) if f.read(12) != 'DVDVIDEO-VTS': raise Exception, '(IFO) file not of type DVDVIDEO-VTS' f.seek(0xcc) # sector offset of VTS_PGCI. off = struct.unpack('!L', f.read(4))[0] pgci = off * 0x800 # seek to offset of pgc relative to pgci # (XXX we should check there is at least one program chain) f.seek(pgci + 12) pgc = pgci + struct.unpack('!L', f.read(4))[0] # seek to palette f.seek(pgc + 0xa4) # list comprehension is my friend return [ yuv2rgb(struct.unpack('xBBB', f.read(4))) for _ in xrange(16)] # for i in xrange(16)] # print 'Color %d Y Cr CB ' % i, # print struct.unpack('xBBB', f.read(4)) class PNG4File: def __init__(self, filename, height, width, palette): self.file = file(filename, 'w') self.width = width self.nibble = -1 self.rowsleft = height self.rfile = cStringIO.StringIO() self.file.write('\x89PNG\r\n\x1a\n') data = 'IHDR' + struct.pack('!LL', width, height) + '\004\003\0\0\0' self.file.write(struct.pack('!L', len(data) - 4)) self.file.write(data) self.file.write(struct.pack('!L', zlib.crc32(data))) data = 'PLTE' + struct.pack('BBBBBBBBBBBB', palette[0][0],palette[0][1],palette[0][2], palette[1][0],palette[1][1],palette[1][2], palette[2][0],palette[2][1],palette[2][2], palette[3][0],palette[3][1],palette[3][2]) self.file.write(struct.pack('!L', len(data) - 4)) self.file.write(data) self.file.write(struct.pack('!L', zlib.crc32(data))) # XXX quick hack, first color transparent. self.file.write('\0\0\0\001' + 'tRNS' + '\0' + '\x40\xe6\xd8\x66') self.rfile.write('\0') def addPixel(self, pixel): if self.nibble < 0: self.nibble = (pixel << 4) else: self.rfile.write(chr(self.nibble | pixel)) self.nibble = -1 def endRow(self): if self.nibble >= 0: self.rfile.write(chr(self.nibble)) self.nibble = -1 self.rowsleft -= 1 if self.rowsleft > 0: self.rfile.write('\0') def close(self): data = zlib.compress(self.rfile.getvalue(), 0) self.file.write(struct.pack('!L', len(data))) data = 'IDAT' + data self.file.write(data) self.file.write(struct.pack('!L', zlib.crc32(data))) self.file.write('\0\0\0\0IEND\xae\x42\x60\x82') self.file.close() # class BMP4File: # def __init__(self, filename, height, width, palette): # self.file = f = file(filename, 'w') # self.width = width # self.nibble = -1 # self.count = 0 # self.rows = [ ] # self.rfile = cStringIO.StringIO() # # BITMAPFILEHEADER # offset = 14 + 40 + 16 * 4 # imgsize = ((width / 2 + 3) & ~ 3) * height # f.write('BM') # f.write(struct.pack('= 0: # self.rfile.write(chr(self.nibble)) # self.count += 1 # while self.count & 0x03: # self.rfile.write('\0') # self.count += 1 # self.rows.append(self.rfile) # self.rfile = cStringIO.StringIO() # def close(self): # self.rows.reverse() # for f in self.rows: # self.file.write(f.getvalue()) class NibbleBuf: def __init__(self, string): self.stringfile = cStringIO.StringIO(string) self.halfbyte = -1 def getNibble(self): if self.halfbyte >= 0: rv = self.halfbyte & 0x0f self.halfbyte = -1 return rv else: try: self.halfbyte = ord(self.stringfile.read(1)) except: self.halfbyte = 0; return self.halfbyte >> 4 def clearHalfByte(self): self.halfbyte = -1 def getline(ibuf, width, picfile): #drawbuf = [ ] col = 0 while True: # gtkspu direct copy bits = ibuf.getNibble() if (bits & 0xc) != 0: # have 4-bit code number = (bits & 0xc) >> 2 cindex = bits & 0x3 else: bits = (bits << 4) | ibuf.getNibble() if (bits & 0xf0) != 0: # have 8-bit code number = (bits & 0x3c) >> 2 cindex = bits & 0x3 else: bits = (bits << 4) | ibuf.getNibble() if (bits & 0xfc0) != 0: # have 12-bit code number = (bits & 0xfc) >> 2 cindex = bits & 0x3 else: # have 16-bit code bits = (bits << 4) | ibuf.getNibble() number = (bits & 0x3fc) >> 2 cindex = bits & 0x3 if number == 0: number = width # write "number many "cindex"s until end of row while number > 0 and col < width: picfile.addPixel(cindex) #this code, 1280x1025 and xterm -fn micro in fullscreen, urles. #if cindex == 3: # drawbuf.append('#') #elif cindex == 2: # drawbuf.append('I') #elif cindex == 1: # drawbuf.append(':') #else: # drawbuf.append(' ') number -= 1; col += 1; if col == width: ibuf.clearHalfByte() picfile.endRow() #print ''.join(drawbuf) return def makebitmap(data, w, h, top, bottom, fn, palette): top_ibuf = NibbleBuf(data[top:]) # extra data in buffer, but ... bot_ibuf = NibbleBuf(data[bottom:]) picfile = PNG4File(fn, h, w, palette) for _ in xrange(h / 2): getline(top_ibuf, w, picfile) getline(bot_ibuf, w, picfile) picfile.close() def pts2ts(pts, is_png_filename = False): h = pts / (3600 * 90000) m = pts / (60 * 90000) % 60 s = pts / 90000 % 60 hs = (pts + 450) / 900 % 100 if is_png_filename: return '%02d+%02d+%02d.%02d.png' % (h, m, s, hs) else: return '%02d:%02d:%02d.%02d' % (h, m, s, hs) # Implementation matches at least with ProjecX 0.82.1.02 def pxsubtitle(supfile, ddir, outfile, palette): """extract subtitles from ProjectX-generated .sup file. Picky!""" # Here thanks go to ProjetcX source, http://www.via.ecp.fr/~sam/doc/dvd/ # and gtkspu program in gopchop distribution f = file(supfile) if f.read(2) != 'SP': raise Exception, "Syncword missing. XXX bailing out." #transparent = '%02x%02x%02x' % (palette[0][0],palette[0][1],palette[0][2]) if path.exists(ddir + 'spumux.xml'): print 'spumux.xml exists. Skipping generation of pngfiles.' dopngs = False else: dopngs = True while True: # X.java reads 5 bytes of pts, SubPicture.java writes 4. With # 4 bytes 47721 seconds (13.25 hours) can be handled. pts, = struct.unpack('HH', f.read(4)) #print pts / 90, 'ms.', size, pack data = f.read(pack - 4) ctrl = cStringIO.StringIO(f.read(size - pack)) if f.read(2) != 'SP': if len(f.read(1)) == 0: return # EOF raise Exception, "Syncword missing. XXX bailing out." # parsing control info prev = 0 while True: date, = struct.unpack('>H', ctrl.read(2)) next, = struct.unpack('>H', ctrl.read(2)) # XXX while True: cmd = ord(ctrl.read(1)) if cmd == 0x00: # force display: continue if cmd == 0x01: # start date (read above) start = date # XXX + previous continue if cmd == 0x02: # stop date (read above) end = date # XXX + previous continue if cmd == 0x03: # palette xpalette = ctrl.read(2) continue if cmd == 0x04: # alpha channel alpha = ctrl.read(2) continue if cmd == 0x05: # coordinates coords = ctrl.read(6) x1 = (ord(coords[0]) << 4) + (ord(coords[1]) >> 4) x2 = ((ord(coords[1]) & 0xf) << 8) + ord(coords[2]) y1 = (ord(coords[3]) << 4) + (ord(coords[4]) >> 4) y2 = ((ord(coords[4]) & 0xf) << 8) + ord(coords[5]) continue if cmd == 0x06: # rle offsets top_field,bottom_field = struct.unpack('>HH', ctrl.read(4)) continue if cmd == 0xff: # end command break else: raise Execption, "%d: Unknown control sequence" % cmd if prev == next: break prev = next startpts = pts; endpts = pts + end * 1000 # other values seen: 900, 1024 sptstr = pts2ts(pts) print '\r', sptstr,; sys.stdout.flush() pic = ddir + pts2ts(pts, True) outfile.write(' \n' % (sptstr, pts2ts(endpts), pic, x1, y1) ) if dopngs: makebitmap(data, x2 - x1 + 1, y2 - y1 + 1, top_field - 4, bottom_field - 4, pic , palette) def main(): if len(sys.argv) < 3: print >> sys.stderr, \ "\nUsage: %s supfile ifofile" % sys.argv[0] print >> sys.stderr, '\n shell wildcard "*" is your friend.' sys.exit(1) print '\nThere is much faster C version of this available. It is also' print 'Further developed. I strongly suggest using it to do this work\n.' colorpalette = ifopalette(sys.argv[2]) #print colorpalette ddir = sys.argv[1].replace('.sup', '') + '.d/' try: mkdir(ddir) except: pass # XXX print '\nWriting palette (subpictures uses 4 first colors).' file(ddir + 'palette.rgb', 'w').write( '\n'.join(['%02x%02x%02x' % (r,b,g) for r,b,g in colorpalette])) print 'Starting spumux.tmp. Writing subpicture PNG files...' f = file(ddir + 'spumux.tmp', 'w') f.write('\n \n') pxsubtitle(sys.argv[1], ddir, f, colorpalette) f.write(' \n\n') print '\nClosing spumux.tmp. Renaming it to spumux.xml.' rename(ddir + 'spumux.tmp', ddir + 'spumux.xml') print 'Output files reside in' , ddir # for pychecker(1) if __name__ == '__main__': main()