]> git.lyx.org Git - lyx.git/blobdiff - src/graphics/GraphicsConverter.C
* src/LaTeX.C
[lyx.git] / src / graphics / GraphicsConverter.C
index 548a524fd18860f407050900190b4adc08296a6f..3a11012ba7ed81d67a7d7c73f8b54e0b652ae236 100644 (file)
@@ -1,42 +1,63 @@
 /**
- *  \file GraphicsConverter.C
- *  Copyright 2002 the LyX Team
- *  Read the file COPYING
+ * \file GraphicsConverter.C
+ * This file is part of LyX, the document processor.
+ * Licence details can be found in the file COPYING.
  *
- *  \author Angus Leeming <leeming@lyx.org>
+ * \author Angus Leeming
+ *
+ * Full author contact details are available in file CREDITS.
  */
 
 #include <config.h>
 
-#ifdef __GNUG__
-#pragma implementation
-#endif
-
 #include "GraphicsConverter.h"
 
 #include "converter.h"
 #include "debug.h"
+#include "format.h"
 
 #include "support/filetools.h"
-#include "support/forkedcall.h"
+#include "support/forkedcallqueue.h"
+#include "support/convert.h"
+#include "support/lstrings.h"
 #include "support/lyxlib.h"
+#include "support/os.h"
 
 #include <boost/bind.hpp>
-#include <boost/signals/trackable.hpp>
 
-#include "Lsstream.h"
-#include "support/LOstream.h"
+#include <sstream>
 #include <fstream>
-#include <sys/types.h> // needed for pid_t
+
+namespace support = lyx::support;
+
+using support::addExtension;
+using support::changeExtension;
+using support::FileName;
+using support::Forkedcall;
+using support::ForkedCallQueue;
+using support::getExtension;
+using support::libScriptSearch;
+using support::onlyPath;
+using support::onlyFilename;
+using support::quoteName;
+using support::quote_python;
+using support::subst;
+using support::tempName;
+using support::unlink;
 
 using std::endl;
 using std::ostream;
+using std::ostringstream;
+using std::string;
 
