]> git.lyx.org Git - lyx.git/blob - lib/scripts/docbook_copy.py
Remove profiling.py
[lyx.git] / lib / scripts / docbook_copy.py
1 # file docbook_copy.py
2 # This file is part of LyX, the document processor.
3 # Licence details can be found in the file COPYING.
4 #
5 # \author Thibaut Cuvelier
6 #
7 # Full author contact details are available in file CREDITS
8
9 # Usage:
10 #   python docbook_copy.py lilypond_book_command in.docbook out.docbook
11 # This script copies the original DocBook file (directly produced by LyX) to the output DocBook file,
12 # potentially applying a post-processing step. For now, the only implemented post-processing step is
13 # LilyPond.
14 # lilypond_book_command is either directly the binary to call OR the equivalent Python script that is
15 # not directly executable.
16 # /!\ The original file may be modified by this script!
17
18
19 import subprocess
20 import os
21 import os.path
22 import re
23 import shutil
24 import sys
25
26
27 class DocBookCopier:
28     def __init__(self, args):
29
30         # Parse the command line.
31         self.lilypond_command = args[1]
32         self.in_file = args[2]
33         self.out_file = args[3]
34
35         # Compute a few things from the raw parameters.
36         self.in_folder = os.path.split(self.in_file)[0]
37         self.out_folder = os.path.split(self.out_file)[0]
38
39         self.in_lily_file = self.in_file.replace('.xml', '.lyxml')
40         self.has_lilypond = self.lilypond_command not in {'', 'none'}
41         self.lilypond_folder = os.path.split(self.lilypond_command)[0] if self.has_lilypond else ''
42         self.do_lilypond_processing = self.has_lilypond and self.in_file_needs_lilypond()
43
44         # Help debugging.
45         print('>> Given arguments:')
46         print('>> LilyPond: ' + ('present' if self.has_lilypond else 'not found') + '.')
47         print('>> LilyPond callable as: ' + self.lilypond_command + '.')
48         print('>> LilyPond path: ' + self.lilypond_folder + '.')
49         print('>> Input file: ' + self.in_file + '.')
50         print('>> Output file: ' + self.out_file + '.')
51         print('>> Input folder: ' + self.in_folder + '.')
52         print('>> Output folder: ' + self.out_folder + '.')
53
54     def in_file_needs_lilypond(self):
55         # Really tailored to the kind of output lilypond.module makes (in lib/layouts).
56         with open(self.in_file) as f:
57             return "language='lilypond'" in f.read()
58
59     def preprocess_input_for_lilypond(self):
60         # LilyPond requires that its input file has the .lyxml extension. Due to a bug in LilyPond,
61         # use " instead of ' to encode XML attributes.
62         # Bug report: https://gitlab.com/lilypond/lilypond/-/issues/6185
63         # Typical transformation:
64         # Fixed by 2.23.4.
65         #     FROM:  language='lilypond' role='fragment verbatim staffsize=16 ragged-right relative=2'
66         #     TO:    language="lilypond" role="fragment verbatim staffsize=16 ragged-right relative=2"
67
68         # Another problem to fix: the output is in XML, with some characters encoded as XML
69         # entities. For instance, this could be in a LilyPond snippet:
70         #     \new PianoStaff <<
71         # instead of:
72         #     \new PianoStaff <<
73         # (More complete example:
74         # https://lilypond.org/doc/v2.23/Documentation/learning/piano-centered-lyrics.)
75         # This issue must be fixed by LilyPond, as any change in this part would make the XML
76         # file invalid.
77         # Bug report: https://gitlab.com/lilypond/lilypond/-/issues/6204
78         with open(self.in_file, encoding='utf-8') as f, open(self.in_lily_file, 'w', encoding='utf-8') as f_lily:
79             for line in f:
80                 if "language='lilypond'" in line:
81                     line = re.sub(
82                         '<programlisting\\s+language=\'lilypond\'.*?(role=\'(?P<options>.*?)\')?>',
83                         '<programlisting language="lilypond" role="\\g<options>">',
84                         line
85                     )
86                 f_lily.write(line)
87         os.unlink(self.in_file)
88
89     def postprocess_output_for_lilypond(self):
90         # Major problem: LilyPond used to output the LilyPond code outside the image, which is then always displayed
91         # before the image (instead of only the generated image).
92         # Bug report: https://gitlab.com/lilypond/lilypond/-/issues/6186
93         # No more necessary with the new version of LilyPond (2.23.4). No efficient way to decide how to post-process
94         # for previous versions of LilyPond. Basically, it does not make sense to post-process.
95         pass
96
97     def call_lilypond(self):
98         # LilyPond requires that its input file has the .lyxml extension (plus bugs in LilyPond).
99         print('>> Rewriting ' + self.in_file)
100         print('>> as ' + self.in_lily_file + '.')
101         self.preprocess_input_for_lilypond()
102
103         # Add LilyPond to the PATH. lilypond-book uses a direct call to lilypond from the PATH.
104         if os.path.isdir(self.lilypond_folder):
105             os.environ['PATH'] += os.pathsep + self.lilypond_folder
106
107         # Make LilyPond believe it is working from the temporary LyX directory. Otherwise, it tries to find files
108         # starting from LyX's working directory... LilyPond bug, most likely.
109         # https://lists.gnu.org/archive/html/bug-lilypond/2021-09/msg00041.html
110         os.chdir(self.in_folder)
111
112         # Start LilyPond on the copied file. First test the binary, then check if adding Python helps.
113         command_args = ['--format=docbook', '--output=' + self.in_folder, self.in_lily_file]
114         command_raw = [self.lilypond_command] + command_args
115         command_python = ['python', self.lilypond_command] + command_args
116
117         print('>> Running LilyPond.')
118         sys.stdout.flush()  # So that the LilyPond output is at the right place in the logs.
119
120         failed = True
121         exceptions = []
122         for cmd in [command_raw, command_python]:
123             try:
124                 subprocess.check_call(cmd, stdout=sys.stdout.fileno(), stderr=sys.stdout.fileno())
125                 print('>> Success running LilyPond with ')
126                 print('>> ' + str(cmd))
127                 failed = False
128             except (subprocess.CalledProcessError, OSError) as e:
129                 exceptions.append((cmd, e))
130
131         if failed:
132             print('>> Error from LilyPond. The successive calls were:')
133             for (i, pair) in enumerate(exceptions):
134                 exc = pair[0]
135                 cmd = pair[1]
136
137                 print('>> (' + i + ') Error from trying ' + str(cmd) + ':')
138                 print('>> (' + i + ') ' + str(exc))
139
140         if failed:
141             sys.exit(1)
142
143         # LilyPond has a distressing tendency to leave the raw LilyPond code in the new file.
144         self.postprocess_output_for_lilypond()
145
146         # Now, in_file should have the clean LilyPond-processed contents.
147
148     def copy_lilypond_generated_images(self):
149         # LilyPond generates a lot of files in LyX' temporary folder, within the ff folder: source LilyPond files
150         # for each snippet to render, images in several formats.
151         in_generated_images_folder = os.path.join(self.in_folder, 'ff')
152         out_generated_images_folder = os.path.join(self.out_folder, 'ff')
153
154         if not os.path.isdir(out_generated_images_folder):
155             os.mkdir(out_generated_images_folder)
156
157         for img in os.listdir(in_generated_images_folder):
158             if not img.endswith('.png') and not img.endswith('.pdf'):
159                 continue
160
161             shutil.copyfile(
162                 os.path.join(in_generated_images_folder, img),
163                 os.path.join(out_generated_images_folder, img),
164             )
165
166     def copy(self):
167         # Apply LilyPond to the original file if available and needed.
168         if self.do_lilypond_processing:
169             print('>> The input file needs a LilyPond pass and LilyPond is available.')
170             self.call_lilypond()
171
172         # Perform the actual copy: both the modified XML file and the generated images, if LilyPond is used.
173         shutil.copyfile(self.in_file, self.out_file)
174         if self.do_lilypond_processing:
175             self.copy_lilypond_generated_images()
176
177
178 if __name__ == '__main__':
179     if len(sys.argv) != 4:
180         print(f'Exactly four arguments are expected, only {len(sys.argv)} found: {sys.argv}.')
181         sys.exit(1)
182
183     DocBookCopier(sys.argv).copy()