diff --git a/lib/ChangeLog b/lib/ChangeLog index 26110abac6..e7c0e2788e 100644 --- a/lib/ChangeLog +++ b/lib/ChangeLog @@ -1,3 +1,21 @@ +2004-08-09 Angus Leeming + + * scripts/lyxpreview_tools.py: move code common to both + legacy_lyxpreview2ppm.py and lyxpreview2bitmap.py into a separate + file. The code itself is a straight copy of the implementation + to be found in the 1.3.x tree. It is much more flexible and powerful + than the existing code in the 1.4.x tree. It's also better tested. + + * scripts/legacy_lyxpreview2ppm.py: use the code in + lyxpreview_tools.py. Also use try-except blocks when reading files. + The code is as similar as possible to that found in the 1.3.x tree, + differing only in the parsing of the metrics file as 1.4.x requires + different data. + + * scripts/lyxpreview2bitmap.py: use the code in lyxpreview_tools.py. + Also use the experience gained in making the 1.3.x code robust to + wrap arguments to external programs in 'single quotes'. + 2004-07-23 Angus Leeming * layouts/apa.layout: Patch from Yvonnick Noel. Spell the diff --git a/lib/scripts/legacy_lyxpreview2ppm.py b/lib/scripts/legacy_lyxpreview2ppm.py index 9b39354673..992af3f76e 100644 --- a/lib/scripts/legacy_lyxpreview2ppm.py +++ b/lib/scripts/legacy_lyxpreview2ppm.py @@ -1,68 +1,73 @@ #! /usr/bin/env python +# -*- coding: iso-8859-1 -*- # file legacy_lyxpreview2ppm.py # This file is part of LyX, the document processor. # Licence details can be found in the file COPYING. # author Angus Leeming - # Full author contact details are available in file CREDITS -# This script converts a LaTeX file to a bunch of ppm files using the -# deprecated dvi->ps->ppm conversion route. +# with much advice from members of the preview-latex project: +# David Kastrup, dak@gnu.org and +# Jan-Åke Larsson, jalar@mai.liu.se. +# and with much help testing the code under Windows from +# Paul A. Rubin, rubin@msu.edu. +# This script takes a LaTeX file and generates a collection of +# ppm image files, one per previewed snippet. +# Example usage: +# legacy_lyxpreview2bitmap.py 0lyxpreview.tex 128 ppm 000000 faf0e6 + +# This script takes five arguments: +# TEXFILE: the name of the .tex file to be converted. +# SCALEFACTOR: a scale factor, used to ascertain the resolution of the +# generated image which is then passed to gs. +# OUTPUTFORMAT: the format of the output bitmap image files. +# This particular script can produce only "ppm" format output. +# FG_COLOR: the foreground color as a hexadecimal string, eg '000000'. +# BG_COLOR: the background color as a hexadecimal string, eg 'faf0e6'. + +# Decomposing TEXFILE's name as DIR/BASE.tex, this script will, +# if executed successfully, leave in DIR: +# * a (possibly large) number of image files with names +# like BASE[0-9]+.ppm +# * a file BASE.metrics, containing info needed by LyX to position +# the images correctly on the screen. + +# The script uses several external programs and files: +# * A latex executable; +# * preview.sty; +# * dvips; +# * gs; +# * pnmcrop (optional). + +# preview.sty is part of the preview-latex project +# http://preview-latex.sourceforge.net/ +# Alternatively, it can be obtained from +# CTAN/support/preview-latex/ + +# The script uses the deprecated dvi->ps->ppm conversion route. # If possible, please grab 'dvipng'; it's faster and more robust. -# This legacy support will be removed one day... +# If you have it then this script will not be invoked by +# lyxpreview2bitmap.py. +# Warning: this legacy support will be removed one day... -import glob, os, re, string, sys -import pipes, tempfile +import glob, os, pipes, re, string, sys +from lyxpreview_tools import copyfileobj, error, find_exe, \ + find_exe_or_terminate, mkstemp, run_command -# Pre-compiled regular expressions. +# Pre-compiled regular expression. latex_file_re = re.compile("\.tex$") def usage(prog_name): - return "Usage: %s \n"\ + return "Usage: %s ppm \n"\ "\twhere the colors are hexadecimal strings, eg 'faf0e6'"\ % prog_name -def error(message): - sys.stderr.write(message + '\n') - sys.exit(1) - - -def find_exe(candidates, path): - for prog in candidates: - for directory in path: - if os.name == "nt": - full_path = os.path.join(directory, prog + ".exe") - else: - full_path = os.path.join(directory, prog) - - if os.access(full_path, os.X_OK): - return full_path - - return None - - -def find_exe_or_terminate(candidates, path): - exe = find_exe(candidates, path) - if exe == None: - error("Unable to find executable from '%s'" % string.join(candidates)) - - return exe - - -def run_command(cmd): - handle = os.popen(cmd, 'r') - cmd_stdout = handle.read() - cmd_status = handle.close() - - return cmd_status, cmd_stdout - - def extract_metrics_info(log_file, metrics_file): metrics = open(metrics_file, 'w') @@ -73,30 +78,37 @@ def extract_metrics_info(log_file, metrics_file): tp_descent = 0.0 success = 0 - for line in open(log_file, 'r').readlines(): - match = log_re.match(line) - if match == None: - continue + try: + for line in open(log_file, 'r').readlines(): + match = log_re.match(line) + if match == None: + continue - snippet = (match.group(1) == 'S') - success = 1 - match = data_re.search(line) - if match == None: - error("Unexpected data in %s\n%s" % (log_file, line)) + snippet = (match.group(1) == 'S') + success = 1 + match = data_re.search(line) + if match == None: + error("Unexpected data in %s\n%s" % (log_file, line)) - if snippet: - ascent = string.atof(match.group(2)) + tp_ascent - descent = string.atof(match.group(3)) - tp_descent + if snippet: + ascent = string.atof(match.group(2)) + tp_ascent + descent = string.atof(match.group(3)) - tp_descent - frac = 0.5 - if abs(ascent + descent) > 0.1: - frac = ascent / (ascent + descent) + frac = 0.5 + if abs(ascent + descent) > 0.1: + frac = ascent / (ascent + descent) - metrics.write("Snippet %s %f\n" % (match.group(1), frac)) + metrics.write("Snippet %s %f\n" % (match.group(1), frac)) - else: - tp_descent = string.atof(match.group(2)) - tp_ascent = string.atof(match.group(4)) + else: + tp_descent = string.atof(match.group(2)) + tp_ascent = string.atof(match.group(4)) + + except: + # Unable to open the file, but do nothing here because + # the calling function will act on the value of 'success'. + warning('Warning in extract_metrics_info! Unable to open "%s"' % log_file) + warning(`sys.exc_type` + ',' + `sys.exc_value`) return success @@ -112,102 +124,41 @@ def extract_resolution(log_file, dpi): # Default values magnification = 1000.0 - fontsize = 0.0 + fontsize = 10.0 - for line in open(log_file, 'r').readlines(): - if found_fontsize and found_magnification: - break + try: + for line in open(log_file, 'r').readlines(): + if found_fontsize and found_magnification: + break - if not found_fontsize: - match = fontsize_re.match(line) - if match != None: - match = extract_decimal_re.search(line) - if match == None: - error("Unable to parse: %s" % line) - fontsize = string.atof(match.group(1)) - found_fontsize = 1 - continue + if not found_fontsize: + match = fontsize_re.match(line) + if match != None: + match = extract_decimal_re.search(line) + if match == None: + error("Unable to parse: %s" % line) + fontsize = string.atof(match.group(1)) + found_fontsize = 1 + continue - if not found_magnification: - match = magnification_re.match(line) - if match != None: - match = extract_integer_re.search(line) - if match == None: - error("Unable to parse: %s" % line) - magnification = string.atof(match.group(1)) - found_magnification = 1 - continue + if not found_magnification: + match = magnification_re.match(line) + if match != None: + match = extract_integer_re.search(line) + if match == None: + error("Unable to parse: %s" % line) + magnification = string.atof(match.group(1)) + found_magnification = 1 + continue + except: + warning('Warning in extract_resolution! Unable to open "%s"' % log_file) + warning(`sys.exc_type` + ',' + `sys.exc_value`) + + # This is safe because both fontsize and magnification have + # non-zero default values. return dpi * (10.0 / fontsize) * (1000.0 / magnification) - -def get_version_info(): - version_re = re.compile("([0-9])\.([0-9])") - - match = version_re.match(sys.version) - if match == None: - error("Unable to extract version info from 'sys.version'") - - return string.atoi(match.group(1)), string.atoi(match.group(2)) - - -def copyfileobj(fsrc, fdst, rewind=0, length=16*1024): - """copy data from file-like object fsrc to file-like object fdst""" - if rewind: - fsrc.flush() - fsrc.seek(0) - - while 1: - buf = fsrc.read(length) - if not buf: - break - fdst.write(buf) - - -class TempFile: - """clone of tempfile.TemporaryFile to use with python < 2.0.""" - # Cache the unlinker so we don't get spurious errors at shutdown - # when the module-level "os" is None'd out. Note that this must - # be referenced as self.unlink, because the name TempFile - # may also get None'd out before __del__ is called. - unlink = os.unlink - - def __init__(self): - self.filename = tempfile.mktemp() - self.file = open(self.filename,"w+b") - self.close_called = 0 - - def close(self): - if not self.close_called: - self.close_called = 1 - self.file.close() - self.unlink(self.filename) - - def __del__(self): - self.close() - - def read(self, size = -1): - return self.file.read(size) - - def write(self, line): - return self.file.write(line) - - def seek(self, offset): - return self.file.seek(offset) - - def flush(self): - return self.file.flush() - - -def mkstemp(): - """create a secure temporary file and return its object-like file""" - major, minor = get_version_info() - - if major >= 2 and minor >= 0: - return tempfile.TemporaryFile() - else: - return TempFile() - def legacy_latex_file(latex_file, fg_color, bg_color): use_preview_re = re.compile("(\\\\usepackage\[[^]]+)(\]{preview})") @@ -215,19 +166,26 @@ def legacy_latex_file(latex_file, fg_color, bg_color): tmp = mkstemp() success = 0 - for line in open(latex_file, 'r').readlines(): - match = use_preview_re.match(line) - if match == None: - tmp.write(line) - continue + try: + for line in open(latex_file, 'r').readlines(): + match = use_preview_re.match(line) + if match == None: + tmp.write(line) + continue - success = 1 - tmp.write("%s,dvips,tightpage%s\n\n" \ - "\\AtBeginDocument{\\AtBeginDvi{%%\n" \ - "\\special{!userdict begin/bop-hook{//bop-hook exec\n" \ - "<%s%s>{255 div}forall setrgbcolor\n" \ - "clippath fill setrgbcolor}bind def end}}}\n" \ - % (match.group(1), match.group(2), fg_color, bg_color)) + success = 1 + tmp.write("%s,dvips,tightpage%s\n\n" \ + "\\AtBeginDocument{\\AtBeginDvi{%%\n" \ + "\\special{!userdict begin/bop-hook{//bop-hook exec\n" \ + "<%s%s>{255 div}forall setrgbcolor\n" \ + "clippath fill setrgbcolor}bind def end}}}\n" \ + % (match.group(1), match.group(2), fg_color, bg_color)) + + except: + # Unable to open the file, but do nothing here because + # the calling function will act on the value of 'success'. + warning('Warning in legacy_latex_file! Unable to open "%s"' % latex_file) + warning(`sys.exc_type` + ',' + `sys.exc_value`) if success: copyfileobj(tmp, open(latex_file,"wb"), 1) @@ -237,8 +195,8 @@ def legacy_latex_file(latex_file, fg_color, bg_color): def crop_files(pnmcrop, basename): t = pipes.Template() - t.append("%s -left" % pnmcrop, '--') - t.append("%s -right" % pnmcrop, '--') + t.append('"%s" -left' % pnmcrop, '--') + t.append('"%s" -right' % pnmcrop, '--') for file in glob.glob("%s*.ppm" % basename): tmp = mkstemp() @@ -253,13 +211,16 @@ def legacy_conversion(argv): if len(argv) != 6: error(usage(argv[0])) - # Ignore argv[1] - - dir, latex_file = os.path.split(argv[2]) + dir, latex_file = os.path.split(argv[1]) if len(dir) != 0: os.chdir(dir) - dpi = string.atoi(argv[3]) + dpi = string.atoi(argv[2]) + + output_format = argv[3] + if output_format != "ppm": + error("This script will generate ppm format images only.") + fg_color = argv[4] bg_color = argv[5] @@ -275,7 +236,7 @@ def legacy_conversion(argv): error("Unable to move color info into the latex file") # Compile the latex file. - latex_call = "%s %s" % (latex, latex_file) + latex_call = '"%s" "%s"' % (latex, latex_file) latex_status, latex_stdout = run_command(latex_call) if latex_status != None: @@ -286,8 +247,8 @@ def legacy_conversion(argv): dvi_file = latex_file_re.sub(".dvi", latex_file) ps_file = latex_file_re.sub(".ps", latex_file) - dvips_call = "%s -o %s %s" % (dvips, ps_file, dvi_file) - + dvips_call = '"%s" -o "%s" "%s"' % (dvips, ps_file, dvi_file) + dvips_status, dvips_stdout = run_command(dvips_call) if dvips_status != None: error("Failed: %s %s" % (os.path.basename(dvips), dvi_file)) @@ -303,10 +264,10 @@ def legacy_conversion(argv): alpha = 2 # Generate the bitmap images - gs_call = "%s -dNOPAUSE -dBATCH -dSAFER -sDEVICE=pnmraw " \ - "-sOutputFile=%s%%d.ppm " \ - "-dGraphicsAlphaBit=%d -dTextAlphaBits=%d " \ - "-r%f %s" \ + gs_call = '"%s" -dNOPAUSE -dBATCH -dSAFER -sDEVICE=pnmraw ' \ + '-sOutputFile="%s%%d.ppm" ' \ + '-dGraphicsAlphaBit=%d -dTextAlphaBits=%d ' \ + '-r%f "%s"' \ % (gs, latex_file_re.sub("", latex_file), \ alpha, alpha, resolution, ps_file) @@ -324,3 +285,7 @@ def legacy_conversion(argv): error("Failed to extract metrics info from %s" % log_file) return 0 + + +if __name__ == "__main__": + legacy_conversion(sys.argv) diff --git a/lib/scripts/lyxpreview2bitmap.py b/lib/scripts/lyxpreview2bitmap.py index bb7438bf06..7be31537bf 100755 --- a/lib/scripts/lyxpreview2bitmap.py +++ b/lib/scripts/lyxpreview2bitmap.py @@ -31,9 +31,10 @@ # lyxpreview2bitmap.py png 0lyxpreview.tex 128 000000 faf0e6 # This script takes five arguments: -# FORMAT: either 'png' or 'ppm'. The desired output format. +# FORMAT: The desired output format. Either 'png' or 'ppm'. # TEXFILE: the name of the .tex file to be converted. -# DPI: a scale factor, passed to dvipng. +# DPI: a scale factor, used to ascertain the resolution of the +# generated image which is then passed to gs. # FG_COLOR: the foreground color as a hexadecimal string, eg '000000'. # BG_COLOR: the background color as a hexadecimal string, eg 'faf0e6'. @@ -45,8 +46,12 @@ # the images correctly on the screen. import glob, os, re, string, sys + from legacy_lyxpreview2ppm import legacy_conversion +from lyxpreview_tools import error, find_exe, \ + find_exe_or_terminate, run_command + # Pre-compiled regular expressions. hexcolor_re = re.compile("^[0-9a-fA-F]{6}$") @@ -54,46 +59,11 @@ latex_file_re = re.compile("\.tex$") def usage(prog_name): - return "Usage: %s \n"\ + return "Usage: %s \n"\ "\twhere the colors are hexadecimal strings, eg 'faf0e6'"\ % prog_name -def error(message): - sys.stderr.write(message + '\n') - sys.exit(1) - - -def find_exe(candidates, path): - for prog in candidates: - for directory in path: - if os.name == "nt": - full_path = os.path.join(directory, prog + ".exe") - else: - full_path = os.path.join(directory, prog) - - if os.access(full_path, os.X_OK): - return full_path - - return None - - -def find_exe_or_terminate(candidates, path): - exe = find_exe(candidates, path) - if exe == None: - error("Unable to find executable from '%s'" % string.join(candidates)) - - return exe - - -def run_command(cmd): - handle = os.popen(cmd, 'r') - cmd_stdout = handle.read() - cmd_status = handle.close() - - return cmd_status, cmd_stdout - - def make_texcolor(hexcolor): # Test that the input string contains 6 hexadecimal chars. if not hexcolor_re.match(hexcolor): @@ -137,7 +107,7 @@ def convert_to_ppm_format(pngtopnm, basename): for png_file in glob.glob("%s*.png" % basename): ppm_file = png_file_re.sub(".ppm", png_file) - p2p_cmd = "%s %s" % (pngtopnm, png_file) + p2p_cmd = "'%s' '%s'" % (pngtopnm, png_file) p2p_status, p2p_stdout = run_command(p2p_cmd) if p2p_status != None: error("Unable to convert %s to ppm format" % png_file) @@ -170,7 +140,11 @@ def main(argv): dvipng = find_exe(["dvipng"], path) if dvipng == None: if output_format == "ppm": - return legacy_conversion(argv) + # The data is input to legacy_conversion in as similar + # as possible a manner to that input to the code used in + # LyX 1.3.x. + vec = [ argv[0], argv[2], argv[3], argv[1], argv[4], argv[5] ] + return legacy_conversion(vec) else: error("The old 'dvi->ps->ppm' conversion requires " "ppm as the output format") @@ -180,7 +154,7 @@ def main(argv): pngtopnm = find_exe_or_terminate(["pngtopnm"], path) # Compile the latex file. - latex_call = "%s %s" % (latex, latex_file) + latex_call = "'%s' '%s'" % (latex, latex_file) latex_status, latex_stdout = run_command(latex_call) if latex_status != None: @@ -189,7 +163,7 @@ def main(argv): # Run the dvi file through dvipng. dvi_file = latex_file_re.sub(".dvi", latex_file) - dvipng_call = "%s -Ttight -depth -height -D %d -fg '%s' -bg '%s' %s" \ + dvipng_call = "'%s' -Ttight -depth -height -D %d -fg '%s' -bg '%s' '%s'" \ % (dvipng, dpi, fg_color, bg_color, dvi_file) dvipng_status, dvipng_stdout = run_command(dvipng_call) @@ -208,5 +182,6 @@ def main(argv): return 0 + if __name__ == "__main__": main(sys.argv) diff --git a/lib/scripts/lyxpreview_tools.py b/lib/scripts/lyxpreview_tools.py new file mode 100644 index 0000000000..272f3158de --- /dev/null +++ b/lib/scripts/lyxpreview_tools.py @@ -0,0 +1,195 @@ +#! /usr/bin/env python + +# file lyxpreview_tools.py +# This file is part of LyX, the document processor. +# Licence details can be found in the file COPYING. + +# author Angus Leeming +# Full author contact details are available in file CREDITS + +# and with much help testing the code under Windows from +# Paul A. Rubin, rubin@msu.edu. + +# A repository of the following functions, used by the lyxpreview2xyz scripts. +# copyfileobj, error, find_exe, find_exe_or_terminate, mkstemp, +# run_command, warning + +import os, re, string, sys, tempfile + +use_win32_modules = 0 +if os.name == "nt": + use_win32_modules = 1 + try: + import pywintypes + import win32con + import win32event + import win32file + import win32pipe + import win32process + import win32security + import winerror + except: + sys.stderr.write("Consider installing the PyWin extension modules "\ + "if you're irritated by windows appearing briefly.\n") + use_win32_modules = 0 + + +def warning(message): + sys.stderr.write(message + '\n') + + +def error(message): + sys.stderr.write(message + '\n') + sys.exit(1) + + +def find_exe(candidates, path): + for prog in candidates: + for directory in path: + if os.name == "nt": + full_path = os.path.join(directory, prog + ".exe") + else: + full_path = os.path.join(directory, prog) + + if os.access(full_path, os.X_OK): + return full_path + + return None + + +def find_exe_or_terminate(candidates, path): + exe = find_exe(candidates, path) + if exe == None: + error("Unable to find executable from '%s'" % string.join(candidates)) + + return exe + + +def run_command_popen(cmd): + handle = os.popen(cmd, 'r') + cmd_stdout = handle.read() + cmd_status = handle.close() + + return cmd_status, cmd_stdout + + +def run_command_win32(cmd): + sa = win32security.SECURITY_ATTRIBUTES() + sa.bInheritHandle = True + stdout_r, stdout_w = win32pipe.CreatePipe(sa, 0) + + si = win32process.STARTUPINFO() + si.dwFlags = (win32process.STARTF_USESTDHANDLES + | win32process.STARTF_USESHOWWINDOW) + si.wShowWindow = win32con.SW_HIDE + si.hStdOutput = stdout_w + + process, thread, pid, tid = \ + win32process.CreateProcess(None, cmd, None, None, True, + 0, None, None, si) + if process == None: + return -1, "" + + # Must close the write handle in this process, or ReadFile will hang. + stdout_w.Close() + + # Read the pipe until we get an error (including ERROR_BROKEN_PIPE, + # which is okay because it happens when child process ends). + data = "" + error = 0 + while 1: + try: + hr, buffer = win32file.ReadFile(stdout_r, 4096) + if hr != winerror.ERROR_IO_PENDING: + data = data + buffer + + except pywintypes.error, e: + if e.args[0] != winerror.ERROR_BROKEN_PIPE: + error = 1 + break + + if error: + return -2, "" + + # Everything is okay --- the called process has closed the pipe. + # For safety, check that the process ended, then pick up its exit code. + win32event.WaitForSingleObject(process, win32event.INFINITE) + if win32process.GetExitCodeProcess(process): + return -3, "" + + return None, data + + +def run_command(cmd): + if use_win32_modules: + return run_command_win32(cmd) + else: + return run_command_popen(cmd) + + +def get_version_info(): + version_re = re.compile("([0-9])\.([0-9])") + + match = version_re.match(sys.version) + if match == None: + error("Unable to extract version info from 'sys.version'") + + return string.atoi(match.group(1)), string.atoi(match.group(2)) + + +def copyfileobj(fsrc, fdst, rewind=0, length=16*1024): + """copy data from file-like object fsrc to file-like object fdst""" + if rewind: + fsrc.flush() + fsrc.seek(0) + + while 1: + buf = fsrc.read(length) + if not buf: + break + fdst.write(buf) + + +class TempFile: + """clone of tempfile.TemporaryFile to use with python < 2.0.""" + # Cache the unlinker so we don't get spurious errors at shutdown + # when the module-level "os" is None'd out. Note that this must + # be referenced as self.unlink, because the name TempFile + # may also get None'd out before __del__ is called. + unlink = os.unlink + + def __init__(self): + self.filename = tempfile.mktemp() + self.file = open(self.filename,"w+b") + self.close_called = 0 + + def close(self): + if not self.close_called: + self.close_called = 1 + self.file.close() + self.unlink(self.filename) + + def __del__(self): + self.close() + + def read(self, size = -1): + return self.file.read(size) + + def write(self, line): + return self.file.write(line) + + def seek(self, offset): + return self.file.seek(offset) + + def flush(self): + return self.file.flush() + + +def mkstemp(): + """create a secure temporary file and return its object-like file""" + major, minor = get_version_info() + + if major >= 2 and minor >= 0: + return tempfile.TemporaryFile() + else: + return TempFile()