#!/usr/bin/env python ''' NAME music2png - Converts textual music notation to classically notated PNG file SYNOPSIS music2png [options] INFILE DESCRIPTION This filter reads LilyPond or ABC music notation text from the input file INFILE (or stdin if INFILE is -), converts it to classical music notation and writes it to a trimmed PNG image file. This script is a wrapper for LilyPond and ImageMagick commands. OPTIONS -f FORMAT The INFILE music format. 'abc' for ABC notation, 'ly' for LilyPond notation. Defaults to 'abc' unless source starts with backslash. -o OUTFILE The file name of the output file. If not specified the output file is named like INFILE but with a .png file name extension. -m Skip if the PNG output file is newer that than the INFILE. Compares timestamps on INFILE and OUTFILE. If INFILE is - (stdin) then compares MD5 checksum stored in file named like OUTFILE but with a .md5 file name extension. The .md5 file is created if the -m option is used and the INFILE is - (stdin). -v Verbosely print processing information to stderr. --help, -h Print this documentation. --version Print program version number. SEE ALSO lilypond(1), abc2ly(1), convert(1) AUTHOR Written by Stuart Rackham, <srackham@gmail.com> COPYING Copyright (C) 2006 Stuart Rackham. Free use of this software is granted under the terms of the GNU General Public License (GPL). ''' # Suppress warning: "the md5 module is deprecated; use hashlib instead" import warnings warnings.simplefilter('ignore',DeprecationWarning) import os, sys, tempfile, md5 VERSION = '0.1.2' # Globals. verbose = False class EApp(Exception): pass # Application specific exception. def print_stderr(line): sys.stderr.write(line + os.linesep) def print_verbose(line): if verbose: print_stderr(line) def write_file(filename, data, mode='w'): f = open(filename, mode) try: f.write(data) finally: f.close() def read_file(filename, mode='r'): f = open(filename, mode) try: return f.read() finally: f.close() def run(cmd): global verbose if not verbose: cmd += ' 2>%s' % os.devnull print_verbose('executing: %s' % cmd) if os.system(cmd): raise EApp, 'failed command: %s' % cmd def music2png(format, infile, outfile, modified): '''Convert ABC notation in file infile to cropped PNG file named outfile.''' outfile = os.path.abspath(outfile) outdir = os.path.dirname(outfile) if not os.path.isdir(outdir): raise EApp, 'directory does not exist: %s' % outdir basefile = tempfile.mktemp(dir=os.path.dirname(outfile)) temps = [basefile + ext for ext in ('.abc', '.ly', '.ps', '.midi')] skip = False if infile == '-': source = sys.stdin.read() checksum = md5.new(source).digest() filename = os.path.splitext(outfile)[0] + '.md5' if modified: if os.path.isfile(filename) and os.path.isfile(outfile) and \ checksum == read_file(filename,'rb'): skip = True else: write_file(filename, checksum, 'wb') else: if not os.path.isfile(infile): raise EApp, 'input file does not exist: %s' % infile if modified and os.path.isfile(outfile) and \ os.path.getmtime(infile) <= os.path.getmtime(outfile): skip = True source = read_file(infile) if skip: print_verbose('skipped: no change: %s' % outfile) return if format is None: if source and source.startswith('\\'): # Guess input format. format = 'ly' else: format = 'abc' # Write temporary source file. write_file('%s.%s' % (basefile,format), source) abc = basefile + '.abc' ly = basefile + '.ly' png = basefile + '.png' saved_pwd = os.getcwd() os.chdir(outdir) try: if format == 'abc': run('abc2ly -o "%s" "%s"' % (ly,abc)) run('lilypond --png -o "%s" "%s"' % (basefile,ly)) os.rename(png, outfile) finally: os.chdir(saved_pwd) # Chop the bottom 75 pixels off to get rid of the page footer then crop the # music image. The -strip option necessary because FOP does not like the # custom PNG color profile used by Lilypond. run('convert "%s" -strip -gravity South -chop 0x75 -trim "%s"' % (outfile, outfile)) for f in temps: if os.path.isfile(f): print_verbose('deleting: %s' % f) os.remove(f) def usage(msg=''): if msg: print_stderr(msg) print_stderr('\n' 'usage:\n' ' music2png [options] INFILE\n' '\n' 'options:\n' ' -f FORMAT\n' ' -o OUTFILE\n' ' -m\n' ' -v\n' ' --help\n' ' --version') def main(): # Process command line options. global verbose format = None outfile = None modified = False import getopt opts,args = getopt.getopt(sys.argv[1:], 'f:o:mhv', ['help','version']) for o,v in opts: if o in ('--help','-h'): print __doc__ sys.exit(0) if o =='--version': print('music2png version %s' % (VERSION,)) sys.exit(0) if o == '-f': format = v if o == '-o': outfile = v if o == '-m': modified = True if o == '-v': verbose = True if len(args) != 1: usage() sys.exit(1) infile = args[0] if format not in (None, 'abc', 'ly'): usage('invalid FORMAT') sys.exit(1) if outfile is None: if infile == '-': usage('OUTFILE must be specified') sys.exit(1) outfile = os.path.splitext(infile)[0] + '.png' # Do the work. music2png(format, infile, outfile, modified) # Print something to suppress asciidoc 'no output from filter' warnings. if infile == '-': sys.stdout.write(' ') if __name__ == "__main__": try: main() except SystemExit: raise except KeyboardInterrupt: sys.exit(1) except Exception, e: print_stderr("%s: %s" % (os.path.basename(sys.argv[0]), str(e))) sys.exit(1)