1 # -*- coding: utf-8 -*-
4 # This file is part of LyX, the document processor.
5 # Licence details can be found in the file COPYING.
7 # \author Thibaut Cuvelier
9 # Full author contact details are available in file CREDITS
12 # python docbook_copy.py lilypond_book_command in.docbook out.docbook
13 # This script copies the original DocBook file (directly produced by LyX) to the output DocBook file,
14 # potentially applying a post-processing step. For now, the only implemented post-processing step is
16 # lilypond_book_command is either directly the binary to call OR the equivalent Python script that is
17 # not directly executable.
18 # /!\ The original file may be modified by this script!
30 def __init__(self, args):
32 # Parse the command line.
33 self.lilypond_command = args[1]
34 self.in_file = args[2]
35 self.out_file = args[3]
37 # Compute a few things from the raw parameters.
38 self.in_folder = os.path.split(self.in_file)[0]
39 self.out_folder = os.path.split(self.out_file)[0]
41 self.in_lily_file = self.in_file.replace('.xml', '.lyxml')
42 self.has_lilypond = self.lilypond_command not in {'', 'none'}
43 self.lilypond_folder = os.path.split(self.lilypond_command)[0] if self.has_lilypond else ''
44 self.do_lilypond_processing = self.has_lilypond and self.in_file_needs_lilypond()
47 print('>> Given arguments:')
48 print('>> LilyPond: ' + ('present' if self.has_lilypond else 'not found') + '.')
49 print('>> LilyPond callable as: ' + self.lilypond_command + '.')
50 print('>> LilyPond path: ' + self.lilypond_folder + '.')
51 print('>> Input file: ' + self.in_file + '.')
52 print('>> Output file: ' + self.out_file + '.')
53 print('>> Input folder: ' + self.in_folder + '.')
54 print('>> Output folder: ' + self.out_folder + '.')
56 def in_file_needs_lilypond(self):
57 # Really tailored to the kind of output lilypond.module makes (in lib/layouts).
58 with open(self.in_file, 'r') as f:
59 return "language='lilypond'" in f.read()
61 def preprocess_input_for_lilypond(self):
62 # LilyPond requires that its input file has the .lyxml extension. Due to a bug in LilyPond,
63 # use " instead of ' to encode XML attributes.
64 # https://lists.gnu.org/archive/html/bug-lilypond/2021-09/msg00039.html
65 # Typical transformation:
66 # FROM: language='lilypond' role='fragment verbatim staffsize=16 ragged-right relative=2'
67 # TO: language="lilypond" role="fragment verbatim staffsize=16 ragged-right relative=2"
68 with open(self.in_file, 'r', encoding='utf-8') as f, open(self.in_lily_file, 'w', encoding='utf-8') as f_lily:
70 if "language='lilypond'" in line:
72 '<programlisting\\s+language=\'lilypond\'.*?(role=\'(?P<options>.*?)\')?>',
73 '<programlisting language="lilypond" role="\\g<options>">',
77 os.unlink(self.in_file)
79 def postprocess_output_for_lilypond(self):
81 # # Erase the <programlisting> that LilyPond left behind in the XML.
82 # in_file_before = self.in_file + '.tmp'
83 # shutil.move(self.in_file, in_file_before)
84 # with open(in_file_before, 'r', encoding='utf-8') as f_before, open(self.in_file, 'w', encoding='utf-8') as f_after:
85 # looking_for_end_programlisting = False
86 # for line in f_before:
87 # # TODO: find an efficient way to distinguish those left-overs.
88 # https://lists.gnu.org/archive/html/bug-lilypond/2021-09/msg00040.html
90 def call_lilypond(self):
91 # LilyPond requires that its input file has the .lyxml extension (plus bugs in LilyPond).
92 print('>> Rewriting ' + self.in_file)
93 print('>> as ' + self.in_lily_file + '.')
94 self.preprocess_input_for_lilypond()
96 # Add LilyPond to the PATH. lilypond-book uses a direct call to lilypond from the PATH.
97 if os.path.isdir(self.lilypond_folder):
98 os.environ['PATH'] += os.pathsep + self.lilypond_folder
100 # Make LilyPond believe it is working from the temporary LyX directory. Otherwise, it tries to find files
101 # starting from LyX's working directory... LilyPond bug, most likely.
102 # https://lists.gnu.org/archive/html/bug-lilypond/2021-09/msg00041.html
103 os.chdir(self.in_folder)
105 # Start LilyPond on the copied file. First test the binary, then check if adding Python helps.
106 command_args = ['--format=docbook', '--output=' + self.in_folder, self.in_lily_file]
107 command_raw = [self.lilypond_command] + command_args
108 command_python = ['python', self.lilypond_command] + command_args
110 print('>> Running LilyPond.')
111 sys.stdout.flush() # So that the LilyPond output is at the right place in the logs.
115 for cmd in [command_raw, command_python]:
117 subprocess.check_call(cmd, stdout=sys.stdout.fileno(), stderr=sys.stdout.fileno())
118 print('>> Success running LilyPond with ')
119 print('>> ' + str(cmd))
121 except (subprocess.CalledProcessError, OSError) as e:
122 exceptions.append((cmd, e))
125 print('>> Error from LilyPond. The successive calls were:')
126 for (i, pair) in enumerate(exceptions):
130 print('>> (' + i + ') Error from trying ' + str(cmd) + ':')
131 print('>> (' + i + ') ' + str(exc))
136 # LilyPond has a distressing tendency to leave the raw LilyPond code in the new file.
137 self.postprocess_output_for_lilypond()
139 # Now, in_file should have the clean LilyPond-processed contents.
141 def copy_lilypond_generated_images(self):
142 # LilyPond generates a lot of files in LyX' temporary folder, within the ff folder: source LilyPond files
143 # for each snippet to render, images in several formats.
144 in_generated_images_folder = os.path.join(self.in_folder, 'ff')
145 out_generated_images_folder = os.path.join(self.out_folder, 'ff')
147 if not os.path.isdir(out_generated_images_folder):
148 os.mkdir(out_generated_images_folder)
150 for img in os.listdir(in_generated_images_folder):
151 if not img.endswith('.png') and not img.endswith('.pdf'):
155 os.path.join(in_generated_images_folder, img),
156 os.path.join(out_generated_images_folder, img),
157 follow_symlinks=False,
161 # Apply LilyPond to the original file if available and needed.
162 if self.do_lilypond_processing:
163 print('>> The input file needs a LilyPond pass and LilyPond is available.')
166 # Perform the actual copy: both the modified XML file and the generated images, if LilyPond is used.
167 shutil.copyfile(self.in_file, self.out_file, follow_symlinks=False)
168 if self.do_lilypond_processing:
169 self.copy_lilypond_generated_images()
172 if __name__ == '__main__':
173 if len(sys.argv) != 4:
174 print('Exactly four arguments are expected, only %s found: %s.' % (len(sys.argv), sys.argv))
177 DocBookCopier(sys.argv).copy()