]> git.donarmstrong.com Git - lilypond.git/blob - scripts/lilypond-book.py
patch::: 1.5.26.jcn1
[lilypond.git] / scripts / lilypond-book.py
1 #!@PYTHON@
2 # vim: set noexpandtab:
3 # TODO:
4 # * junk --outdir for --output 
5 # * Figure out clean set of options.
6 # * 
7 # * EndLilyPondOutput is def'd as vfil. Causes large white gaps.
8 # * texinfo: add support for @pagesize
9
10 # todo: dimension handling (all the x2y) is clumsy. (tca: Thats
11 #       because the values are taken directly from texinfo.tex,
12 #       geometry.sty and article.cls. Give me a hint, and I'll
13 #       fix it.)
14
15 #
16 # TODO: magnification support should also work for texinfo -> html: eg. add as option to dvips. 
17
18
19 # This is was the idea for handling of comments:
20 #       Multiline comments, @ignore .. @end ignore is scanned for
21 #       in read_doc_file, and the chunks are marked as 'ignore', so
22 #       lilypond-book will not touch them any more. The content of the
23 #       chunks are written to the output file. Also 'include' and 'input'
24 #       regex has to check if they are commented out.
25 #
26 #       Then it is scanned for 'lilypond', 'lilypond-file' and 'lilypond-block'.
27 #       These three regex's has to check if they are on a commented line,
28 #       % for latex, @c for texinfo.
29 #
30 #       Then lines that are commented out with % (latex) and @c (Texinfo)
31 #       are put into chunks marked 'ignore'. This cannot be done before
32 #       searching for the lilypond-blocks because % is also the comment character
33 #       for lilypond.
34 #
35 #       The the rest of the rexeces are searched for. They don't have to test
36 #       if they are on a commented out line.
37
38
39
40 import os
41 import stat
42 import string
43 import re
44 import getopt
45 import sys
46 import __main__
47 import operator
48
49
50 program_version = '@TOPLEVEL_VERSION@'
51 if program_version == '@' + 'TOPLEVEL_VERSION' + '@':
52         program_version = '1.5.18'
53
54 #
55 # Try to cater for bad installations of LilyPond, that have
56 # broken TeX setup.  Just hope this doesn't hurt good TeX
57 # setups.  Maybe we should check if kpsewhich can find
58 # feta16.{afm,mf,tex,tfm}, and only set env upon failure.
59 #
60 datadir = '@datadir@'
61 environment = {
62         'MFINPUTS' : datadir + '/mf:',
63         'TEXINPUTS': datadir + '/tex:' + datadir + '/ps:.:',
64         'TFMFONTS' : datadir + '/tfm:',
65         'GS_FONTPATH' : datadir + '/afm:' + datadir + '/pfa',
66         'GS_LIB' : datadir + '/ps',
67 }
68
69 def setup_environment ():
70         for key in environment.keys ():
71                 val = environment[key]
72                 if os.environ.has_key (key):
73                         val = val + os.pathsep + os.environ[key]
74                 os.environ[key] = val
75
76
77
78 include_path = [os.getcwd()]
79
80
81 # g_ is for global (?)
82
83 g_here_dir = os.getcwd ()
84 g_dep_prefix = ''
85 g_outdir = ''
86 g_force_lilypond_fontsize = 0
87 g_read_lys = 0
88 g_do_pictures = 1
89 g_num_cols = 1
90 format = ''
91 g_run_lilypond = 1
92 no_match = 'a\ba'
93
94 default_music_fontsize = 16
95 default_text_fontsize = 12
96 paperguru = None
97
98 # this code is ugly. It should be cleaned
99 class LatexPaper:
100         def __init__(self):
101                 self.m_paperdef =  {
102                         # the dimensions are from geometry.sty
103                         'a0paper': (mm2pt(841), mm2pt(1189)),
104                         'a1paper': (mm2pt(595), mm2pt(841)),
105                         'a2paper': (mm2pt(420), mm2pt(595)),
106                         'a3paper': (mm2pt(297), mm2pt(420)),
107                         'a4paper': (mm2pt(210), mm2pt(297)),
108                         'a5paper': (mm2pt(149), mm2pt(210)),
109                         'b0paper': (mm2pt(1000), mm2pt(1414)),
110                         'b1paper': (mm2pt(707), mm2pt(1000)),
111                         'b2paper': (mm2pt(500), mm2pt(707)),
112                         'b3paper': (mm2pt(353), mm2pt(500)),
113                         'b4paper': (mm2pt(250), mm2pt(353)),
114                         'b5paper': (mm2pt(176), mm2pt(250)),
115                         'letterpaper': (in2pt(8.5), in2pt(11)),
116                         'legalpaper': (in2pt(8.5), in2pt(14)),
117                         'executivepaper': (in2pt(7.25), in2pt(10.5))}
118                 self.m_use_geometry = None
119                 self.m_papersize = 'letterpaper'
120                 self.m_fontsize = 10
121                 self.m_num_cols = 1
122                 self.m_landscape = 0
123                 self.m_geo_landscape = 0
124                 self.m_geo_width = None
125                 self.m_geo_textwidth = None
126                 self.m_geo_lmargin = None
127                 self.m_geo_rmargin = None
128                 self.m_geo_includemp = None
129                 self.m_geo_marginparwidth = {10: 57, 11: 50, 12: 35}
130                 self.m_geo_marginparsep = {10: 11, 11: 10, 12: 10}
131                 self.m_geo_x_marginparwidth = None
132                 self.m_geo_x_marginparsep = None
133                 self.__body = None
134         def set_geo_option(self, name, value):
135
136                 if type(value) == type(""):
137                         m = re.match ("([0-9.]+)(cm|in|pt|mm|em|ex)",value)
138                         if m:
139                                 unit = m.group (2)
140                                 num = string.atof(m.group (1))
141                                 conv =  dimension_conversion_dict[m.group(2)]
142
143                                 value = conv(num)
144
145                 if name == 'body' or name == 'text':
146                         if type(value) == type(""):
147                                 self.m_geo_textwidth =  value
148                         else:
149                                 self.m_geo_textwidth =  value[0]
150                         self.__body = 1
151                 elif name == 'portrait':
152                         self.m_geo_landscape = 0
153                 elif name == 'reversemp' or name == 'reversemarginpar':
154                         if self.m_geo_includemp == None:
155                                 self.m_geo_includemp = 1
156                 elif name == 'marginparwidth' or name == 'marginpar':
157                         self.m_geo_x_marginparwidth =  value
158                         self.m_geo_includemp = 1
159                 elif name == 'marginparsep':
160                         self.m_geo_x_marginparsep =  value
161                         self.m_geo_includemp = 1
162                 elif name == 'scale':
163                         if type(value) == type(""):
164                                 self.m_geo_width = self.get_paperwidth() * float(value)
165                         else:
166                                 self.m_geo_width = self.get_paperwidth() * float(value[0])
167                 elif name == 'hscale':
168                         self.m_geo_width = self.get_paperwidth() * float(value)
169                 elif name == 'left' or name == 'lmargin':
170                         self.m_geo_lmargin =  value
171                 elif name == 'right' or name == 'rmargin':
172                         self.m_geo_rmargin =  value
173                 elif name == 'hdivide' or name == 'divide':
174                         if value[0] not in ('*', ''):
175                                 self.m_geo_lmargin =  value[0]
176                         if value[1] not in ('*', ''):
177                                 self.m_geo_width =  value[1]
178                         if value[2] not in ('*', ''):
179                                 self.m_geo_rmargin =  value[2]
180                 elif name == 'hmargin':
181                         if type(value) == type(""):
182                                 self.m_geo_lmargin =  value
183                                 self.m_geo_rmargin =  value
184                         else:
185                                 self.m_geo_lmargin =  value[0]
186                                 self.m_geo_rmargin =  value[1]
187                 elif name == 'margin':#ugh there is a bug about this option in
188                                         # the geometry documentation
189                         if type(value) == type(""):
190                                 self.m_geo_lmargin =  value
191                                 self.m_geo_rmargin =  value
192                         else:
193                                 self.m_geo_lmargin =  value[0]
194                                 self.m_geo_rmargin =  value[0]
195                 elif name == 'total':
196                         if type(value) == type(""):
197                                 self.m_geo_width =  value
198                         else:
199                                 self.m_geo_width =  value[0]
200                 elif name == 'width' or name == 'totalwidth':
201                         self.m_geo_width =  value
202                 elif name == 'paper' or name == 'papername':
203                         self.m_papersize = value
204                 elif name[-5:] == 'paper':
205                         self.m_papersize = name
206                 else:
207                         pass 
208                         # what is _set_dimen ?? /MB
209                         #self._set_dimen('m_geo_'+name, value)
210         def __setattr__(self, name, value):
211                 if type(value) == type("") and \
212                    dimension_conversion_dict.has_key (value[-2:]):
213                         f = dimension_conversion_dict[value[-2:]]
214                         self.__dict__[name] = f(float(value[:-2]))
215                 else:
216                         self.__dict__[name] = value
217                         
218         def __str__(self):
219                 s =  "LatexPaper:\n-----------"
220                 for v in self.__dict__.keys():
221                         if v[:2] == 'm_':
222                                 s = s +  str (v) + ' ' + str (self.__dict__[v])
223                 s = s +  "-----------"
224                 return s
225         
226         def get_linewidth(self):
227                 w = self._calc_linewidth()
228                 if self.m_num_cols == 2:
229                         return (w - 10) / 2
230                 else:
231                         return w
232         def get_paperwidth(self):
233                 #if self.m_use_geometry:
234                         return self.m_paperdef[self.m_papersize][self.m_landscape or self.m_geo_landscape]
235                 #return self.m_paperdef[self.m_papersize][self.m_landscape]
236         
237         def _calc_linewidth(self):
238                 # since geometry sometimes ignores 'includemp', this is
239                 # more complicated than it should be
240                 mp = 0
241                 if self.m_geo_includemp:
242                         if self.m_geo_x_marginparsep is not None:
243                                 mp = mp + self.m_geo_x_marginparsep
244                         else:
245                                 mp = mp + self.m_geo_marginparsep[self.m_fontsize]
246                         if self.m_geo_x_marginparwidth is not None:
247                                 mp = mp + self.m_geo_x_marginparwidth
248                         else:
249                                 mp = mp + self.m_geo_marginparwidth[self.m_fontsize]
250
251                 #ugh test if this is necessary                          
252                 if self.__body:
253                         mp = 0
254
255                 if not self.m_use_geometry:
256                         return latex_linewidths[self.m_papersize][self.m_fontsize]
257                 else:
258                         geo_opts = (self.m_geo_lmargin == None,
259                                     self.m_geo_width == None,
260                                     self.m_geo_rmargin == None)
261
262                         if geo_opts == (1, 1, 1):
263                                 if self.m_geo_textwidth:
264                                         return self.m_geo_textwidth
265                                 w = self.get_paperwidth() * 0.8
266                                 return w - mp
267                         elif geo_opts == (0, 1, 1):
268                                  if self.m_geo_textwidth:
269                                         return self.m_geo_textwidth
270                                  return self.f1(self.m_geo_lmargin, mp)
271                         elif geo_opts == (1, 1, 0):
272                                  if self.m_geo_textwidth:
273                                         return self.m_geo_textwidth
274                                  return self.f1(self.m_geo_rmargin, mp)
275                         elif geo_opts \
276                                         in ((0, 0, 1), (1, 0, 0), (1, 0, 1)):
277                                 if self.m_geo_textwidth:
278                                         return self.m_geo_textwidth
279                                 return self.m_geo_width - mp
280                         elif geo_opts in ((0, 1, 0), (0, 0, 0)):
281                                 w = self.get_paperwidth() \
282                                   - self.m_geo_lmargin - self.m_geo_rmargin - mp
283                                 if w < 0:
284                                         w = 0
285                                 return w
286                         raise "Never do this!"
287         def f1(self, m, mp):
288                 tmp = self.get_paperwidth() - m * 2 - mp
289                 if tmp < 0:
290                         tmp = 0
291                 return tmp
292         def f2(self):
293                 tmp = self.get_paperwidth() - self.m_geo_lmargin \
294                         - self.m_geo_rmargin
295                 if tmp < 0:
296                         return 0
297                 return tmp
298
299 class TexiPaper:
300         def __init__(self):
301                 self.m_papersize = 'letterpaper'
302                 self.m_fontsize = 12
303         def get_linewidth(self):
304                 return texi_linewidths[self.m_papersize][self.m_fontsize]
305
306 def mm2pt(x):
307         return x * 2.8452756
308 def in2pt(x):
309         return x * 72.26999
310 def em2pt(x, fontsize = 10):
311         return {10: 10.00002, 11: 10.8448, 12: 11.74988}[fontsize] * x
312 def ex2pt(x, fontsize = 10):
313         return {10: 4.30554, 11: 4.7146, 12: 5.16667}[fontsize] * x
314
315 def pt2pt(x):
316         return x
317
318 dimension_conversion_dict ={
319         'mm': mm2pt,
320         'cm': lambda x: mm2pt(10*x),
321         'in': in2pt,
322         'em': em2pt,
323         'ex': ex2pt,
324         'pt': pt2pt
325         }
326
327         
328 # latex linewidths:
329 # indices are no. of columns, papersize,  fontsize
330 # Why can't this be calculated?
331 latex_linewidths = {
332         'a4paper':{10: 345, 11: 360, 12: 390},
333         'a4paper-landscape': {10: 598, 11: 596, 12:592},
334         'a5paper':{10: 276, 11: 276, 12: 276},
335         'b5paper':{10: 345, 11: 356, 12: 356},
336         'letterpaper':{10: 345, 11: 360, 12: 390},
337         'letterpaper-landscape':{10: 598, 11: 596, 12:596},
338         'legalpaper': {10: 345, 11: 360, 12: 390},
339         'executivepaper':{10: 345, 11: 360, 12: 379}}
340
341 texi_linewidths = {
342         'afourpaper': {12: mm2pt(160)},
343         'afourwide': {12: in2pt(6.5)},
344         'afourlatex': {12: mm2pt(150)},
345         'smallbook': {12: in2pt(5)},
346         'letterpaper': {12: in2pt(6)}}
347
348 option_definitions = [
349   ('EXT', 'f', 'format', 'set format.  EXT is one of texi and latex.'),
350   ('DIM',  '', 'default-music-fontsize', 'default fontsize for music.  DIM is assumed to be in points'),
351   ('DIM',  '', 'default-lilypond-fontsize', 'deprecated, use --default-music-fontsize'),
352   ('DIM', '', 'force-music-fontsize', 'force fontsize for all inline lilypond. DIM is assumed be to in points'),
353   ('DIM', '', 'force-lilypond-fontsize', 'deprecated, use --force-music-fontsize'),
354   ('DIR', 'I', 'include', 'include path'),
355   ('', 'M', 'dependencies', 'write dependencies'),
356   ('PREF', '',  'dep-prefix', 'prepend PREF before each -M dependency'),
357   ('', 'n', 'no-lily', 'don\'t run lilypond'),
358   ('', '', 'no-pictures', "don\'t generate pictures"),
359   ('', '', 'read-lys', "don't write ly files."),
360   ('FILE', 'o', 'outname', 'filename main output file'),
361   ('FILE', '', 'outdir', "where to place generated files"),
362   ('', 'v', 'version', 'print version information' ),
363   ('', 'h', 'help', 'print help'),
364   ]
365
366 # format specific strings, ie. regex-es for input, and % strings for output
367 output_dict= {
368         'latex': {
369                 'output-lilypond-fragment' : r"""\begin[eps,singleline,%s]{lilypond}
370   \context Staff <
371     \context Voice{
372       %s
373     }
374   >
375 \end{lilypond}""",
376                 'output-filename' : r'''
377
378 \verb+%s+:''',
379                 'output-lilypond': r"""\begin[%s]{lilypond}
380 %s
381 \end{lilypond}""",
382                 'output-verbatim': "\\begin{verbatim}%s\\end{verbatim}",
383                 'output-default-post': "\\def\postLilypondExample{}\n",
384                 'output-default-pre': "\\def\preLilypondExample{}\n",
385                 'usepackage-graphics': '\\usepackage{graphics}\n',
386                 'output-eps': '\\noindent\\parbox{\\lilypondepswidth{%(fn)s.eps}}{\includegraphics{%(fn)s.eps}}',
387                 'output-tex': '{\\preLilypondExample \\input %(fn)s.tex \\postLilypondExample\n}',
388                 'pagebreak': r'\pagebreak',
389                 },
390         'texi' : {'output-lilypond': """@lilypond[%s]
391 %s
392 @end lilypond 
393 """,
394                 'output-filename' : r'''
395
396 @file{%s}:''',    
397                   'output-lilypond-fragment': """@lilypond[%s]
398 \context Staff\context Voice{ %s }
399 @end lilypond """,
400                   'pagebreak': None,
401                   'output-verbatim': r"""@example
402 %s
403 @end example
404 """,
405
406 # do some tweaking: @ is needed in some ps stuff.
407 # override EndLilyPondOutput, since @tex is done
408 # in a sandbox, you can't do \input lilyponddefs at the
409 # top of the document.
410
411 # should also support fragment in
412                   
413                   'output-all': r"""
414 @tex
415 \catcode`\@=12
416 \input lilyponddefs
417 \def\EndLilyPondOutput{}
418 \input %(fn)s.tex
419 \catcode`\@=0
420 @end tex
421 @html
422 <p>
423 <a href="%(fn)s.png">
424 <img border=0 src="%(fn)s.png" alt="[picture of music]">
425 </a>
426 @end html
427 """,
428                 }
429         }
430
431 def output_verbatim (body):
432         if __main__.format == 'texi':
433                 body = re.sub ('([@{}])', '@\\1', body)
434         return get_output ('output-verbatim') % body
435
436
437 re_dict = {
438         'latex': {'input': r'(?m)^[^%\n]*?(?P<match>\\mbinput{?([^}\t \n}]*))',
439                   'include': r'(?m)^[^%\n]*?(?P<match>\\mbinclude{(?P<filename>[^}]+)})',
440                   'option-sep' : ',\s*',
441                   'header': r"\\documentclass\s*(\[.*?\])?",
442                   'geometry': r"^(?m)[^%\n]*?\\usepackage\s*(\[(?P<options>.*)\])?\s*{geometry}",
443                   'preamble-end': r'(?P<code>\\begin{document})',
444                   'verbatim': r"(?s)(?P<code>\\begin{verbatim}.*?\\end{verbatim})",
445                   'verb': r"(?P<code>\\verb(?P<del>.).*?(?P=del))",
446                   'lilypond-file': r'(?m)^[^%\n]*?(?P<match>\\lilypondfile\s*(\[(?P<options>.*?)\])?\s*\{(?P<filename>.+)})',
447                   'lilypond' : r'(?m)^[^%\n]*?(?P<match>\\lilypond\s*(\[(?P<options>.*?)\])?\s*{(?P<code>.*?)})',
448                   'lilypond-block': r"(?sm)^[^%\n]*?(?P<match>\\begin\s*(\[(?P<options>.*?)\])?\s*{lilypond}(?P<code>.*?)\\end{lilypond})",
449                   'def-post-re': r"\\def\\postLilypondExample",
450                   'def-pre-re': r"\\def\\preLilypondExample",             
451                   'usepackage-graphics': r"\usepackage{graphics}",
452                   'intertext': r',?\s*intertext=\".*?\"',
453                   'multiline-comment': no_match,
454                   'singleline-comment': r"(?m)^.*?(?P<match>(?P<code>^%.*$\n+))",
455                   'numcols': r"(?P<code>\\(?P<num>one|two)column)",
456                   },
457
458
459         # why do we have distinction between @mbinclude and @include? 
460         'texi': {
461                  'include':  '(?m)^[^%\n]*?(?P<match>@mbinclude[ \n\t]+(?P<filename>[^\t \n]*))',
462                  'input': no_match,
463                  'header': no_match,
464                  'preamble-end': no_match,
465                  'landscape': no_match,
466                  'verbatim': r"""(?s)(?P<code>@example\s.*?@end example\s)""",
467                  'verb': r"""(?P<code>@code{.*?})""",
468                  'lilypond-file': '(?m)^(?!@c)(?P<match>@lilypondfile(\[(?P<options>.*?)\])?{(?P<filename>[^}]+)})',
469                  'lilypond' : '(?m)^(?!@c)(?P<match>@lilypond(\[(?P<options>.*?)\])?{(?P<code>.*?)})',
470 # pyton2.2b2 barfs on this
471 #                'lilypond-block': r"""(?m)^(?!@c)(?P<match>(?s)(?P<match>@lilypond(\[(?P<options>.*?)\])?\s(?P<code>.*?)@end lilypond\s))""",
472                  'lilypond-block': r"""(?m)^(?!@c)(?P<match>@lilypond(\[(?P<options>.*?)\])?\s(?P<code>.*?)@end lilypond\s)""",
473                   'option-sep' : ',\s*',
474                   'intertext': r',?\s*intertext=\".*?\"',
475                   'multiline-comment': r"(?sm)^\s*(?!@c\s+)(?P<code>@ignore\s.*?@end ignore)\s",
476                   'singleline-comment': r"(?m)^.*?(?P<match>(?P<code>@c.*$\n+))",
477                   'numcols': no_match,
478                  }
479         }
480
481
482 for r in re_dict.keys ():
483         olddict = re_dict[r]
484         newdict = {}
485         for k in olddict.keys ():
486                 newdict[k] = re.compile (olddict[k])
487         re_dict[r] = newdict
488
489         
490 def uniq (list):
491         list.sort ()
492         s = list
493         list = []
494         for x in s:
495                 if x not in list:
496                         list.append (x)
497         return list
498                 
499
500 def get_output (name):
501         return  output_dict[format][name]
502
503 def get_re (name):
504         return  re_dict[format][name]
505
506 def bounding_box_dimensions(fname):
507         if g_outdir:
508                 fname = os.path.join(g_outdir, fname)
509         try:
510                 fd = open(fname)
511         except IOError:
512                 error ("Error opening `%s'" % fname)
513         str = fd.read ()
514         s = re.search('%%BoundingBox: ([0-9]+) ([0-9]+) ([0-9]+) ([0-9]+)', str)
515         if s:
516                 return (int (s.group (3) - s.group (1) + 0.5),
517                         int (s.group (4) - s.group (2) + 0.5))
518         else:
519                 return (0,0)
520
521 def error (str):
522         sys.stderr.write (str + "\n  Exiting ... \n\n")
523         raise 'Exiting.'
524
525
526 def compose_full_body (body, opts):
527         """Construct the lilypond code to send to Lilypond.
528         Add stuff to BODY using OPTS as options."""
529         music_size = default_music_fontsize
530         latex_size = default_text_fontsize
531         for o in opts:
532                 if g_force_lilypond_fontsize:
533                         music_size = g_force_lilypond_fontsize
534                 else:
535                         m = re.match ('([0-9]+)pt', o)
536                         if m:
537                                 music_size = string.atoi(m.group (1))
538
539                 m = re.match ('latexfontsize=([0-9]+)pt', o)
540                 if m:
541                         latex_size = string.atoi (m.group (1))
542
543         if re.search ('\\\\score', body):
544                 is_fragment = 0
545         else:
546                 is_fragment = 1
547         if 'fragment' in opts:
548                 is_fragment = 1
549         if 'nofragment' in opts:
550                 is_fragment = 0
551
552         if is_fragment and not 'multiline' in opts:
553                 opts.append('singleline')
554         if 'singleline' in opts:
555                 l = -1.0;
556         else:
557                 l = __main__.paperguru.get_linewidth()
558
559         for o in opts:
560                 m= re.search ('relative(.*)', o)
561                 v = 0
562                 if m:
563                         try:
564                                 v = string.atoi (m.group (1))
565                         except ValueError:
566                                 pass
567
568                         v = v + 1
569                         pitch = 'c'
570                         if v < 0:
571                                 pitch = pitch + '\,' * v
572                         elif v > 0:
573                                 pitch = pitch + '\'' * v
574
575                         body = '\\relative %s { %s }' %(pitch, body)
576         
577         if is_fragment:
578                 body = r"""\score { 
579  \notes { %s }
580   \paper { }  
581 }""" % body
582
583         opts = uniq (opts)
584         optstring = string.join (opts, ' ')
585         optstring = re.sub ('\n', ' ', optstring)
586         body = r"""
587 %% Generated automatically by: lilypond-book.py
588 %% options are %s  
589 \include "paper%d.ly"
590 \paper  { linewidth = %f \pt } 
591 """ % (optstring, music_size, l) + body
592
593         # ughUGH not original options
594         return body
595
596 def parse_options_string(s):
597         d = {}
598         r1 = re.compile("((\w+)={(.*?)})((,\s*)|$)")
599         r2 = re.compile("((\w+)=(.*?))((,\s*)|$)")
600         r3 = re.compile("(\w+?)((,\s*)|$)")
601         while s:
602                 m = r1.match(s)
603                 if m:
604                         s = s[m.end():]
605                         d[m.group(2)] = re.split(",\s*", m.group(3))
606                         continue
607                 m = r2.match(s)
608                 if m:
609                         s = s[m.end():]
610                         d[m.group(2)] = m.group(3)
611                         continue
612                 m = r3.match(s)
613                 if m:
614                         s = s[m.end():]
615                         d[m.group(1)] = 1
616                         continue
617                 
618                 error ("format of option string invalid (was `%')" % s)
619         return d
620
621 def scan_latex_preamble(chunks):
622         # first we want to scan the \documentclass line
623         # it should be the first non-comment line
624         idx = 0
625         while 1:
626                 if chunks[idx][0] == 'ignore':
627                         idx = idx + 1
628                         continue
629                 m = get_re ('header').match(chunks[idx][1])
630                 if m <> None and m.group (1):
631                         options = re.split (',[\n \t]*', m.group(1)[1:-1])
632                 else:
633                         options = []
634                 for o in options:
635                         if o == 'landscape':
636                                 paperguru.m_landscape = 1
637                         m = re.match("(.*?)paper", o)
638                         if m:
639                                 paperguru.m_papersize = m.group()
640                         else:
641                                 m = re.match("(\d\d)pt", o)
642                                 if m:
643                                         paperguru.m_fontsize = int(m.group(1))
644                 break
645         
646         while chunks[idx][0] != 'preamble-end':
647                 if chunks[idx] == 'ignore':
648                         idx = idx + 1
649                         continue
650                 m = get_re ('geometry').search(chunks[idx][1])
651                 if m:
652                         paperguru.m_use_geometry = 1
653                         o = parse_options_string(m.group('options'))
654                         for k in o.keys():
655                                 paperguru.set_geo_option(k, o[k])
656                 idx = idx + 1
657
658 def scan_texi_preamble (chunks):
659         # this is not bulletproof..., it checks the first 10 chunks
660         for c in chunks[:10]: 
661                 if c[0] == 'input':
662                         for s in ('afourpaper', 'afourwide', 'letterpaper',
663                                   'afourlatex', 'smallbook'):
664                                 if string.find(c[1], "@%s" % s) != -1:
665                                         paperguru.m_papersize = s
666
667 def scan_preamble (chunks):
668         if __main__.format == 'texi':
669                 scan_texi_preamble(chunks)
670         else:
671                 assert __main__.format == 'latex'
672                 scan_latex_preamble(chunks)
673                 
674
675 def completize_preamble (chunks):
676         if __main__.format == 'texi':
677                 return chunks
678         pre_b = post_b = graphics_b = None
679         for chunk in chunks:
680                 if chunk[0] == 'preamble-end':
681                         break
682                 if chunk[0] == 'input':
683                         m = get_re('def-pre-re').search(chunk[1])
684                         if m:
685                                 pre_b = 1
686                 if chunk[0] == 'input':
687                         m = get_re('def-post-re').search(chunk[1])
688                         if m:
689                                 post_b = 1
690                 if chunk[0] == 'input':
691                         m = get_re('usepackage-graphics').search(chunk[1])
692                         if m:
693                                 graphics_b = 1
694         x = 0
695         while chunks[x][0] != 'preamble-end':
696                 x = x + 1
697         if not pre_b:
698                 chunks.insert(x, ('input', get_output ('output-default-pre')))
699         if not post_b:
700                 chunks.insert(x, ('input', get_output ('output-default-post')))
701         if not graphics_b:
702                 chunks.insert(x, ('input', get_output ('usepackage-graphics')))
703         return chunks
704
705
706 read_files = []
707 def find_file (name):
708         """
709         Search the include path for NAME. If found, return the (CONTENTS, PATH) of the file.
710         """
711         
712         f = None
713         nm = ''
714         for a in include_path:
715                 try:
716                         nm = os.path.join (a, name)
717                         f = open (nm)
718                         __main__.read_files.append (nm)
719                         break
720                 except IOError:
721                         pass
722         if f:
723                 sys.stderr.write ("Reading `%s'\n" % nm)
724                 return (f.read (), nm)
725         else:
726                 error ("File not found `%s'\n" % name)
727                 return ('', '')
728
729 def do_ignore(match_object):
730         return [('ignore', match_object.group('code'))]
731 def do_preamble_end(match_object):
732         return [('preamble-end', match_object.group('code'))]
733
734 def make_verbatim(match_object):
735         return [('verbatim', match_object.group('code'))]
736
737 def make_verb(match_object):
738         return [('verb', match_object.group('code'))]
739
740 def do_include_file(m):
741         "m: MatchObject"
742         return [('input', get_output ('pagebreak'))] \
743              + read_doc_file(m.group('filename')) \
744              + [('input', get_output ('pagebreak'))] 
745
746 def do_input_file(m):
747         return read_doc_file(m.group('filename'))
748
749 def make_lilypond(m):
750         if m.group('options'):
751                 options = m.group('options')
752         else:
753                 options = ''
754         return [('input', get_output('output-lilypond-fragment') % 
755                         (options, m.group('code')))]
756
757 def make_lilypond_file(m):
758         """
759
760         Find @lilypondfile{bla.ly} occurences and substitute bla.ly
761         into a @lilypond .. @end lilypond block.
762         
763         """
764         
765         if m.group('options'):
766                 options = m.group('options')
767         else:
768                 options = ''
769         (content, nm) = find_file(m.group('filename'))
770         options = "filename=%s," % nm + options
771
772         return [('input', get_output('output-lilypond') %
773                         (options, content))]
774
775 def make_lilypond_block(m):
776         if m.group('options'):
777                 options = get_re('option-sep').split (m.group('options'))
778         else:
779             options = []
780         options = filter(lambda s: s != '', options)
781         return [('lilypond', m.group('code'), options)]
782
783 def do_columns(m):
784         if __main__.format != 'latex':
785                 return []
786         if m.group('num') == 'one':
787                 return [('numcols', m.group('code'), 1)]
788         if m.group('num') == 'two':
789                 return [('numcols', m.group('code'), 2)]
790         
791 def chop_chunks(chunks, re_name, func, use_match=0):
792     newchunks = []
793     for c in chunks:
794         if c[0] == 'input':
795             str = c[1]
796             while str:
797                 m = get_re (re_name).search (str)
798                 if m == None:
799                     newchunks.append (('input', str))
800                     str = ''
801                 else:
802                     if use_match:
803                         newchunks.append (('input', str[:m.start ('match')]))
804                     else:
805                         newchunks.append (('input', str[:m.start (0)]))
806                     #newchunks.extend(func(m))
807                     # python 1.5 compatible:
808                     newchunks = newchunks + func(m)
809                     str = str [m.end(0):]
810         else:
811             newchunks.append(c)
812     return newchunks
813
814 def determine_format (str):
815         if __main__.format == '':
816                 
817                 latex =  re.search ('\\\\document', str[:200])
818                 texinfo =  re.search ('@node|@setfilename', str[:200])
819
820                 f = ''
821                 g = None
822                 
823                 if texinfo and latex == None:
824                         f = 'texi'
825                 elif latex and texinfo == None: 
826                         f = 'latex'
827                 else:
828                         error("error: can't determine format, please specify")
829                 __main__.format = f
830
831         if __main__.paperguru == None:
832                 if __main__.format == 'texi':
833                         g = TexiPaper()
834                 else:
835                         g = LatexPaper()
836                         
837                 __main__.paperguru = g
838
839
840 def read_doc_file (filename):
841         """Read the input file, find verbatim chunks and do \input and \include
842         """
843         (str, path) = find_file(filename)
844         determine_format (str)
845         
846         chunks = [('input', str)]
847         
848         # we have to check for verbatim before doing include,
849         # because we don't want to include files that are mentioned
850         # inside a verbatim environment
851         chunks = chop_chunks(chunks, 'verbatim', make_verbatim)
852         chunks = chop_chunks(chunks, 'verb', make_verb)
853         chunks = chop_chunks(chunks, 'multiline-comment', do_ignore)
854         #ugh fix input
855         chunks = chop_chunks(chunks, 'include', do_include_file, 1)
856         chunks = chop_chunks(chunks, 'input', do_input_file, 1)
857         return chunks
858
859
860 taken_file_names = {}
861 def schedule_lilypond_block (chunk):
862         """Take the body and options from CHUNK, figure out how the
863         real .ly should look, and what should be left MAIN_STR (meant
864         for the main file).  The .ly is written, and scheduled in
865         TODO.
866
867         Return: a chunk (TYPE_STR, MAIN_STR, OPTIONS, TODO, BASE)
868
869         TODO has format [basename, extension, extension, ... ]
870         
871         """
872         (type, body, opts) = chunk
873         assert type == 'lilypond'
874         file_body = compose_full_body (body, opts)
875         basename = 'lily-' + `abs(hash (file_body))`
876         for o in opts:
877                 m = re.search ('filename="(.*?)"', o)
878                 if m:
879                         basename = m.group (1)
880                         if not taken_file_names.has_key(basename):
881                             taken_file_names[basename] = 0
882                         else:
883                             taken_file_names[basename] = taken_file_names[basename] + 1
884                             basename = basename + "-%i" % taken_file_names[basename]
885         if not g_read_lys:
886                 update_file(file_body, os.path.join(g_outdir, basename) + '.ly')
887         needed_filetypes = ['tex']
888
889         if format  == 'texi':
890                 needed_filetypes.append('eps')
891                 needed_filetypes.append('png')
892         if 'eps' in opts and not ('eps' in needed_filetypes):
893                 needed_filetypes.append('eps')
894         pathbase = os.path.join (g_outdir, basename)
895         def f(base, ext1, ext2):
896                 a = os.path.isfile(base + ext2)
897                 if (os.path.isfile(base + ext1) and
898                     os.path.isfile(base + ext2) and
899                                 os.stat(base+ext1)[stat.ST_MTIME] >
900                                 os.stat(base+ext2)[stat.ST_MTIME]) or \
901                                 not os.path.isfile(base + ext2):
902                         return 1
903         todo = []
904         if 'tex' in needed_filetypes and f(pathbase, '.ly', '.tex'):
905                 todo.append('tex')
906         if 'eps' in needed_filetypes and f(pathbase, '.tex', '.eps'):
907                 todo.append('eps')
908         if 'png' in needed_filetypes and f(pathbase, '.eps', '.png'):
909                 todo.append('png')
910         newbody = ''
911
912         if 'printfilename' in opts:
913                 for o in opts:
914                         m= re.match ("filename=(.*)", o)
915                         if m:
916                                 newbody = newbody + get_output ("output-filename") % m.group(1)
917                                 break
918                 
919         
920         if 'verbatim' in opts:
921                 newbody = output_verbatim (body)
922
923         for o in opts:
924                 m = re.search ('intertext="(.*?)"', o)
925                 if m:
926                         newbody = newbody  + m.group (1) + "\n\n"
927         if format == 'latex':
928                 if 'eps' in opts:
929                         s = 'output-eps'
930                 else:
931                         s = 'output-tex'
932         else: # format == 'texi'
933                 s = 'output-all'
934         newbody = newbody + get_output (s) % {'fn': basename }
935         return ('lilypond', newbody, opts, todo, basename)
936
937 def process_lilypond_blocks(outname, chunks):#ugh rename
938         newchunks = []
939         # Count sections/chapters.
940         for c in chunks:
941                 if c[0] == 'lilypond':
942                         c = schedule_lilypond_block (c)
943                 elif c[0] == 'numcols':
944                         paperguru.m_num_cols = c[2]
945                 newchunks.append (c)
946         return newchunks
947
948
949
950 def system (cmd):
951         sys.stderr.write ("invoking `%s'\n" % cmd)
952         st = os.system (cmd)
953         if st:
954                 error ('Error command exited with value %d\n' % st)
955         return st
956
957
958 def get_bbox (filename):
959         f = open (filename)
960         gr = []
961         while 1:
962                 l =f.readline ()
963                 m = re.match ('^%%BoundingBox: ([0-9]+) ([0-9]+) ([0-9]+) ([0-9]+)', l)
964                 if m:
965                         gr = map (string.atoi, m.groups ())
966                         break
967         
968         return gr
969
970 def make_pixmap (name):
971         bbox = get_bbox (name + '.eps')
972         margin = 3
973         fo = open (name + '.trans.eps' , 'w')
974         fo.write ('%d %d translate\n' % (-bbox[0]+margin, -bbox[1]+margin))
975         fo.close ()
976         
977         res = 90
978
979         x = (2* margin + bbox[2] - bbox[0]) * res / 72.
980         y = (2* margin + bbox[3] - bbox[1]) * res / 72.
981
982         cmd = r"""gs -g%dx%d -sDEVICE=pgm  -dTextAlphaBits=4 -dGraphicsAlphaBits=4  -q -sOutputFile=- -r%d -dNOPAUSE %s %s -c quit | pnmtopng > %s"""
983         
984         cmd = cmd % (x, y, res, name + '.trans.eps', name + '.eps',name + '.png')
985         try:
986                 status = system (cmd)
987         except:
988                 os.unlink (name + '.png')
989                 error ("Removing output file")
990
991 def compile_all_files (chunks):
992         global foutn
993         eps = []
994         tex = []
995         png = []
996
997         for c in chunks:
998                 if c[0] <> 'lilypond':
999                         continue
1000                 base  = c[4]
1001                 exts = c[3]
1002                 for e in exts:
1003                         if e == 'eps':
1004                                 eps.append (base)
1005                         elif e == 'tex':
1006                                 #ugh
1007                                 if base + '.ly' not in tex:
1008                                         tex.append (base + '.ly')
1009                         elif e == 'png' and g_do_pictures:
1010                                 png.append (base)
1011         d = os.getcwd()
1012         if g_outdir:
1013                 os.chdir(g_outdir)
1014         if tex:
1015                 # fixme: be sys-independent.
1016                 def incl_opt (x):
1017                         if g_outdir and x[0] <> '/' :
1018                                 x = os.path.join (g_here_dir, x)
1019                         return ' -I %s' % x
1020
1021                 incs = map (incl_opt, include_path)
1022                 lilyopts = string.join (incs, ' ' )
1023                 if do_deps:
1024                         lilyopts = lilyopts + ' --dependencies '
1025                         if g_outdir:
1026                                 lilyopts = lilyopts + '--dep-prefix=' + g_outdir + '/'
1027                 texfiles = string.join (tex, ' ')
1028                 system ('lilypond --header=texidoc %s %s' % (lilyopts, texfiles))
1029
1030                 #
1031                 # Ugh, fixing up dependencies for .tex generation
1032                 #
1033                 if do_deps:
1034                         depfiles=map (lambda x: re.sub ('(.*)\.ly', '\\1.dep', x), tex)
1035                         for i in depfiles:
1036                                 f =open (i)
1037                                 text=f.read ()
1038                                 f.close ()
1039                                 text=re.sub ('\n([^:\n]*):', '\n' + foutn + ':', text)
1040                                 f = open (i, 'w')
1041                                 f.write (text)
1042                                 f.close ()
1043
1044         for e in eps:
1045                 system(r"tex '\nonstopmode \input %s'" % e)
1046                 system(r"dvips -E -o %s %s" % (e + '.eps', e))
1047                 
1048         for g in png:
1049                 make_pixmap (g)
1050                 
1051         os.chdir (d)
1052
1053
1054 def update_file (body, name):
1055         """
1056         write the body if it has changed
1057         """
1058         same = 0
1059         try:
1060                 f = open (name)
1061                 fs = f.read (-1)
1062                 same = (fs == body)
1063         except:
1064                 pass
1065
1066         if not same:
1067                 f = open (name , 'w')
1068                 f.write (body)
1069                 f.close ()
1070         
1071         return not same
1072
1073
1074 def getopt_args (opts):
1075         "Construct arguments (LONG, SHORT) for getopt from  list of options."
1076         short = ''
1077         long = []
1078         for o in opts:
1079                 if o[1]:
1080                         short = short + o[1]
1081                         if o[0]:
1082                                 short = short + ':'
1083                 if o[2]:
1084                         l = o[2]
1085                         if o[0]:
1086                                 l = l + '='
1087                         long.append (l)
1088         return (short, long)
1089
1090 def option_help_str (o):
1091         "Transform one option description (4-tuple ) into neatly formatted string"
1092         sh = '  '       
1093         if o[1]:
1094                 sh = '-%s' % o[1]
1095
1096         sep = ' '
1097         if o[1] and o[2]:
1098                 sep = ','
1099                 
1100         long = ''
1101         if o[2]:
1102                 long= '--%s' % o[2]
1103
1104         arg = ''
1105         if o[0]:
1106                 if o[2]:
1107                         arg = '='
1108                 arg = arg + o[0]
1109         return '  ' + sh + sep + long + arg
1110
1111
1112 def options_help_str (opts):
1113         "Convert a list of options into a neatly formatted string"
1114         w = 0
1115         strs =[]
1116         helps = []
1117
1118         for o in opts:
1119                 s = option_help_str (o)
1120                 strs.append ((s, o[3]))
1121                 if len (s) > w:
1122                         w = len (s)
1123
1124         str = ''
1125         for s in strs:
1126                 str = str + '%s%s%s\n' % (s[0], ' ' * (w - len(s[0])  + 3), s[1])
1127         return str
1128
1129 def help():
1130         sys.stdout.write("""Usage: lilypond-book [options] FILE\n
1131 Generate hybrid LaTeX input from Latex + lilypond
1132 Options:
1133 """)
1134         sys.stdout.write (options_help_str (option_definitions))
1135         sys.stdout.write (r"""Warning all output is written in the CURRENT directory
1136
1137
1138
1139 Report bugs to bug-gnu-music@gnu.org.
1140
1141 Written by Tom Cato Amundsen <tca@gnu.org> and
1142 Han-Wen Nienhuys <hanwen@cs.uu.nl>
1143 """)
1144
1145         sys.exit (0)
1146
1147
1148 def write_deps (fn, target, chunks):
1149         global read_files
1150         sys.stdout.write('Writing `%s\'\n' % os.path.join(g_outdir, fn))
1151         f = open (os.path.join(g_outdir, fn), 'w')
1152         f.write ('%s%s: ' % (g_dep_prefix, target))
1153         for d in read_files:
1154                 f.write ('%s ' %  d)
1155         basenames=[]
1156         for c in chunks:
1157                 if c[0] == 'lilypond':
1158                         (type, body, opts, todo, basename) = c;
1159                         basenames.append (basename)
1160         for d in basenames:
1161                 if g_outdir:
1162                         d=g_outdir + '/' + d
1163                 if g_dep_prefix:
1164                         #if not os.isfile (d): # thinko?
1165                         if not re.search ('/', d):
1166                                 d = g_dep_prefix + d
1167                 f.write ('%s.tex ' %  d)
1168         f.write ('\n')
1169         #if len (basenames):
1170         #       for d in basenames:
1171         #               f.write ('%s.ly ' %  d)
1172         #       f.write (' : %s' % target)
1173         f.write ('\n')
1174         f.close ()
1175         read_files = []
1176
1177 def identify():
1178         sys.stdout.write ('lilypond-book (GNU LilyPond) %s\n' % program_version)
1179
1180 def print_version ():
1181         identify()
1182         sys.stdout.write (r"""Copyright 1998--1999
1183 Distributed under terms of the GNU General Public License. It comes with
1184 NO WARRANTY.
1185 """)
1186
1187
1188 def check_texidoc (chunks):
1189         n = []
1190         for c in chunks:
1191                 if c[0] == 'lilypond':
1192                         (type, body, opts, todo, basename) = c;
1193                         pathbase = os.path.join (g_outdir, basename)
1194                         if os.path.isfile (pathbase + '.texidoc'):
1195                                 body = '\n@include %s.texidoc\n' % basename + body
1196                                 c = (type, body, opts, todo, basename)
1197                 n.append (c)
1198         return n
1199
1200
1201 ## what's this? Docme --hwn
1202 ##
1203 def fix_epswidth (chunks):
1204         newchunks = []
1205         for c in chunks:
1206                 if c[0] <> 'lilypond' or 'eps' not in c[2]:
1207                         newchunks.append (c)
1208                         continue
1209
1210                 mag = 1.0
1211                 for o in c[2]:
1212                         m  = re.match ('magnification=([0-9.]+)', o)
1213                         if m:
1214                                 mag = string.atof (m.group (1))
1215
1216                 def replace_eps_dim (match, lmag = mag):
1217                         filename = match.group (1)
1218                         dims = bounding_box_dimensions (filename)
1219
1220                         return '%fpt' % (dims[0] *lmag)
1221         
1222                 body = re.sub (r"""\\lilypondepswidth{(.*?)}""", replace_eps_dim, c[1])
1223                 newchunks.append(('lilypond', body, c[2], c[3], c[4]))
1224                         
1225         return newchunks
1226
1227
1228 foutn=""
1229 def do_file(input_filename):
1230         global foutn
1231         file_settings = {}
1232         if outname:
1233                 my_outname = outname
1234         else:
1235                 my_outname = os.path.basename(os.path.splitext(input_filename)[0])
1236         my_depname = my_outname + '.dep'                
1237
1238         chunks = read_doc_file(input_filename)
1239         chunks = chop_chunks(chunks, 'lilypond', make_lilypond, 1)
1240         chunks = chop_chunks(chunks, 'lilypond-file', make_lilypond_file, 1)
1241         chunks = chop_chunks(chunks, 'lilypond-block', make_lilypond_block, 1)
1242         chunks = chop_chunks(chunks, 'singleline-comment', do_ignore, 1)
1243         chunks = chop_chunks(chunks, 'preamble-end', do_preamble_end)
1244         chunks = chop_chunks(chunks, 'numcols', do_columns)
1245         #print "-" * 50
1246         #for c in chunks: print "c:", c;
1247         #sys.exit()
1248         scan_preamble(chunks)
1249         chunks = process_lilypond_blocks(my_outname, chunks)
1250
1251         foutn = os.path.join (g_outdir, my_outname + '.' + format)
1252
1253         # Do It.
1254         if __main__.g_run_lilypond:
1255                 compile_all_files (chunks)
1256                 chunks = fix_epswidth (chunks)
1257
1258         if __main__.format == 'texi':
1259                 chunks = check_texidoc (chunks)
1260
1261         x = 0
1262         chunks = completize_preamble (chunks)
1263         sys.stderr.write ("Writing `%s'\n" % foutn)
1264         fout = open (foutn, 'w')
1265         for c in chunks:
1266                 fout.write (c[1])
1267         fout.close ()
1268         # should chmod -w
1269
1270         if do_deps:
1271                 write_deps (my_depname, foutn, chunks)
1272
1273
1274 outname = ''
1275 try:
1276         (sh, long) = getopt_args (__main__.option_definitions)
1277         (options, files) = getopt.getopt(sys.argv[1:], sh, long)
1278 except getopt.error, msg:
1279         sys.stderr.write("error: %s" % msg)
1280         sys.exit(1)
1281
1282 do_deps = 0
1283 for opt in options:     
1284         o = opt[0]
1285         a = opt[1]
1286
1287         if o == '--include' or o == '-I':
1288                 include_path.append (a)
1289         elif o == '--version' or o == '-v':
1290                 print_version ()
1291                 sys.exit  (0)
1292         elif o == '--format' or o == '-f':
1293                 __main__.format = a
1294         elif o == '--outname' or o == '-o':
1295                 if len(files) > 1:
1296                         #HACK
1297                         sys.stderr.write("Lilypond-book is confused by --outname on multiple files")
1298                         sys.exit(1)
1299                 outname = a
1300         elif o == '--help' or o == '-h':
1301                 help ()
1302         elif o == '--no-lily' or o == '-n':
1303                 __main__.g_run_lilypond = 0
1304         elif o == '--dependencies' or o == '-M':
1305                 do_deps = 1
1306         elif o == '--default-music-fontsize':
1307                 default_music_fontsize = string.atoi (a)
1308         elif o == '--default-lilypond-fontsize':
1309                 print "--default-lilypond-fontsize is deprecated, use --default-music-fontsize"
1310                 default_music_fontsize = string.atoi (a)
1311         elif o == '--force-music-fontsize':
1312                 g_force_lilypond_fontsize = string.atoi(a)
1313         elif o == '--force-lilypond-fontsize':
1314                 print "--force-lilypond-fontsize is deprecated, use --default-lilypond-fontsize"
1315                 g_force_lilypond_fontsize = string.atoi(a)
1316         elif o == '--dep-prefix':
1317                 g_dep_prefix = a
1318         elif o == '--no-pictures':
1319                 g_do_pictures = 0
1320         elif o == '--read-lys':
1321                 g_read_lys = 1
1322         elif o == '--outdir':
1323                 g_outdir = a
1324
1325 identify()
1326 if g_outdir:
1327         if os.path.isfile(g_outdir):
1328                 error ("outdir is a file: %s" % g_outdir)
1329         if not os.path.exists(g_outdir):
1330                 os.mkdir(g_outdir)
1331 setup_environment ()
1332 for input_filename in files:
1333         do_file(input_filename)
1334         
1335 #
1336 # Petr, ik zou willen dat ik iets zinvoller deed,
1337 # maar wat ik kan ik doen, het verandert toch niets?
1338 #   --hwn 20/aug/99