1 | #!/usr/bin/env python |
---|
2 | ''' |
---|
3 | NAME |
---|
4 | music2png - Converts textual music notation to classically notated PNG file |
---|
5 | |
---|
6 | SYNOPSIS |
---|
7 | music2png [options] INFILE |
---|
8 | |
---|
9 | DESCRIPTION |
---|
10 | This filter reads LilyPond or ABC music notation text from the input file |
---|
11 | INFILE (or stdin if INFILE is -), converts it to classical music notation |
---|
12 | and writes it to a trimmed PNG image file. |
---|
13 | |
---|
14 | This script is a wrapper for LilyPond and ImageMagick commands. |
---|
15 | |
---|
16 | OPTIONS |
---|
17 | -f FORMAT |
---|
18 | The INFILE music format. 'abc' for ABC notation, 'ly' for LilyPond |
---|
19 | notation. Defaults to 'abc' unless source starts with backslash. |
---|
20 | |
---|
21 | -o OUTFILE |
---|
22 | The file name of the output file. If not specified the output file is |
---|
23 | named like INFILE but with a .png file name extension. |
---|
24 | |
---|
25 | -m |
---|
26 | Skip if the PNG output file is newer that than the INFILE. |
---|
27 | Compares timestamps on INFILE and OUTFILE. If |
---|
28 | INFILE is - (stdin) then compares MD5 checksum stored in file |
---|
29 | named like OUTFILE but with a .md5 file name extension. |
---|
30 | The .md5 file is created if the -m option is used and the |
---|
31 | INFILE is - (stdin). |
---|
32 | |
---|
33 | -v |
---|
34 | Verbosely print processing information to stderr. |
---|
35 | |
---|
36 | --help, -h |
---|
37 | Print this documentation. |
---|
38 | |
---|
39 | --version |
---|
40 | Print program version number. |
---|
41 | |
---|
42 | SEE ALSO |
---|
43 | lilypond(1), abc2ly(1), convert(1) |
---|
44 | |
---|
45 | AUTHOR |
---|
46 | Written by Stuart Rackham, <srackham@gmail.com> |
---|
47 | |
---|
48 | COPYING |
---|
49 | Copyright (C) 2006 Stuart Rackham. Free use of this software is |
---|
50 | granted under the terms of the GNU General Public License (GPL). |
---|
51 | ''' |
---|
52 | |
---|
53 | # Suppress warning: "the md5 module is deprecated; use hashlib instead" |
---|
54 | import warnings |
---|
55 | warnings.simplefilter('ignore',DeprecationWarning) |
---|
56 | |
---|
57 | import os, sys, tempfile, md5 |
---|
58 | |
---|
59 | VERSION = '0.1.2' |
---|
60 | |
---|
61 | # Globals. |
---|
62 | verbose = False |
---|
63 | |
---|
64 | class EApp(Exception): pass # Application specific exception. |
---|
65 | |
---|
66 | def print_stderr(line): |
---|
67 | sys.stderr.write(line + os.linesep) |
---|
68 | |
---|
69 | def print_verbose(line): |
---|
70 | if verbose: |
---|
71 | print_stderr(line) |
---|
72 | |
---|
73 | def write_file(filename, data, mode='w'): |
---|
74 | f = open(filename, mode) |
---|
75 | try: |
---|
76 | f.write(data) |
---|
77 | finally: |
---|
78 | f.close() |
---|
79 | |
---|
80 | def read_file(filename, mode='r'): |
---|
81 | f = open(filename, mode) |
---|
82 | try: |
---|
83 | return f.read() |
---|
84 | finally: |
---|
85 | f.close() |
---|
86 | |
---|
87 | def run(cmd): |
---|
88 | global verbose |
---|
89 | if not verbose: |
---|
90 | cmd += ' 2>%s' % os.devnull |
---|
91 | print_verbose('executing: %s' % cmd) |
---|
92 | if os.system(cmd): |
---|
93 | raise EApp, 'failed command: %s' % cmd |
---|
94 | |
---|
95 | def music2png(format, infile, outfile, modified): |
---|
96 | '''Convert ABC notation in file infile to cropped PNG file named outfile.''' |
---|
97 | outfile = os.path.abspath(outfile) |
---|
98 | outdir = os.path.dirname(outfile) |
---|
99 | if not os.path.isdir(outdir): |
---|
100 | raise EApp, 'directory does not exist: %s' % outdir |
---|
101 | basefile = tempfile.mktemp(dir=os.path.dirname(outfile)) |
---|
102 | temps = [basefile + ext for ext in ('.abc', '.ly', '.ps', '.midi')] |
---|
103 | skip = False |
---|
104 | if infile == '-': |
---|
105 | source = sys.stdin.read() |
---|
106 | checksum = md5.new(source).digest() |
---|
107 | filename = os.path.splitext(outfile)[0] + '.md5' |
---|
108 | if modified: |
---|
109 | if os.path.isfile(filename) and os.path.isfile(outfile) and \ |
---|
110 | checksum == read_file(filename,'rb'): |
---|
111 | skip = True |
---|
112 | else: |
---|
113 | write_file(filename, checksum, 'wb') |
---|
114 | else: |
---|
115 | if not os.path.isfile(infile): |
---|
116 | raise EApp, 'input file does not exist: %s' % infile |
---|
117 | if modified and os.path.isfile(outfile) and \ |
---|
118 | os.path.getmtime(infile) <= os.path.getmtime(outfile): |
---|
119 | skip = True |
---|
120 | source = read_file(infile) |
---|
121 | if skip: |
---|
122 | print_verbose('skipped: no change: %s' % outfile) |
---|
123 | return |
---|
124 | if format is None: |
---|
125 | if source and source.startswith('\\'): # Guess input format. |
---|
126 | format = 'ly' |
---|
127 | else: |
---|
128 | format = 'abc' |
---|
129 | # Write temporary source file. |
---|
130 | write_file('%s.%s' % (basefile,format), source) |
---|
131 | abc = basefile + '.abc' |
---|
132 | ly = basefile + '.ly' |
---|
133 | png = basefile + '.png' |
---|
134 | saved_pwd = os.getcwd() |
---|
135 | os.chdir(outdir) |
---|
136 | try: |
---|
137 | if format == 'abc': |
---|
138 | run('abc2ly -o "%s" "%s"' % (ly,abc)) |
---|
139 | run('lilypond --png -o "%s" "%s"' % (basefile,ly)) |
---|
140 | os.rename(png, outfile) |
---|
141 | finally: |
---|
142 | os.chdir(saved_pwd) |
---|
143 | # Chop the bottom 75 pixels off to get rid of the page footer then crop the |
---|
144 | # music image. The -strip option necessary because FOP does not like the |
---|
145 | # custom PNG color profile used by Lilypond. |
---|
146 | run('convert "%s" -strip -gravity South -chop 0x75 -trim "%s"' % (outfile, outfile)) |
---|
147 | for f in temps: |
---|
148 | if os.path.isfile(f): |
---|
149 | print_verbose('deleting: %s' % f) |
---|
150 | os.remove(f) |
---|
151 | |
---|
152 | def usage(msg=''): |
---|
153 | if msg: |
---|
154 | print_stderr(msg) |
---|
155 | print_stderr('\n' |
---|
156 | 'usage:\n' |
---|
157 | ' music2png [options] INFILE\n' |
---|
158 | '\n' |
---|
159 | 'options:\n' |
---|
160 | ' -f FORMAT\n' |
---|
161 | ' -o OUTFILE\n' |
---|
162 | ' -m\n' |
---|
163 | ' -v\n' |
---|
164 | ' --help\n' |
---|
165 | ' --version') |
---|
166 | |
---|
167 | def main(): |
---|
168 | # Process command line options. |
---|
169 | global verbose |
---|
170 | format = None |
---|
171 | outfile = None |
---|
172 | modified = False |
---|
173 | import getopt |
---|
174 | opts,args = getopt.getopt(sys.argv[1:], 'f:o:mhv', ['help','version']) |
---|
175 | for o,v in opts: |
---|
176 | if o in ('--help','-h'): |
---|
177 | print __doc__ |
---|
178 | sys.exit(0) |
---|
179 | if o =='--version': |
---|
180 | print('music2png version %s' % (VERSION,)) |
---|
181 | sys.exit(0) |
---|
182 | if o == '-f': format = v |
---|
183 | if o == '-o': outfile = v |
---|
184 | if o == '-m': modified = True |
---|
185 | if o == '-v': verbose = True |
---|
186 | if len(args) != 1: |
---|
187 | usage() |
---|
188 | sys.exit(1) |
---|
189 | infile = args[0] |
---|
190 | if format not in (None, 'abc', 'ly'): |
---|
191 | usage('invalid FORMAT') |
---|
192 | sys.exit(1) |
---|
193 | if outfile is None: |
---|
194 | if infile == '-': |
---|
195 | usage('OUTFILE must be specified') |
---|
196 | sys.exit(1) |
---|
197 | outfile = os.path.splitext(infile)[0] + '.png' |
---|
198 | # Do the work. |
---|
199 | music2png(format, infile, outfile, modified) |
---|
200 | # Print something to suppress asciidoc 'no output from filter' warnings. |
---|
201 | if infile == '-': |
---|
202 | sys.stdout.write(' ') |
---|
203 | |
---|
204 | if __name__ == "__main__": |
---|
205 | try: |
---|
206 | main() |
---|
207 | except SystemExit: |
---|
208 | raise |
---|
209 | except KeyboardInterrupt: |
---|
210 | sys.exit(1) |
---|
211 | except Exception, e: |
---|
212 | print_stderr("%s: %s" % (os.path.basename(sys.argv[0]), str(e))) |
---|
213 | sys.exit(1) |
---|