theonlineoasis

This python module runs an x264 encoder and sends it frames generated from LaTeX source files or loaded as images, to output a compressed video stream.

Instantiate the class passing in a configuration list and the width and height for output (which must both be multiples of two). The configuration list has dictionaries specifying scenes as its elements. Each scene dictionary should have a key 'type' associated with either 'images' or 'static text'. The tool reads each scene in turn and renders it to the output file. Read the source code to see which keys need to be present for each of the two scene types.

More information coming soon ...

Code samples on this website come without any guarantees – use them at your own risk! I am grateful for comments or patches sent to my email address: first name.last name@cl.cam.ac.uk.

#!/opt/local/bin/python

from PIL import Image
import os

x264_path = '/path/to/x264'
yuv_fifo = '/tmp/vidauthor_fifo.yuv'
temp_dir = '/tmp/'
temp_tex_file = 'temp_texsource.tex'
temp_png_file = temp_tex_file.replace('.tex', '.png')

temp_tex_file_path = os.path.join(temp_dir, temp_tex_file)
temp_pdf_file_path = os.path.join(temp_dir, temp_tex_file.replace('.tex', '.pdf'))
temp_png_file_path = os.path.join(temp_dir, temp_tex_file.replace('.tex', '.png'))

class vidauthor:
  def __init__(self, scenes, video_width, video_height):
    self.scenes = scenes
    self.video_width = video_width
    self.video_height = video_height
    self.ar = float(self.video_height) / float(self.video_width)
  
  def tex_to_png(self, title, content_file_name):
    content = ''
    if content_file_name != None:
      content = open(content_file_name).read()
    f = open(temp_tex_file_path, 'w')
    latex_width_cm = int((float(self.video_width) / 1280.0) * 32.0)
    latex_height_cm = int(latex_width_cm * self.ar)
    f.write(r"""\documentclass{article}
      \usepackage{url}
      \pdfpagewidth %dcm
      \pdfpageheight %dcm
      \setlength{\textwidth}{%dcm}
      \setlength{\oddsidemargin}{-0.8in}
      \setlength{\evensidemargin}{\oddsidemargin}
      \setlength{\topmargin}{0mm}
      \setlength{\topmargin}{0mm}
      \setlength{\headheight}{0mm}
      \setlength{\headsep}{0mm}
      \setlength{\footskip}{0mm}
      \setlength{\parindent}{0mm}
      \renewcommand\familydefault{\sfdefault}
      \begin{document}
      \begin{center} \Huge\textbf{%s} \end{center}

      \vspace{0.3cm}

      \Huge
      %s

      \end{document}
      """ % (latex_width_cm, latex_height_cm, latex_width_cm - 1, title, content,))
    f.close()
    os.system('pdflatex -output-directory %s %s' % (temp_dir, temp_tex_file_path,))
    os.system('convert -density 600x600 %s -resize %dx -density 600x600 %s' % (temp_pdf_file_path, self.video_width, temp_png_file_path,))
    
  def generate_video(self, video_file_path):
    # Create the YUV fifo if it doesn't exist.
    if not os.path.exists(yuv_fifo):
      os.mkfifo(yuv_fifo)
    
    # Start x264.
    os.system('%s -o %s %s %dx%d &' % (x264_path, video_file_path, yuv_fifo, self.video_width, self.video_height,))
    
    f = open(yuv_fifo, 'wb')
    
    def append_image(image_file_name):
      # Open the image and convert from RGB to YCbCr
      im = Image.open(image_file_name)
      y,cb,cr = im.convert('YCbCr').split()
      
      # Down-sample chroma
      cb = cb.resize((cb.size[0] / 2, cb.size[1] / 2), Image.BILINEAR)
      cr = cr.resize((cr.size[0] / 2, cr.size[1] / 2), Image.BILINEAR)
      
      # Write raw data to the YUV file
      f.write(y.tostring("raw", "L"))
      f.write(cb.tostring("raw", "L"))
      f.write(cr.tostring("raw", "L"))
    
    # Concatenate the scenes in order
    for i, s in enumerate(self.scenes):
      if s.has_key('disable') and s['disable'] == True:
        continue
    
      if s['type'] == 'images':
        source_path = s['source path']
        frame_to_name = s['frame to name']
        present_to_frame = s['present to frame']
        present_count = s['present count']
        last_frame_no = -1
        
        present_no = 0
        for present_no in range(0, present_count):
          # Convert from the current (presentation) frame number to the frame number in the scene
          frame_no = present_to_frame(present_no)
          image_file_name = os.path.join(source_path, frame_to_name(frame_no))
          if not os.path.exists(image_file_name):
            break
          append_image(image_file_name)
        
        if s.has_key('final frame duration'):
          frame_no = present_to_frame(present_no - 1)
          image_file_name = os.path.join(source_path, frame_to_name(frame_no))
          for i in range(1, s['final frame duration'] - 1):
            append_image(image_file_name)
      elif s['type'] == 'static text':
        tex_source_path = s['source path']
        present_count = s['present count']
        
        self.tex_to_png(s['scene title'], tex_source_path)
        for i in range(0, present_count):
          append_image(temp_png_file_path)
        
    # Flush data to the YUV file
    f.close()