]> git.lyx.org Git - lyx.git/blob - src/graphics/GraphicsConverter.cpp
remove one <boost/shared_ptr.hpp>
[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 Converter::~Converter()
106 {
107         delete pimpl_;
108 }
109
110
111 void Converter::startConversion() const
112 {
113         pimpl_->startConversion();
114 }
115
116
117 boost::signals::connection Converter::connect(slot_type const & slot) const
118 {
119         return pimpl_->finishedConversion.connect(slot);
120 }
121
122
123 FileName const & Converter::convertedFile() const
124 {
125         static FileName const empty;
126         return pimpl_->finished_ ? pimpl_->to_file_ : empty;
127 }
128
129 /** Build the conversion script.
130  *  The script is output to the stream \p script.
131  */
132 static void build_script(FileName const & from_file, string const & to_file_base,
133                   string const & from_format, string const & to_format,
134                   ostream & script);
135
136
137 Converter::Impl::Impl(FileName const & from_file, string const & to_file_base,
138                       string const & from_format, string const & to_format)
139         : valid_process_(false), finished_(false)
140 {
141         LYXERR(Debug::GRAPHICS, "Converter c-tor:\n"
142                 << "\tfrom_file:      " << from_file
143                 << "\n\tto_file_base: " << to_file_base
144                 << "\n\tfrom_format:  " << from_format
145                 << "\n\tto_format:    " << to_format);
146
147         // The converted image is to be stored in this file (we do not
148         // use ChangeExtension because this is a basename which may
149         // nevertheless contain a '.')
150         to_file_ = FileName(to_file_base + '.' +  formats.extension(to_format));
151
152         // The conversion commands are stored in a stringstream
153         ostringstream script;
154         build_script(from_file, to_file_base, from_format, to_format, script);
155         LYXERR(Debug::GRAPHICS, "\tConversion script:"
156                    "\n--------------------------------------\n"
157                 << script.str()
158                 << "\n--------------------------------------\n");
159
160         // Output the script to file.
161         static int counter = 0;
162         script_file_ = FileName(onlyPath(to_file_base) + "lyxconvert" +
163                 convert<string>(counter++) + ".py");
164
165         std::ofstream fs(script_file_.toFilesystemEncoding().c_str());
166         if (!fs.good()) {
167                 lyxerr << "Unable to write the conversion script to \""
168                        << script_file_ << '\n'
169                        << "Please check your directory permissions."
170                        << std::endl;
171                 return;
172         }
173
174         fs << script.str();
175         fs.close();
176
177         // The command needed to run the conversion process
178         // We create a dummy command for ease of understanding of the
179         // list of forked processes.
180         // Note: 'python ' is absolutely essential, or execvp will fail.
181         script_command_ = support::os::python() + ' ' +
182                 quoteName(script_file_.toFilesystemEncoding()) + ' ' +
183                 quoteName(onlyFilename(from_file.toFilesystemEncoding())) + ' ' +
184                 quoteName(to_format);
185         // All is ready to go
186         valid_process_ = true;
187 }
188
189
190 void Converter::Impl::startConversion()
191 {
192         if (!valid_process_) {
193                 converted(0, 1);
194                 return;
195         }
196
197         Forkedcall::SignalTypePtr
198                 ptr = ForkedCallQueue::get().add(script_command_);
199
200         ptr->connect(boost::bind(&Impl::converted, this, _1, _2));
201 }
202
203
204 void Converter::Impl::converted(pid_t /* pid */, int retval)
205 {
206         if (finished_)
207                 // We're done already!
208                 return;
209
210         finished_ = true;
211         // Clean-up behind ourselves
212         unlink(script_file_);
213
214         if (retval > 0) {
215                 unlink(to_file_);
216                 to_file_.erase();
217                 finishedConversion(false);
218         } else {
219                 finishedConversion(true);
220         }
221 }
222
223
224 static string const move_file(string const & from_file, string const & to_file)
225 {
226         if (from_file == to_file)
227                 return string();
228
229         ostringstream command;
230         command << "fromfile = utf8ToDefaultEncoding(" << from_file << ")\n"
231                 << "tofile = utf8ToDefaultEncoding("   << to_file << ")\n\n"
232                 << "try:\n"
233                 << "  os.rename(fromfile, tofile)\n"
234                 << "except:\n"
235                 << "  try:\n"
236                 << "    shutil.copy(fromfile, tofile)\n"
237                 << "  except:\n"
238                 << "    sys.exit(1)\n"
239                 << "  unlinkNoThrow(fromfile)\n";
240
241         return command.str();
242 }
243
244
245 static void build_conversion_command(string const & command, ostream & script)
246 {
247         // Store in the python script
248         script << "\nif os.system(r'" << command << "') != 0:\n";
249
250         // Test that this was successful. If not, remove
251         // ${outfile} and exit the python script
252         script << "  unlinkNoThrow(outfile)\n"
253                << "  sys.exit(1)\n\n";
254
255         // Test that the outfile exists.
256         // ImageMagick's convert will often create ${outfile}.0,
257         // ${outfile}.1.
258         // If this occurs, move ${outfile}.0 to ${outfile}
259         // and delete ${outfile}.? (ignore errors)
260         script << "if not os.path.isfile(outfile):\n"
261                   "  if os.path.isfile(outfile + '.0'):\n"
262                   "    os.rename(outfile + '.0', outfile)\n"
263                   "    import glob\n"
264                   "    for file in glob.glob(outfile + '.?'):\n"
265                   "      unlinkNoThrow(file)\n"
266                   "  else:\n"
267                   "    sys.exit(1)\n\n";
268
269         // Delete the infile
270         script << "unlinkNoThrow(infile)\n\n";
271 }
272
273
274 static void build_script(FileName const & from_file,
275                   string const & to_file_base,
276                   string const & from_format,
277                   string const & to_format,
278                   ostream & script)
279 {
280         BOOST_ASSERT(from_format != to_format);
281         LYXERR(Debug::GRAPHICS, "build_script ... ");
282         typedef Converters::EdgePath EdgePath;
283
284         script << "#!/usr/bin/env python\n"
285                   "# -*- coding: utf-8 -*-\n"
286                   "import os, shutil, sys, locale\n\n"
287                   "def unlinkNoThrow(file):\n"
288                   "  ''' remove a file, do not throw if an error occurs '''\n"
289                   "  try:\n"
290                   "    os.unlink(file)\n"
291                   "  except:\n"
292                   "    pass\n\n"
293                   "def utf8ToDefaultEncoding(file):\n"
294                   "  ''' if possible, convert to the default encoding '''\n"
295                   "  try:\n"
296                   "    language, output_encoding = locale.getdefaultlocale()\n"
297                   "    if output_encoding == None:\n"
298                   "      output_encoding = 'latin1'\n"
299                   "    return unicode(file, 'utf8').encode(output_encoding)\n"
300                   "  except:\n"
301                   "    return file\n\n";
302
303         // we do not use ChangeExtension because this is a basename
304         // which may nevertheless contain a '.'
305         string const to_file = to_file_base + '.'
306                 + formats.extension(to_format);
307
308         EdgePath const edgepath = from_format.empty() ?
309                 EdgePath() :
310                 theConverters().getPath(from_format, to_format);
311
312         // Create a temporary base file-name for all intermediate steps.
313         // Remember to remove the temp file because we only want the name...
314         static int counter = 0;
315         string const tmp = "gconvert" + convert<string>(counter++);
316         FileName const to_base(tempName(FileName(), tmp));
317         unlink(to_base);
318
319         // Create a copy of the file in case the original name contains
320         // problematic characters like ' or ". We can work around that problem
321         // in python, but the converters might be shell scripts and have more
322         // troubles with it.
323         string outfile = addExtension(to_base.absFilename(), getExtension(from_file.absFilename()));
324         script << "infile = utf8ToDefaultEncoding("
325                         << quoteName(from_file.absFilename(), quote_python)
326                         << ")\n"
327                   "outfile = utf8ToDefaultEncoding("
328                         << quoteName(outfile, quote_python) << ")\n"
329                   "shutil.copy(infile, outfile)\n";
330
331         // Some converters (e.g. lilypond) can only output files to the
332         // current directory, so we need to change the current directory.
333         // This has the added benefit that all other files that may be
334         // generated by the converter are deleted when LyX closes and do not
335         // clutter the real working directory.
336         script << "os.chdir(utf8ToDefaultEncoding("
337                << quoteName(onlyPath(outfile)) << "))\n";
338
339         if (edgepath.empty()) {
340                 // Either from_format is unknown or we don't have a
341                 // converter path from from_format to to_format, so we use
342                 // the default converter.
343                 script << "infile = outfile\n"
344                        << "outfile = utf8ToDefaultEncoding("
345                        << quoteName(to_file, quote_python) << ")\n";
346
347                 ostringstream os;
348                 os << support::os::python() << ' '
349                    << libScriptSearch("$$s/scripts/convertDefault.py",
350                                       quote_python) << ' ';
351                 if (!from_format.empty())
352                         os << from_format << ':';
353                 // The extra " quotes around infile and outfile are needed
354                 // because the filename may contain spaces and it is used
355                 // as argument of os.system().
356                 os << "' + '\"' + infile + '\"' + ' "
357                    << to_format << ":' + '\"' + outfile + '\"' + '";
358                 string const command = os.str();
359
360                 LYXERR(Debug::GRAPHICS,
361                         "\tNo converter defined! I use convertDefault.py\n\t"
362                         << command);
363
364                 build_conversion_command(command, script);
365         }
366
367         // The conversion commands may contain these tokens that need to be
368         // changed to infile, infile_base, outfile respectively.
369         string const token_from = "$$i";
370         string const token_base = "$$b";
371         string const token_to   = "$$o";
372
373         EdgePath::const_iterator it  = edgepath.begin();
374         EdgePath::const_iterator end = edgepath.end();
375
376         for (; it != end; ++it) {
377                 lyx::Converter const & conv = theConverters().get(*it);
378
379                 // Build the conversion command
380                 string const infile      = outfile;
381                 string const infile_base = changeExtension(infile, string());
382                 outfile = addExtension(to_base.absFilename(), conv.To->extension());
383
384                 // Store these names in the python script
385                 script << "infile = utf8ToDefaultEncoding("
386                                 << quoteName(infile, quote_python) << ")\n"
387                           "infile_base = utf8ToDefaultEncoding("
388                                 << quoteName(infile_base, quote_python) << ")\n"
389                           "outfile = utf8ToDefaultEncoding("
390                                 << quoteName(outfile, quote_python) << ")\n";
391
392                 // See comment about extra " quotes above (although that
393                 // applies only for the first loop run here).
394                 string command = conv.command;
395                 command = subst(command, token_from, "' + '\"' + infile + '\"' + '");
396                 command = subst(command, token_base, "' + '\"' + infile_base + '\"' + '");
397                 command = subst(command, token_to,   "' + '\"' + outfile + '\"' + '");
398                 command = libScriptSearch(command, quote_python);
399
400                 build_conversion_command(command, script);
401         }
402
403         // Move the final outfile to to_file
404         script << move_file("outfile", quoteName(to_file, quote_python));
405         LYXERR(Debug::GRAPHICS, "ready!");
406 }
407
408 } // namespace graphics
409 } // namespace lyx