-namespace grfx {
 
-struct Converter::Impl : public boost::signals::trackable {
+namespace lyx {
+namespace graphics {
+
+class Converter::Impl : public boost::signals::trackable {
+public:
        ///
-       Impl(string const &, string const &, string const &, string const &);
+       Impl(FileName const &, string const &, string const &, string const &);
 
        ///
        void startConversion();
@@ -46,21 +67,21 @@ struct Converter::Impl : public boost::signals::trackable {
         *  Cleans-up the temporary files, emits the finishedConversion
         *  signal and removes the Converter from the list of all processes.
         */
-       void converted(string const & cmd, pid_t pid, int retval);
+       void converted(pid_t pid, int retval);
 
        /** At the end of the conversion process inform the outside world
         *  by emitting a signal.
         */
-       typedef boost::signal1<void, bool> SignalType;
+       typedef boost::signal<void(bool)> SignalType;
        ///
        SignalType finishedConversion;
 
        ///
        string script_command_;
        ///
-       string script_file_;
+       FileName script_file_;
        ///
-       string to_file_;
+       FileName to_file_;
        ///
        bool valid_process_;
        ///
@@ -71,16 +92,17 @@ struct Converter::Impl : public boost::signals::trackable {
 bool Converter::isReachable(string const & from_format_name,
                            string const & to_format_name)
 {
-       return converters.isReachable(from_format_name, to_format_name);
+       return theConverters().isReachable(from_format_name, to_format_name);
 }
 
 
-Converter::Converter(string const & from_file,   string const & to_file_base,
+Converter::Converter(FileName const & from_file, string const & to_file_base,
                     string const & from_format, string const & to_format)
        : pimpl_(new Impl(from_file, to_file_base, from_format, to_format))
 {}
 
 
+// Empty d-tor out-of-line to keep boost::scoped_ptr happy.
 Converter::~Converter()
 {}
 
@@ -97,33 +119,21 @@ boost::signals::connection Converter::connect(slot_type const & slot) const
 }
 
 
-string const & Converter::convertedFile() const
+FileName const & Converter::convertedFile() const
 {
-       static string const empty;
+       static FileName const empty;
        return pimpl_->finished_ ? pimpl_->to_file_ : empty;
 }
 
-} // namespace grfx
-
-//------------------------------
-// Implementation details follow
-//------------------------------
-
-namespace {
-
-/** Build the conversion script, returning true if able to build it.
- *  The script is output to the ostringstream 'script'.
+/** Build the conversion script.
+ *  The script is output to the stream \p script.
  */
-bool build_script(string const & from_file, string const & to_file_base,
+static void build_script(FileName const & from_file, string const & to_file_base,
                  string const & from_format, string const & to_format,
                  ostream & script);
 
-} // namespace anon
-
-
-namespace grfx {
 
-Converter::Impl::Impl(string const & from_file,   string const & to_file_base,
+Converter::Impl::Impl(FileName const & from_file, string const & to_file_base,
                      string const & from_format, string const & to_format)
        : valid_process_(false), finished_(false)
 {
@@ -133,59 +143,44 @@ Converter::Impl::Impl(string const & from_file,   string const & to_file_base,
                << "\n\tfrom_format:  " << from_format
                << "\n\tto_format:    " << to_format << endl;
 
+       // The converted image is to be stored in this file (we do not
+       // use ChangeExtension because this is a basename which may
+       // nevertheless contain a '.')
+       to_file_ = FileName(to_file_base + '.' +  formats.extension(to_format));
+
        // The conversion commands are stored in a stringstream
        ostringstream script;
-       script << "#!/bin/sh\n";
-       bool const success = build_script(from_file, to_file_base,
-                                         from_format, to_format, script);
-
-       // The converted image is to be stored in this file
-       to_file_ = ChangeExtension(to_file_base, formats.extension(to_format));
-
-       if (!success) {
-               script_file_ = string();
-               if (from_format == "lyxpreview") {
-                       script_command_ =
-                               LibFileSearch("scripts", "lyxpreview2xpm.sh")
-                                       + " " +from_file + " " + to_file_;
-                       lyxerr[Debug::GRAPHICS]
-                               << "\tI use lyxpreview2xpm for the conversion\n\t"
-                               << script_command_ << endl;
-               } else {
-                       script_command_ =
-                               LibFileSearch("scripts", "convertDefault.sh") +
-                                       ' ' + from_format + ':' + from_file + ' ' +
-                                       to_format + ':' + to_file_;
-                       lyxerr[Debug::GRAPHICS]
-                               << "\tNo converter defined! I use convertDefault.sh\n\t"
-                               << script_command_ << endl;
-               }
-       } else {
+       build_script(from_file, to_file_base, from_format, to_format, script);
+       lyxerr[Debug::GRAPHICS] << "\tConversion script:"
+               << "\n--------------------------------------\n"
+               << script.str()
+               << "\n--------------------------------------\n";
 
-               lyxerr[Debug::GRAPHICS] << "\tConversion script:"
-                               << "\n--------------------------------------\n"
-                               << script.str().c_str()
-                               << "\n--------------------------------------\n";
-
-               // Output the script to file.
-               static int counter = 0;
-               script_file_ = OnlyPath(to_file_base) + "lyxconvert" +
-                       tostr(counter++) + ".sh";
-
-               std::ofstream fs(script_file_.c_str());
-               if (!fs.good())
-                       return;
-
-               fs << script.str().c_str();
-               fs.close();
-
-               // The command needed to run the conversion process
-               // We create a dummy command for ease of understanding of the
-               // list of forked processes.
-               // Note that 'sh ' is absolutely essential, or execvp will fail.
-               script_command_ = "sh " + script_file_ + " " +
-                       OnlyFilename(from_file) + " " + to_format;
+       // Output the script to file.
+       static int counter = 0;
+       script_file_ = FileName(onlyPath(to_file_base) + "lyxconvert" +
+               convert<string>(counter++) + ".py");
+
+       std::ofstream fs(script_file_.toFilesystemEncoding().c_str());
+       if (!fs.good()) {
+               lyxerr << "Unable to write the conversion script to \""
+                      << script_file_ << '\n'
+                      << "Please check your directory permissions."
+                      << std::endl;
+               return;
        }
+
+       fs << script.str();
+       fs.close();
+
+       // The command needed to run the conversion process
+       // We create a dummy command for ease of understanding of the
+       // list of forked processes.
+       // Note: 'python ' is absolutely essential, or execvp will fail.
+       script_command_ = support::os::python() + ' ' +
+               quoteName(script_file_.toFilesystemEncoding()) + ' ' +
+               quoteName(onlyFilename(from_file.toFilesystemEncoding())) + ' ' +
+               quoteName(to_format);
        // All is ready to go
        valid_process_ = true;
 }
@@ -194,28 +189,18 @@ Converter::Impl::Impl(string const & from_file,   string const & to_file_base,
 void Converter::Impl::startConversion()
 {
        if (!valid_process_) {
-               converted(string(), 0, 1);
+               converted(0, 1);
                return;
        }
 
-       // Initiate the conversion
-       Forkedcall::SignalTypePtr convert_ptr;
-       convert_ptr.reset(new Forkedcall::SignalType);
+       Forkedcall::SignalTypePtr
+               ptr = ForkedCallQueue::get().add(script_command_);
 
-       convert_ptr->connect(
-               boost::bind(&Impl::converted, this, _1, _2, _3));
+       ptr->connect(boost::bind(&Impl::converted, this, _1, _2));
 
-       Forkedcall call;
-       int retval = call.startscript(script_command_, convert_ptr);
-       if (retval > 0) {
-               // Unable to even start the script, so clean-up the mess!
-               converted(string(), 0, 1);
-       }
 }
 
-
-void Converter::Impl::converted(string const & /* cmd */,
-                               pid_t /* pid */, int retval)
+void Converter::Impl::converted(pid_t /* pid */, int retval)
 {
        if (finished_)
                // We're done already!
@@ -223,10 +208,10 @@ void Converter::Impl::converted(string const & /* cmd */,
 
        finished_ = true;
        // Clean-up behind ourselves
-       lyx::unlink(script_file_);
+       unlink(script_file_);
 
        if (retval > 0) {
-               lyx::unlink(to_file_);
+               unlink(to_file_);
                to_file_.erase();
                finishedConversion(false);
        } else {
@@ -234,64 +219,147 @@ void Converter::Impl::converted(string const & /* cmd */,
        }
 }
 
-} // namespace grfx
 
-namespace {
-
-string const move_file(string const & from_file, string const & to_file)
+static string const move_file(string const & from_file, string const & to_file)
 {
        if (from_file == to_file)
                return string();
 
        ostringstream command;
-       command << "fromfile=" << from_file << "\n"
-               << "tofile="   << to_file << "\n\n"
-               << "'mv' -f ${fromfile} ${tofile}\n"
-               << "if [ $? -ne 0 ]; then\n"
-               << "\t'cp' -f ${fromfile} ${tofile}\n"
-               << "\tif [ $? -ne 0 ]; then\n"
-               << "\t\texit 1\n"
-               << "\tfi\n"
-               << "\t'rm' -f ${fromfile}\n"
-               << "fi\n";
-
-       return command.str().c_str();
+       command << "fromfile = " << from_file << "\n"
+               << "tofile = "   << to_file << "\n\n"
+               << "try:\n"
+               << "  os.rename(fromfile, tofile)\n"
+               << "except:\n"
+               << "  try:\n"
+               << "    shutil.copy(fromfile, tofile)\n"
+               << "  except:\n"
+               << "    sys.exit(1)\n"
+               << "  unlinkNoThrow(fromfile)\n";
+
+       return command.str();
+}
+
+
+static void build_conversion_command(string const & command, ostream & script)
+{
+       // Store in the python script
+       script << "\nif os.system(r'" << command << "') != 0:\n";
+
+       // Test that this was successful. If not, remove
+       // ${outfile} and exit the python script
+       script << "  unlinkNoThrow(outfile)\n"
+              << "  sys.exit(1)\n\n";
+
+       // Test that the outfile exists.
+       // ImageMagick's convert will often create ${outfile}.0,
+       // ${outfile}.1.
+       // If this occurs, move ${outfile}.0 to ${outfile}
+       // and delete ${outfile}.? (ignore errors)
+       script << "if not os.path.isfile(outfile):\n"
+                 "  if os.path.isfile(outfile + '.0'):\n"
+                 "    os.rename(outfile + '.0', outfile)\n"
+                 "    import glob\n"
+                 "    for file in glob.glob(outfile + '.?'):\n"
+                 "      unlinkNoThrow(file)\n"
+                 "  else:\n"
+                 "    sys.exit(1)\n\n";
+
+       // Delete the infile
+       script << "unlinkNoThrow(infile)\n\n";
 }
 
 
-bool build_script(string const & from_file,
+static void build_script(FileName const & from_file,
                  string const & to_file_base,
                  string const & from_format,
                  string const & to_format,
                  ostream & script)
 {
+       BOOST_ASSERT(from_format != to_format);
        lyxerr[Debug::GRAPHICS] << "build_script ... ";
        typedef Converters::EdgePath EdgePath;
 
-       string const to_file = ChangeExtension(to_file_base,
-                                              formats.extension(to_format));
-
-       if (from_format == to_format) {
-               script << move_file(QuoteName(from_file), QuoteName(to_file));
-               lyxerr[Debug::GRAPHICS] << "ready (from == to)" << endl;
-               return true;
-       }
-
-       EdgePath edgepath = converters.getPath(from_format, to_format);
-
-       if (edgepath.empty()) {
-               lyxerr[Debug::GRAPHICS] << "ready (edgepath.empty())" << endl;
-               return false;
-       }
+       script << "#!/usr/bin/env python\n"
+                 "# -*- coding: utf-8 -*-\n"
+                 "import os, shutil, sys, locale\n\n"
+                 "def unlinkNoThrow(file):\n"
+                 "  ''' remove a file, do not throw if an error occurs '''\n"
+                 "  try:\n"
+                 "    os.unlink(file)\n"
+                 "  except:\n"
+                 "    pass\n\n"
+                 "def utf8ToDefaultEncoding(file):\n"
+                 "  ''' if possible, convert to the default encoding '''\n"
+                 "  try:\n"
+                 "    language, output_encoding = locale.getdefaultlocale()\n"
+                 "    if output_encoding == None:\n"
+                 "      output_encoding = 'latin1'\n"
+                 "    return unicode(file, 'utf8').encode(output_encoding)\n"
+                 "  except:\n"
+                 "    return file\n\n";
+
+       // we do not use ChangeExtension because this is a basename
+       // which may nevertheless contain a '.'
+       string const to_file = to_file_base + '.'
+               + formats.extension(to_format);
+
+       EdgePath const edgepath = from_format.empty() ?
+               EdgePath() :
+               theConverters().getPath(from_format, to_format);
 
        // Create a temporary base file-name for all intermediate steps.
        // Remember to remove the temp file because we only want the name...
        static int counter = 0;
-       string const tmp = "gconvert" + tostr(counter++);
-       string const to_base = lyx::tempName(string(), tmp);
-       lyx::unlink(to_base);
+       string const tmp = "gconvert" + convert<string>(counter++);
+       FileName const to_base(tempName(FileName(), tmp));
+       unlink(to_base);
+
+       // Create a copy of the file in case the original name contains
+       // problematic characters like ' or ". We can work around that problem
+       // in python, but the converters might be shell scripts and have more
+       // troubles with it.
+       string outfile = addExtension(to_base.absFilename(), getExtension(from_file.absFilename()));
+       script << "infile = utf8ToDefaultEncoding("
+                       << quoteName(from_file.absFilename(), quote_python)
+                       << ")\n"
+                 "outfile = " << quoteName(outfile, quote_python) << "\n"
+                 "shutil.copy(infile, outfile)\n";
+
+       // Some converters (e.g. lilypond) can only output files to the
+       // current directory, so we need to change the current directory.
+       // This has the added benefit that all other files that may be
+       // generated by the converter are deleted when LyX closes and do not
+       // clutter the real working directory.
+       script << "os.chdir(" << quoteName(onlyPath(outfile)) << ")\n";
 
-       string outfile = from_file;
+       if (edgepath.empty()) {
+               // Either from_format is unknown or we don't have a
+               // converter path from from_format to to_format, so we use
+               // the default converter.
+               script << "infile = outfile\n"
+                      << "outfile = " << quoteName(to_file, quote_python)
+                      << '\n';
+
+               ostringstream os;
+               os << support::os::python() << ' '
+                  << libScriptSearch("$$s/scripts/convertDefault.py",
+                                     quote_python) << ' ';
+               if (!from_format.empty())
+                       os << from_format << ':';
+               // The extra " quotes around infile and outfile are needed
+               // because the filename may contain spaces and it is used
+               // as argument of os.system().
+               os << "' + '\"' + infile + '\"' + ' "
+                  << to_format << ":' + '\"' + outfile + '\"' + '";
+               string const command = os.str();
+
+               lyxerr[Debug::GRAPHICS]
+                       << "\tNo converter defined! I use convertDefault.py\n\t"
+                       << command << endl;
+
+               build_conversion_command(command, script);
+       }
 
        // The conversion commands may contain these tokens that need to be
        // changed to infile, infile_base, outfile respectively.
@@ -303,59 +371,34 @@ bool build_script(string const & from_file,
        EdgePath::const_iterator end = edgepath.end();
 
        for (; it != end; ++it) {
-               ::Converter const & conv = converters.get(*it);
+               lyx::Converter const & conv = theConverters().get(*it);
 
                // Build the conversion command
                string const infile      = outfile;
-               string const infile_base = ChangeExtension(infile, string());
-               outfile = ChangeExtension(to_base, conv.To->extension());
+               string const infile_base = changeExtension(infile, string());
+               outfile = addExtension(to_base.absFilename(), conv.To->extension());
 
-               // Store these names in the shell script
-               script << "infile="      << QuoteName(infile) << '\n'
-                      << "infile_base=" << QuoteName(infile_base) << '\n'
-                      << "outfile="     << QuoteName(outfile) << '\n';
+               // Store these names in the python script
+               script << "infile = "      << quoteName(infile, quote_python) << "\n"
+                         "infile_base = " << quoteName(infile_base, quote_python) << "\n"
+                         "outfile = "     << quoteName(outfile, quote_python) << '\n';
 
+               // See comment about extra " quotes above (although that
+               // applies only for the first loop run here).
                string command = conv.command;
-               command = subst(command, token_from, "${infile}");
-               command = subst(command, token_base, "${infile_base}");
-               command = subst(command, token_to,   "${outfile}");
-               command = LibScriptSearch(command);
-
-               // Store in the shell script
-               script << "\n" << command << "\n\n";
-
-               // Test that this was successful. If not, remove
-               // ${outfile} and exit the shell script
-               script << "if [ $? -ne 0 ]; then\n"
-                      << "\t'rm' -f ${outfile}\n"
-                      << "\texit 1\n"
-                      << "fi\n\n";
-
-               // Test that the outfile exists.
-               // ImageMagick's convert will often create ${outfile}.0,
-               // ${outfile}.1.
-               // If this occurs, move ${outfile}.0 to ${outfile}
-               // and delete ${outfile}.?
-               script << "if [ ! -f ${outfile} ]; then\n"
-                      << "\tif [ -f ${outfile}.0 ]; then\n"
-                      << "\t\t'mv' -f ${outfile}.0 ${outfile}\n"
-                      << "\t\t'rm' -f ${outfile}.?\n"
-                      << "\telse\n"
-                      << "\t\texit 1\n"
-                      << "\tfi\n"
-                      << "fi\n\n";
-
-               // Delete the infile, if it isn't the original, from_file.
-               if (infile != from_file) {
-                       script << "'rm' -f ${infile}\n\n";
-               }
+               command = subst(command, token_from, "' + '\"' + infile + '\"' + '");
+               command = subst(command, token_base, "' + '\"' + infile_base + '\"' + '");
+               command = subst(command, token_to,   "' + '\"' + outfile + '\"' + '");
+               command = libScriptSearch(command, quote_python);
+
+               build_conversion_command(command, script);
        }
 
        // Move the final outfile to to_file
-       script << move_file("${outfile}", QuoteName(to_file));
+       script << move_file("outfile", quoteName(to_file, quote_python));
        lyxerr[Debug::GRAPHICS] << "ready!" << endl;
-
-       return true;
 }
 
-} // namespace anon
+} // namespace graphics
+
+} // namespace lyx