announce change of default behaviour and deprecation of `handle_io_errors`
[docutils/kirr.git] / sandbox / amk / abc / abc.py
blobf663cd765fc242247ac9e254b5e4ec0caa109ee3
1 """abc.py
3 Implements a 'music' directive that accepts musical passages written
4 in ABC notation. Requires that abcm2ps and Ghostscript be installed.
6 .. music::
8 % Sample file to test various features of abc2ps
9 X:1
10 T:Scale
11 M:C
12 K: clef=bass
13 "C,"C,"D,"D,
15 """
17 import os, sys, tempfile, popen2
19 from docutils import nodes
20 from docutils.parsers.rst.directives import register_directive
22 music_counter = 1
24 def ABCDirective (name, arguments, options, content, lineno,
25 content_offset, block_text, state, state_machine):
26 abc_temp = tempfile.NamedTemporaryFile("wt")
27 abc_temp.write("\n".join(content))
28 abc_temp.flush()
29 fd, ps_temp = tempfile.mkstemp()
30 global music_counter
31 output_path = state.document.settings._destination or '.'
32 output_name = os.path.join(output_path, 'music-%i.png' % music_counter)
33 music_counter += 1
34 sts1 = sts2 = sts3 = 0
35 pipe = popen2.Popen3('abcm2ps -O/dev/stdout - < %s > %s'
36 % (abc_temp.name, ps_temp),
37 capturestderr=True
39 pipe.tochild.close()
40 pipe.fromchild.read()
41 errors = pipe.childerr.read()
42 errors = parse_errors(errors, lineno)
43 sts1 = pipe.wait()
44 if sts1 == 0:
45 sts2 = os.system('gs -q -sOutputFile=%s -sDEVICE=pngmono - <%s'
46 % (output_name, ps_temp))
47 if sts2 == 0:
48 sts3 = os.system('gs -q -sOutputFile=%s -sDEVICE=pngmono - <%s'
49 % (output_name, ps_temp))
50 try:
51 os.unlink(ps_temp)
52 except os.error:
53 pass
55 abc_temp.close()
56 if sts1 != 0 or sts2 != 0:
57 error = state_machine.reporter.error(
58 'Error processing music directive:\n\t' +
59 ('\n\t'.join(errors)),
60 nodes.literal_block(block_text, block_text),
61 line=lineno)
62 return [error]
63 else:
64 # Crop excess whitespace from the image
65 crop_image(output_name)
67 # Return an image directive.
68 options['uri'] = os.path.basename(output_name)
69 return [nodes.image(output_name, **options)]
71 import re
72 error_pat = re.compile('(in line )(\d+)')
73 def parse_errors (errors, line_start):
74 lines = errors.split('\n')
75 lines = [ line for line in lines if line.startswith('Error')]
76 def f (m):
77 return m.group(1) + str(line_start+int(m.group(2)))
78 lines = [ error_pat.sub(f, line) for line in lines]
79 return lines
82 def crop_image (filename):
83 """Reads the image specified by filename, crops excess space off it,
84 and writes it out to the same file."""
85 from PIL import Image
86 image = Image.open(filename)
87 for edge in 'NSWE':
88 image = _crop(image, edge)
89 image.save(filename)
91 def _crop (image, edge):
92 im_x, im_y = image.size
93 if edge == 'N':
94 start = (0,0)
95 scan_increment = (1,0)
96 line_increment = (0,1)
97 elif edge == 'S':
98 start = (0,im_y-1)
99 scan_increment = (1,0)
100 line_increment = (0,-1)
101 elif edge == 'E':
102 start = (im_x-1,0)
103 scan_increment = (0,1)
104 line_increment = (-1,0)
105 elif edge == 'W':
106 start = (0,0)
107 scan_increment = (0,1)
108 line_increment = (1,0)
110 # Look for blank lines
111 def is_white (color):
112 return color == 255
114 def is_blank (x,y):
115 x_inc, y_inc = scan_increment
116 while 0 <= x < im_x and 0 <= y < im_y:
117 point = image.getpixel((x,y))
118 if not is_white(point):
119 return False
120 x += x_inc
121 y += y_inc
122 return True
124 # Look for blank edges, jumping in increments of JUMP pixels.
125 JUMP = 5
126 x, y = start
127 x_inc, y_inc = line_increment
128 while is_blank(x,y):
129 x += x_inc * JUMP
130 y += y_inc * JUMP
132 # Found a non-blank line, so scan back
133 x -= x_inc
134 y -= y_inc
135 while not is_blank(x,y):
136 x -= x_inc
137 y -= y_inc
139 # OK; x,y are now on the first blank line, so crop it
140 if edge == 'N':
141 box = (0,y, im_x, im_y)
142 elif edge == 'S':
143 box = (0,0, im_x, y)
144 elif edge == 'E':
145 box = (0,0, x, im_y)
146 elif edge == 'W':
147 box = (x,0, im_x, im_y)
149 image = image.crop(box)
150 image.load()
151 return image
154 ABCDirective.content = True
156 def register():
157 register_directive('music', ABCDirective)