]> git.lyx.org Git - lyx.git/blob - lib/scripts/lyxpreview2bitmap.py
430807ef4f04cdb757b156a5c7d5ba500dcd6db2
[lyx.git] / lib / scripts / lyxpreview2bitmap.py
1 #! /usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 # file lyxpreview2bitmap.py
5 # This file is part of LyX, the document processor.
6 # Licence details can be found in the file COPYING.
7
8 # author Angus Leeming
9 # with much advice from members of the preview-latex project:
10 # David Kastrup, dak@gnu.org and
11 # Jan-Åke Larsson, jalar@mai.liu.se.
12
13 # Full author contact details are available in file CREDITS
14
15 # This script takes a LaTeX file and generates a collection of
16 # png or ppm image files, one per previewed snippet.
17
18 # Pre-requisites:
19 # * A latex executable;
20 # * preview.sty;
21 # * dvipng;
22 # * dv2dt;
23 # * pngtoppm (if outputing ppm format images).
24
25 # preview.sty and dvipng are part of the preview-latex project
26 # http://preview-latex.sourceforge.net/
27
28 # preview.sty can alternatively be obtained from
29 # CTAN/support/preview-latex/
30
31 # Example usage:
32 # lyxpreview2bitmap.py png 0lyxpreview.tex 128 000000 faf0e6
33
34 # This script takes six arguments:
35 # FORMAT:   The desired output format. Either 'png' or 'ppm'.
36 # TEXFILE:  the name of the .tex file to be converted.
37 # DPI:      a scale factor, used to ascertain the resolution of the
38 #           generated image which is then passed to gs.
39 # FG_COLOR: the foreground color as a hexadecimal string, eg '000000'.
40 # BG_COLOR: the background color as a hexadecimal string, eg 'faf0e6'.
41 # CONVERTER: the converter (optional). Default is latex.
42
43 # Decomposing TEXFILE's name as DIR/BASE.tex, this script will,
44 # if executed successfully, leave in DIR:
45 # * a (possibly large) number of image files with names
46 #   like BASE[0-9]+.png
47 # * a file BASE.metrics, containing info needed by LyX to position
48 #   the images correctly on the screen.
49
50 # What does this script do?
51 # 1) Call latex/pdflatex/xelatex/whatever (CONVERTER parameter)
52 # 2) If the output is a PDF fallback to legacy
53 # 3) Otherwise check each page of the DVI (with dv2dt) looking for
54 #    PostScript literals, not well supported by dvipng. Pages
55 #    containing them are passed to the legacy method in a new LaTeX file.
56 # 4) Call dvipng on the pages without PS literals
57 # 5) Join metrics info coming from both methods (legacy and dvipng)
58 #    and write them to file
59
60 # dvipng is fast but gives problem in several cases, like with
61 # PSTricks, TikZ and other packages using PostScript literals
62 # for all these cases the legacy route is taken (step 3).
63 # Moreover dvipng can't work with PDF files, so, if the CONVERTER
64 # paramter is pdflatex we have to fallback to legacy route (step 2).
65
66 import glob, os, re, string, sys
67
68 from legacy_lyxpreview2ppm import legacy_conversion, \
69      legacy_conversion_step2, legacy_extract_metrics_info
70
71 from lyxpreview_tools import copyfileobj, error, filter_pages, find_exe, \
72      find_exe_or_terminate, join_metrics_and_rename, latex_commands, \
73      latex_file_re, make_texcolor, mkstemp, run_command, warning, \
74      write_metrics_info
75
76
77 def usage(prog_name):
78     return "Usage: %s <format> <latex file> <dpi> <fg color> <bg color>\n" \
79            "\twhere the colors are hexadecimal strings, eg 'faf0e6'" \
80            % prog_name
81
82 # Returns a list of tuples containing page number and ascent fraction
83 # extracted from dvipng output.
84 # Use write_metrics_info to create the .metrics file with this info
85 def extract_metrics_info(dvipng_stdout):
86     # "\[[0-9]+" can match two kinds of numbers: page numbers from dvipng
87     # and glyph numbers from mktexpk. The glyph numbers always match
88     # "\[[0-9]+\]" while the page number never is followed by "\]". Thus:
89     page_re = re.compile("\[([0-9]+)[^]]");
90     metrics_re = re.compile("depth=(-?[0-9]+) height=(-?[0-9]+)")
91
92     success = 0
93     page = ""
94     pos = 0
95     results = []
96     while 1:
97         match = page_re.search(dvipng_stdout, pos)
98         if match == None:
99             break
100         page = match.group(1)
101         pos = match.end()
102         match = metrics_re.search(dvipng_stdout, pos)
103         if match == None:
104             break
105         success = 1
106
107         # Calculate the 'ascent fraction'.
108         descent = string.atof(match.group(1))
109         ascent  = string.atof(match.group(2))
110
111         frac = 0.5
112         if ascent >= 0 or descent >= 0:
113             if abs(ascent + descent) > 0.1:
114                 frac = ascent / (ascent + descent)
115
116             # Sanity check
117             if frac < 0:
118                 frac = 0.5
119
120         results.append((int(page), frac))
121         pos = match.end() + 2
122
123     if success == 0:
124         error("Failed to extract metrics info from dvipng")
125
126     return results
127
128
129 def color_pdf(latex_file, bg_color, fg_color):
130     use_preview_pdf_re = re.compile("(\s*\\\\usepackage\[[^]]+)((pdftex|xetex)\]{preview})")
131
132     tmp = mkstemp()
133
134     fg = ""
135     if fg_color != "0.000000,0.000000,0.000000":
136         fg = '  \\AtBeginDocument{\\let\\oldpreview\\preview\\renewcommand\\preview{\\oldpreview\\color[rgb]{%s}}}\n' % (fg_color)
137
138     success = 0
139     try:
140         for line in open(latex_file, 'r').readlines():
141             match = use_preview_pdf_re.match(line)
142             if match == None:
143                 tmp.write(line)
144                 continue
145             success = 1
146             tmp.write("  \\usepackage{color}\n" \
147                   "  \\pagecolor[rgb]{%s}\n" \
148                   "%s" \
149                   "%s\n" \
150                   % (bg_color, fg, match.group()))
151             continue
152
153     except:
154         # Unable to open the file, but do nothing here because
155         # the calling function will act on the value of 'success'.
156         warning('Warning in color_pdf! Unable to open "%s"' % latex_file)
157         warning(`sys.exc_type` + ',' + `sys.exc_value`)
158
159     if success:
160         copyfileobj(tmp, open(latex_file,"wb"), 1)
161
162     return success
163
164
165 def fix_latex_file(latex_file):
166     documentclass_re = re.compile("(\\\\documentclass\[)(1[012]pt,?)(.+)")
167
168     tmp = mkstemp()
169
170     changed = 0
171     for line in open(latex_file, 'r').readlines():
172         match = documentclass_re.match(line)
173         if match == None:
174             tmp.write(line)
175             continue
176
177         changed = 1
178         tmp.write("%s%s\n" % (match.group(1), match.group(3)))
179
180     if changed:
181         copyfileobj(tmp, open(latex_file,"wb"), 1)
182
183     return
184
185
186 def convert_to_ppm_format(pngtopnm, basename):
187     png_file_re = re.compile("\.png$")
188
189     for png_file in glob.glob("%s*.png" % basename):
190         ppm_file = png_file_re.sub(".ppm", png_file)
191
192         p2p_cmd = '%s "%s"' % (pngtopnm, png_file)
193         p2p_status, p2p_stdout = run_command(p2p_cmd)
194         if p2p_status != None:
195             error("Unable to convert %s to ppm format" % png_file)
196
197         ppm = open(ppm_file, 'w')
198         ppm.write(p2p_stdout)
199         os.remove(png_file)
200
201 # Returns a tuple of:
202 # ps_pages: list of page indexes of pages containing PS literals
203 # page_count: total number of pages
204 # pages_parameter: parameter for dvipng to exclude pages with PostScript
205 def find_ps_pages(dvi_file):
206     # latex failed
207     # FIXME: try with pdflatex
208     if not os.path.isfile(dvi_file):
209         error("No DVI output.")
210
211     # Check for PostScript specials in the dvi, badly supported by dvipng
212     # This is required for correct rendering of PSTricks and TikZ
213     dv2dt = find_exe_or_terminate(["dv2dt"])
214     dv2dt_call = '%s "%s"' % (dv2dt, dvi_file)
215
216     # The output from dv2dt goes to stdout
217     dv2dt_status, dv2dt_output = run_command(dv2dt_call)
218     psliteral_re = re.compile("^special[1-4] [0-9]+ '(\"|ps:)")
219
220     # Parse the dtl file looking for PostScript specials.
221     # Pages using PostScript specials are recorded in ps_pages and then
222     # used to create a different LaTeX file for processing in legacy mode.
223     page_has_ps = False
224     page_index = 0
225     ps_pages = []
226
227     for line in dv2dt_output.split("\n"):
228         # New page
229         if line.startswith("bop"):
230             page_has_ps = False
231             page_index += 1
232
233         # End of page
234         if line.startswith("eop") and page_has_ps:
235             # We save in a list all the PostScript pages
236             ps_pages.append(page_index)
237
238         if psliteral_re.match(line) != None:
239             # Literal PostScript special detected!
240             page_has_ps = True
241
242     # Create the -pp parameter for dvipng
243     pages_parameter = ""
244     if len(ps_pages) > 0 and len(ps_pages) < page_index:
245         # Don't process Postscript pages with dvipng by selecting the
246         # wanted pages through the -pp parameter. E.g., dvipng -pp 4-12,14,64
247         pages_parameter = " -pp "
248         skip = True
249         last = -1
250
251         # Use page ranges, as a list of pages could exceed command line
252         # maximum length (especially under Win32)
253         for index in xrange(1, page_index + 1):
254             if (not index in ps_pages) and skip:
255                 # We were skipping pages but current page shouldn't be skipped.
256                 # Add this page to -pp, it could stay alone or become the
257                 # start of a range.
258                 pages_parameter += str(index)
259                 # Save the starting index to avoid things such as "11-11"
260                 last = index
261                 # We're not skipping anymore
262                 skip = False
263             elif (index in ps_pages) and (not skip):
264                 # We weren't skipping but current page should be skipped
265                 if last != index - 1:
266                     # If the start index of the range is the previous page
267                     # then it's not a range
268                     pages_parameter += "-" + str(index - 1)
269
270                 # Add a separator
271                 pages_parameter += ","
272                 # Now we're skipping
273                 skip = True
274
275         # Remove the trailing separator
276         pages_parameter = pages_parameter.rstrip(",")
277         # We've to manage the case in which the last page is closing a range
278         if (not index in ps_pages) and (not skip) and (last != index):
279                 pages_parameter += "-" + str(index)
280
281     return (ps_pages, page_index, pages_parameter)
282
283 def main(argv):
284     # Parse and manipulate the command line arguments.
285     if len(argv) != 6 and len(argv) != 7:
286         error(usage(argv[0]))
287
288     script_name = argv[0]
289
290     output_format = string.lower(argv[1])
291
292     input_path = argv[2]
293     dir, latex_file = os.path.split(input_path)
294     if len(dir) != 0:
295         os.chdir(dir)
296
297     dpi = string.atoi(argv[3])
298     fg_color = argv[4]
299     bg_color = argv[5]
300
301     fg_color_dvipng = make_texcolor(fg_color, False)
302     bg_color_dvipng = make_texcolor(bg_color, False)
303
304     fg_color_gr = make_texcolor(fg_color, True)
305     bg_color_gr = make_texcolor(bg_color, True)
306
307     # External programs used by the script.
308     if len(argv) == 7:
309         latex = [argv[6]]
310     else:
311         latex = None
312     latex = find_exe_or_terminate(latex or latex_commands)
313
314     # Omit font size specification in latex file.
315     fix_latex_file(latex_file)
316
317     # This can go once dvipng becomes widespread.
318     dvipng = find_exe(["dvipng"])
319     if dvipng == None:
320         # The data is input to legacy_conversion in as similar
321         # as possible a manner to that input to the code used in
322         # LyX 1.3.x.
323         vec = [ script_name, input_path, str(dpi), output_format, fg_color, bg_color, latex ]
324         return legacy_conversion(vec)
325
326     pngtopnm = ""
327     if output_format == "ppm":
328         pngtopnm = find_exe_or_terminate(["pngtopnm"])
329
330     # Move color information for PDF into the latex file.
331     if not color_pdf(latex_file, bg_color_gr, fg_color_gr):
332         error("Unable to move color info into the latex file")
333
334     # Compile the latex file.
335     latex_call = '%s "%s"' % (latex, latex_file)
336
337     latex_status, latex_stdout = run_command(latex_call)
338     if latex_status != None:
339         warning("%s had problems compiling %s" \
340               % (os.path.basename(latex), latex_file))
341
342     if latex == "xelatex":
343         warning("Using XeTeX")
344         # FIXME: skip unnecessary dvips trial in legacy_conversion_step2
345         return legacy_conversion_step2(latex_file, dpi, output_format)
346
347     # The dvi output file name
348     dvi_file = latex_file_re.sub(".dvi", latex_file)
349
350     # If there's no DVI output, look for PDF and go to legacy or fail
351     if not os.path.isfile(dvi_file):
352         # No DVI, is there a PDF?
353         pdf_file = latex_file_re.sub(".pdf", latex_file)
354         if os.path.isfile(pdf_file):
355             warning("%s produced a PDF output, fallback to legacy." % \
356                 (os.path.basename(latex)))
357             return legacy_conversion_step2(latex_file, dpi, output_format)
358         else:
359             error("No DVI or PDF output. %s failed." \
360                 % (os.path.basename(latex)))
361
362     # Look for PS literals in DVI pages
363     # ps_pages: list of page indexes of pages containing PS literals
364     # page_count: total number of pages
365     # pages_parameter: parameter for dvipng to exclude pages with PostScript
366     (ps_pages, page_count, pages_parameter) = find_ps_pages(dvi_file)
367
368     # If all pages need PostScript, directly use the legacy method.
369     if len(ps_pages) == page_count:
370         vec = [ script_name, input_path, str(dpi), output_format, fg_color, bg_color, latex ]
371         return legacy_conversion(vec)
372
373     # Run the dvi file through dvipng.
374     dvipng_call = '%s -Ttight -depth -height -D %d -fg "%s" -bg "%s" %s "%s"' \
375         % (dvipng, dpi, fg_color_dvipng, bg_color_dvipng, pages_parameter, dvi_file)
376     dvipng_status, dvipng_stdout = run_command(dvipng_call)
377
378     if dvipng_status != None:
379         warning("%s failed to generate images from %s... fallback to legacy method" \
380               % (os.path.basename(dvipng), dvi_file))
381         # FIXME: skip unnecessary dvips trial in legacy_conversion_step2
382         return legacy_conversion_step2(latex_file, dpi, output_format)
383
384     # Extract metrics info from dvipng_stdout.
385     metrics_file = latex_file_re.sub(".metrics", latex_file)
386     dvipng_metrics = extract_metrics_info(dvipng_stdout)
387
388     # If some pages require PostScript pass them to legacy method
389     if len(ps_pages) > 0:
390         # Create a new LaTeX file just for the snippets needing
391         # the legacy method
392         legacy_latex_file = latex_file_re.sub("_legacy.tex", latex_file)
393         filter_pages(latex_file, legacy_latex_file, ps_pages)
394
395         # Pass the new LaTeX file to the legacy method
396         vec = [ script_name, latex_file_re.sub("_legacy.tex", input_path),
397                 str(dpi), output_format, fg_color, bg_color, latex ]
398         legacy_metrics = legacy_conversion(vec, True)[1]
399
400         # Now we need to mix metrics data from dvipng and the legacy method
401         original_bitmap = latex_file_re.sub("%d." + output_format, legacy_latex_file)
402         destination_bitmap = latex_file_re.sub("%d." + output_format, latex_file)
403
404         # Join metrics from dvipng and legacy, and rename legacy bitmaps
405         join_metrics_and_rename(dvipng_metrics, legacy_metrics, ps_pages,
406             original_bitmap, destination_bitmap)
407
408     # Convert images to ppm format if necessary.
409     if output_format == "ppm":
410         convert_to_ppm_format(pngtopnm, latex_file_re.sub("", latex_file))
411
412     # Actually create the .metrics file
413     write_metrics_info(dvipng_metrics, metrics_file)
414
415     return (0, dvipng_metrics)
416
417 if __name__ == "__main__":
418     exit(main(sys.argv)[0])