]> git.lyx.org Git - lyx.git/blob - src/graphics/GraphicsConverter.cpp
Rename .C ==> .cpp for files in src, part one
[lyx.git] / src / graphics / GraphicsConverter.cpp
1 /**
2  * \file GraphicsConverter.cpp
3  * This file is part of LyX, the document processor.
4  * Licence details can be found in the file COPYING.
5  *
6  * \author Angus Leeming
7  *
8  * Full author contact details are available in file CREDITS.
9  */
10
11 #include <config.h>
12
13 #include "GraphicsConverter.h"
14
15 #include "Converter.h"
16 #include "debug.h"
17 #include "Format.h"
18
19 #include "support/filetools.h"
20 #include "support/forkedcallqueue.h"
21 #include "support/convert.h"
22 #include "support/lstrings.h"
23 #include "support/lyxlib.h"
24 #include "support/os.h"
25
26 #include <boost/bind.hpp>
27
28 #include <sstream>
29 #include <fstream>
30
31 namespace support = lyx::support;
32
33 using support::addExtension;
34 using support::changeExtension;
35 using support::FileName;
36 using support::Forkedcall;
37 using support::ForkedCallQueue;
38 using support::getExtension;
39 using support::libScriptSearch;
40 using support::onlyPath;
41 using support::onlyFilename;
42 using support::quoteName;
43 using support::quote_python;
44 using support::subst;
45 using support::tempName;
46 using support::unlink;
47
48 using std::endl;
49 using std::ostream;
50 using std::ostringstream;
51 using std::string;
52
53
54 namespace lyx {
55 namespace graphics {
56
57 class Converter::Impl : public boost::signals::trackable {
58 public:
59         ///
60         Impl(FileName const &, string const &, string const &, string const &);
61
62         ///
63         void startConversion();
64
65         /** This method is connected to a signal passed to the forked call
66          *  class, passing control back here when the conversion is completed.
67          *  Cleans-up the temporary files, emits the finishedConversion
68          *  signal and removes the Converter from the list of all processes.
69          */
70         void converted(pid_t pid, int retval);
71
72         /** At the end of the conversion process inform the outside world
73          *  by emitting a signal.
74          */
75         typedef boost::signal<void(bool)> SignalType;
76         ///
77         SignalType finishedConversion;
78
79         ///
80         string script_command_;
81         ///
82         FileName script_file_;
83         ///
84         FileName to_file_;
85         ///
86         bool valid_process_;
87         ///
88         bool finished_;
89 };
90
91
92 bool Converter::isReachable(string const & from_format_name,
93                             string const & to_format_name)
94 {
95         return theConverters().isReachable(from_format_name, to_format_name);
96 }
97
98
99 Converter::Converter(FileName const & from_file, string const & to_file_base,
100                      string const & from_format, string const & to_format)
101         : pimpl_(new Impl(from_file, to_file_base, from_format, to_format))
102 {}
103
104
105 // Empty d-tor out-of-line to keep boost::scoped_ptr happy.
106 Converter::~Converter()
107 {}
108
109
110 void Converter::startConversion() const
111 {
112         pimpl_->startConversion();
113 }
114
115
116 boost::signals::connection Converter::connect(slot_type const & slot) const
117 {
118         return pimpl_->finishedConversion.connect(slot);
119 }
120
121
122 FileName const & Converter::convertedFile() const
123 {
124         static FileName const empty;
125         return pimpl_->finished_ ? pimpl_->to_file_ : empty;
126 }
127
128 /** Build the conversion script.
129  *  The script is output to the stream \p script.
130  */
131 static void build_script(FileName const & from_file, string const & to_file_base,
132                   string const & from_format, string const & to_format,
133                   ostream & script);
134
135
136 Converter::Impl::Impl(FileName const & from_file, string const & to_file_base,
137                       string const & from_format, string const & to_format)
138         : valid_process_(false), finished_(false)
139 {
140         LYXERR(Debug::GRAPHICS) << "Converter c-tor:\n"
141                 << "\tfrom_file:      " << from_file
142                 << "\n\tto_file_base: " << to_file_base
143                 << "\n\tfrom_format:  " << from_format
144                 << "\n\tto_format:    " << to_format << endl;
145
146         // The converted image is to be stored in this file (we do not
147         // use ChangeExtension because this is a basename which may
148         // nevertheless contain a '.')
149         to_file_ = FileName(to_file_base + '.' +  formats.extension(to_format));
150
151         // The conversion commands are stored in a stringstream
152         ostringstream script;
153         build_script(from_file, to_file_base, from_format, to_format, script);
154         LYXERR(Debug::GRAPHICS) << "\tConversion script:"
155                 << "\n--------------------------------------\n"
156                 << script.str()
157                 << "\n--------------------------------------\n";
158
159         // Output the script to file.
160         static int counter = 0;
161         script_file_ = FileName(onlyPath(to_file_base) + "lyxconvert" +
162                 convert<string>(counter++) + ".py");
163
164         std::ofstream fs(script_file_.toFilesystemEncoding().c_str());
165         if (!fs.good()) {
166                 lyxerr << "Unable to write the conversion script to \""
167                        << script_file_ << '\n'
168                        << "Please check your directory permissions."
169                        << std::endl;
170                 return;
171         }
172
173         fs << script.str();
174         fs.close();
175
176         // The command needed to run the conversion process
177         // We create a dummy command for ease of understanding of the
178         // list of forked processes.
179         // Note: 'python ' is absolutely essential, or execvp will fail.
180         script_command_ = support::os::python() + ' ' +
181                 quoteName(script_file_.toFilesystemEncoding()) + ' ' +
182                 quoteName(onlyFilename(from_file.toFilesystemEncoding())) + ' ' +
183                 quoteName(to_format);
184         // All is ready to go
185         valid_process_ = true;
186 }
187
188
189 void Converter::Impl::startConversion()
190 {
191         if (!valid_process_) {
192                 converted(0, 1);
193                 return;
194         }
195
196         Forkedcall::SignalTypePtr
197                 ptr = ForkedCallQueue::get().add(script_command_);
198
199         ptr->connect(boost::bind(&Impl::converted, this, _1, _2));
200
201 }
202
203 void Converter::Impl::converted(pid_t /* pid */, int retval)
204 {
205         if (finished_)
206                 // We're done already!
207                 return;
208
209         finished_ = true;
210         // Clean-up behind ourselves
211         unlink(script_file_);
212
213         if (retval > 0) {
214                 unlink(to_file_);
215                 to_file_.erase();
216                 finishedConversion(false);
217         } else {
218                 finishedConversion(true);
219         }
220 }
221
222
223 static string const move_file(string const & from_file, string const & to_file)
224 {
225         if (from_file == to_file)
226                 return string();
227
228         ostringstream command;
229         command << "fromfile = " << from_file << "\n"
230                 << "tofile = "   << to_file << "\n\n"
231                 << "try:\n"
232                 << "  os.rename(fromfile, tofile)\n"
233                 << "except:\n"
234                 << "  try:\n"
235                 << "    shutil.copy(fromfile, tofile)\n"
236                 << "  except:\n"
237                 << "    sys.exit(1)\n"
238                 << "  unlinkNoThrow(fromfile)\n";
239
240         return command.str();
241 }
242
243
244 static void build_conversion_command(string const & command, ostream & script)
245 {
246         // Store in the python script
247         script << "\nif os.system(r'" << command << "') != 0:\n";
248
249         // Test that this was successful. If not, remove
250         // ${outfile} and exit the python script
251         script << "  unlinkNoThrow(outfile)\n"
252                << "  sys.exit(1)\n\n";
253
254         // Test that the outfile exists.
255         // ImageMagick's convert will often create ${outfile}.0,
256         // ${outfile}.1.
257         // If this occurs, move ${outfile}.0 to ${outfile}
258         // and delete ${outfile}.? (ignore errors)
259         script << "if not os.path.isfile(outfile):\n"
260                   "  if os.path.isfile(outfile + '.0'):\n"
261                   "    os.rename(outfile + '.0', outfile)\n"
262                   "    import glob\n"
263                   "    for file in glob.glob(outfile + '.?'):\n"
264                   "      unlinkNoThrow(file)\n"
265                   "  else:\n"
266                   "    sys.exit(1)\n\n";
267
268         // Delete the infile
269         script << "unlinkNoThrow(infile)\n\n";
270 }
271
272
273 static void build_script(FileName const & from_file,
274                   string const & to_file_base,
275                   string const & from_format,
276                   string const & to_format,
277                   ostream & script)
278 {
279         BOOST_ASSERT(from_format != to_format);
280         LYXERR(Debug::GRAPHICS) << "build_script ... ";
281         typedef Converters::EdgePath EdgePath;
282
283         script << "#!/usr/bin/env python\n"
284                   "# -*- coding: utf-8 -*-\n"
285                   "import os, shutil, sys, locale\n\n"
286                   "def unlinkNoThrow(file):\n"
287                   "  ''' remove a file, do not throw if an error occurs '''\n"
288                   "  try:\n"
289                   "    os.unlink(file)\n"
290                   "  except:\n"
291                   "    pass\n\n"
292                   "def utf8ToDefaultEncoding(file):\n"
293                   "  ''' if possible, convert to the default encoding '''\n"
294                   "  try:\n"
295                   "    language, output_encoding = locale.getdefaultlocale()\n"
296                   "    if output_encoding == None:\n"
297                   "      output_encoding = 'latin1'\n"
298                   "    return unicode(file, 'utf8').encode(output_encoding)\n"
299                   "  except:\n"
300                   "    return file\n\n";
301
302         // we do not use ChangeExtension because this is a basename
303         // which may nevertheless contain a '.'
304         string const to_file = to_file_base + '.'
305                 + formats.extension(to_format);
306
307         EdgePath const edgepath = from_format.empty() ?
308                 EdgePath() :
309                 theConverters().getPath(from_format, to_format);
310
311         // Create a temporary base file-name for all intermediate steps.
312         // Remember to remove the temp file because we only want the name...
313         static int counter = 0;
314         string const tmp = "gconvert" + convert<string>(counter++);
315         FileName const to_base(tempName(FileName(), tmp));
316         unlink(to_base);
317
318         // Create a copy of the file in case the original name contains
319         // problematic characters like ' or ". We can work around that problem
320         // in python, but the converters might be shell scripts and have more
321         // troubles with it.
322         string outfile = addExtension(to_base.absFilename(), getExtension(from_file.absFilename()));
323         script << "infile = utf8ToDefaultEncoding("
324                         << quoteName(from_file.absFilename(), quote_python)
325                         << ")\n"
326                   "outfile = " << quoteName(outfile, quote_python) << "\n"
327                   "shutil.copy(infile, outfile)\n";
328
329         // Some converters (e.g. lilypond) can only output files to the
330         // current directory, so we need to change the current directory.
331         // This has the added benefit that all other files that may be
332         // generated by the converter are deleted when LyX closes and do not
333         // clutter the real working directory.
334         script << "os.chdir(" << quoteName(onlyPath(outfile)) << ")\n";
335
336         if (edgepath.empty()) {
337                 // Either from_format is unknown or we don't have a
338                 // converter path from from_format to to_format, so we use
339                 // the default converter.
340                 script << "infile = outfile\n"
341                        << "outfile = " << quoteName(to_file, quote_python)
342                        << '\n';
343
344                 ostringstream os;
345                 os << support::os::python() << ' '
346                    << libScriptSearch("$$s/scripts/convertDefault.py",
347                                       quote_python) << ' ';
348                 if (!from_format.empty())
349                         os << from_format << ':';
350                 // The extra " quotes around infile and outfile are needed
351                 // because the filename may contain spaces and it is used
352                 // as argument of os.system().
353                 os << "' + '\"' + infile + '\"' + ' "
354                    << to_format << ":' + '\"' + outfile + '\"' + '";
355                 string const command = os.str();
356
357                 LYXERR(Debug::GRAPHICS)
358                         << "\tNo converter defined! I use convertDefault.py\n\t"
359                         << command << endl;
360
361                 build_conversion_command(command, script);
362         }
363
364         // The conversion commands may contain these tokens that need to be
365         // changed to infile, infile_base, outfile respectively.
366         string const token_from("$$i");
367         string const token_base("$$b");
368         string const token_to("$$o");
369
370         EdgePath::const_iterator it  = edgepath.begin();
371         EdgePath::const_iterator end = edgepath.end();
372
373         for (; it != end; ++it) {
374                 lyx::Converter const & conv = theConverters().get(*it);
375
376                 // Build the conversion command
377                 string const infile      = outfile;
378                 string const infile_base = changeExtension(infile, string());
379                 outfile = addExtension(to_base.absFilename(), conv.To->extension());
380
381                 // Store these names in the python script
382                 script << "infile = "      << quoteName(infile, quote_python) << "\n"
383                           "infile_base = " << quoteName(infile_base, quote_python) << "\n"
384                           "outfile = "     << quoteName(outfile, quote_python) << '\n';
385
386                 // See comment about extra " quotes above (although that
387                 // applies only for the first loop run here).
388                 string command = conv.command;
389                 command = subst(command, token_from, "' + '\"' + infile + '\"' + '");
390                 command = subst(command, token_base, "' + '\"' + infile_base + '\"' + '");
391                 command = subst(command, token_to,   "' + '\"' + outfile + '\"' + '");
392                 command = libScriptSearch(command, quote_python);
393
394                 build_conversion_command(command, script);
395         }
396
397         // Move the final outfile to to_file
398         script << move_file("outfile", quoteName(to_file, quote_python));
399         LYXERR(Debug::GRAPHICS) << "ready!" << endl;
400 }
401
402 } // namespace graphics
403
404 } // namespace lyx