]> git.lyx.org Git - lyx.git/blob - src/graphics/PreviewLoader.C
New lyxrc variables for the preview stuff.
[lyx.git] / src / graphics / PreviewLoader.C
1 /*
2  *  \file PreviewLoader.C
3  *  Copyright 2002 the LyX Team
4  *  Read the file COPYING
5  *
6  * \author Angus Leeming <a.leeming@ic.ac.uk>
7  */
8
9 #include <config.h>
10
11 #ifdef __GNUG__
12 #pragma implementation
13 #endif
14
15 #include "PreviewLoader.h"
16 #include "PreviewImage.h"
17
18 #include "buffer.h"
19 #include "bufferparams.h"
20 #include "converter.h"
21 #include "debug.h"
22 #include "lyxrc.h"
23 #include "lyxtextclasslist.h"
24 #include "LColor.h"
25
26 #include "insets/inset.h"
27
28 #include "frontends/lyx_gui.h" // hexname
29
30 #include "support/filetools.h"
31 #include "support/forkedcall.h"
32 #include "support/forkedcontr.h"
33 #include "support/lstrings.h"
34 #include "support/lyxlib.h"
35
36 #include <boost/bind.hpp>
37 #include <boost/signals/trackable.hpp>
38
39 #include <fstream>
40 #include <iomanip>
41 #include <list>
42 #include <map>
43 #include <utility>
44 #include <vector>
45
46 using std::endl;
47 using std::find;
48 using std::fill;
49 using std::find_if;
50 using std::getline;
51 using std::make_pair;
52 using std::setfill;
53 using std::setw;
54
55 using std::list;
56 using std::map;
57 using std::ifstream;
58 using std::ofstream;
59 using std::ostream;
60 using std::pair;
61 using std::vector;
62
63 namespace {
64
65 typedef pair<string, string> StrPair;
66
67 typedef list<string> PendingStore;
68
69 typedef vector<StrPair> InProgressStore;
70
71
72 double setFontScalingFactor(Buffer &);
73
74 string const unique_filename(string const bufferpath);
75
76 Converter const * setConverter();
77
78 void setAscentFractions(vector<double> & ascent_fractions,
79                         string const & metrics_file);
80
81 struct FindFirst {
82         FindFirst(string const & comp) : comp_(comp) {}
83         bool operator()(StrPair const & sp)
84         {
85                 return sp.first < comp_;
86         }
87 private:
88         string const comp_;
89 };
90
91
92 /// Store info on a currently executing, forked process.
93 struct InProgress {
94         ///
95         InProgress() : pid(0) {}
96         ///
97         InProgress(string const & filename_base,
98                    PendingStore const & pending,
99                    string const & to_format);
100         /// Remove any files left lying around and kill the forked process. 
101         void stop() const;
102
103         ///
104         pid_t pid;
105         ///
106         string metrics_file;
107         /// Each item in the vector is a pair<snippet, image file name>.
108         InProgressStore snippets;
109 };
110
111
112 } // namespace anon
113
114
115 namespace grfx {
116
117 struct PreviewLoader::Impl : public boost::signals::trackable {
118         ///
119         Impl(PreviewLoader & p, Buffer const & b);
120         /// Stop any InProgress items still executing.
121         ~Impl();
122         ///
123         PreviewImage const * preview(string const & latex_snippet) const;
124         ///
125         PreviewLoader::Status status(string const & latex_snippet) const;
126         ///
127         void add(string const & latex_snippet);
128         ///
129         void remove(string const & latex_snippet);
130         ///
131         void startLoading();
132
133 private:
134         /// Called by the Forkedcall process that generated the bitmap files.
135         void finishedGenerating(string const &, pid_t, int);
136         ///
137         void dumpPreamble(ostream &) const;
138         ///
139         void dumpData(ostream &, InProgressStore const &) const;
140
141         /** cache_ allows easy retrieval of already-generated images
142          *  using the LaTeX snippet as the identifier.
143          */
144         typedef boost::shared_ptr<PreviewImage> PreviewImagePtr;
145         ///
146         typedef map<string, PreviewImagePtr> Cache;
147         ///
148         Cache cache_;
149
150         /** pending_ stores the LaTeX snippets in anticipation of them being
151          *  sent to the converter.
152          */
153         PendingStore pending_;
154
155         /** in_progress_ stores all forked processes so that we can proceed
156          *  thereafter.
157             The map uses the conversion commands as its identifiers.
158          */
159         typedef map<string, InProgress> InProgressMap;
160         ///
161         InProgressMap in_progress_;
162
163         ///
164         PreviewLoader & parent_;
165         ///
166         Buffer const & buffer_;
167         ///
168         double font_scaling_factor_;
169
170         /// We don't own this
171         static Converter const * pconverter_;   
172 };
173
174
175 Converter const * PreviewLoader::Impl::pconverter_;
176
177
178 // The public interface, defined in PreviewLoader.h
179 // ================================================
180 PreviewLoader::PreviewLoader(Buffer const & b)
181         : pimpl_(new Impl(*this, b))
182 {}
183
184
185 PreviewLoader::~PreviewLoader()
186 {}
187
188
189 PreviewImage const * PreviewLoader::preview(string const & latex_snippet) const
190 {
191         return pimpl_->preview(latex_snippet);
192 }
193
194
195 PreviewLoader::Status PreviewLoader::status(string const & latex_snippet) const
196 {
197         return pimpl_->status(latex_snippet);
198 }
199
200
201 void PreviewLoader::add(string const & latex_snippet)
202 {
203         pimpl_->add(latex_snippet);
204 }
205
206
207 void PreviewLoader::remove(string const & latex_snippet)
208 {
209         pimpl_->remove(latex_snippet);
210 }
211
212
213 void PreviewLoader::startLoading()
214 {
215         pimpl_->startLoading();
216 }
217
218 } // namespace grfx
219
220
221 // The details of the Impl
222 // =======================
223
224 namespace {
225
226 InProgress::InProgress(string const & filename_base,
227                        PendingStore const & pending,
228                        string const & to_format)
229         : pid(0),
230           metrics_file(filename_base + ".metrics"),
231           snippets(pending.size())
232 {
233         InProgressStore::iterator sit = snippets.begin();
234         PendingStore::const_iterator pit  = pending.begin();
235         PendingStore::const_iterator pend = pending.end();
236
237         int counter = 1; // file numbers start at 1
238         for (; pit != pend; ++pit, ++sit, ++counter) {
239                 ostringstream os;
240                 os << filename_base
241                    << setfill('0') << setw(3) << counter
242                    << "." << to_format;
243                 string const file = os.str().c_str();
244
245                 *sit = make_pair(*pit, file);
246         }
247 }
248
249
250 void InProgress::stop() const
251 {
252         if (pid)
253                 ForkedcallsController::get().kill(pid, 0);
254
255         if (!metrics_file.empty())
256                 lyx::unlink(metrics_file);
257
258         InProgressStore::const_iterator vit  = snippets.begin();
259         InProgressStore::const_iterator vend = snippets.end();
260         for (; vit != vend; ++vit) {
261                 if (!vit->second.empty())
262                         lyx::unlink(vit->second);
263         }
264 }
265  
266 } // namespace anon
267
268
269 namespace grfx {
270
271 PreviewLoader::Impl::Impl(PreviewLoader & p, Buffer const & b)
272         : parent_(p), buffer_(b), font_scaling_factor_(0.0)
273 {
274         font_scaling_factor_ = setFontScalingFactor(const_cast<Buffer &>(b));
275
276         lyxerr[Debug::GRAPHICS] << "The font scaling factor is "
277                                 << font_scaling_factor_ << endl;
278
279         if (!pconverter_)
280                 pconverter_ = setConverter();
281 }
282
283
284 PreviewLoader::Impl::~Impl()
285 {
286         InProgressMap::iterator ipit  = in_progress_.begin();
287         InProgressMap::iterator ipend = in_progress_.end();
288
289         for (; ipit != ipend; ++ipit) {
290                 ipit->second.stop();
291         }
292 }
293
294
295 PreviewImage const *
296 PreviewLoader::Impl::preview(string const & latex_snippet) const
297 {
298         Cache::const_iterator it = cache_.find(latex_snippet);
299         return (it == cache_.end()) ? 0 : it->second.get();
300 }
301
302
303 PreviewLoader::Status
304 PreviewLoader::Impl::status(string const & latex_snippet) const
305 {
306         Cache::const_iterator cit = cache_.find(latex_snippet);
307         if (cit != cache_.end())
308                 return Ready;
309
310         PendingStore::const_iterator pit  = pending_.begin();
311         PendingStore::const_iterator pend = pending_.end();
312         pit = find(pit, pend, latex_snippet);
313
314         if (pit != pend)
315                 return InQueue;
316
317         InProgressMap::const_iterator ipit  = in_progress_.begin();
318         InProgressMap::const_iterator ipend = in_progress_.end();
319
320         for (; ipit != ipend; ++ipit) {
321                 InProgressStore const & snippets = ipit->second.snippets;
322                 InProgressStore::const_iterator sit  = snippets.begin();
323                 InProgressStore::const_iterator send = snippets.end();
324                 sit = find_if(sit, send, FindFirst(latex_snippet));
325
326                 if (sit != send)
327                         return Processing;
328         }
329
330         return NotFound;
331 }
332
333
334 void PreviewLoader::Impl::add(string const & latex_snippet)
335 {
336         if (!pconverter_ || status(latex_snippet) != NotFound)
337                 return;
338
339         pending_.push_back(latex_snippet);
340 }
341
342
343 void PreviewLoader::Impl::remove(string const & latex_snippet)
344 {
345         Cache::iterator cit = cache_.find(latex_snippet);
346         if (cit != cache_.end())
347                 cache_.erase(cit);
348
349         PendingStore::iterator pit  = pending_.begin();
350         PendingStore::iterator pend = pending_.end();
351         pit = find(pit, pend, latex_snippet);
352
353         if (pit != pend) {
354                 PendingStore::iterator first = pit++;
355                 pending_.erase(first, pit);
356         }
357
358         InProgressMap::iterator ipit  = in_progress_.begin();
359         InProgressMap::iterator ipend = in_progress_.end();
360
361         while (ipit != ipend) {
362                 InProgressMap::iterator curr = ipit;
363                 ++ipit;
364
365                 InProgressStore & snippets = curr->second.snippets;
366                 InProgressStore::iterator sit  = snippets.begin();
367                 InProgressStore::iterator send = snippets.end();
368                 sit = find_if(sit, send, FindFirst(latex_snippet));
369
370                 if (sit != send)
371                         snippets.erase(sit, sit+1);
372
373                 if (snippets.empty())
374                         in_progress_.erase(curr);
375         }
376 }
377
378
379 void PreviewLoader::Impl::startLoading()
380 {
381         if (pending_.empty() || !pconverter_)
382                 return;
383
384         lyxerr[Debug::GRAPHICS] << "PreviewLoader::startLoading()" << endl;
385
386         // As used by the LaTeX file and by the resulting image files
387         string const filename_base(unique_filename(buffer_.tmppath));
388
389         // Create an InProgress instance to place in the map of all
390         // such processes if it starts correctly.
391         InProgress inprogress(filename_base, pending_, pconverter_->to);
392
393         // clear pending_, so we're ready to start afresh.
394         pending_.clear();
395
396         // Output the LaTeX file.
397         string const latexfile = filename_base + ".tex";
398
399         ofstream of(latexfile.c_str());
400         of << "\\batchmode\n";
401         dumpPreamble(of);
402         of << "\n\\begin{document}\n";
403         dumpData(of, inprogress.snippets);
404         of << "\n\\end{document}\n";
405         of.close();
406
407         // The conversion command.
408         ostringstream cs;
409         cs << pconverter_->command << " " << latexfile << " "
410            << font_scaling_factor_;
411
412         string const command = cs.str().c_str();
413
414         // Initiate the conversion from LaTeX to bitmap images files.
415         Forkedcall::SignalTypePtr convert_ptr;
416         convert_ptr.reset(new Forkedcall::SignalType);
417
418         convert_ptr->connect(
419                 boost::bind(&Impl::finishedGenerating, this, _1, _2, _3));
420
421         Forkedcall call;
422         int ret = call.startscript(command, convert_ptr);
423
424         if (ret != 0) {
425                 lyxerr[Debug::GRAPHICS] << "PreviewLoader::startLoading()\n"
426                                         << "Unable to start process \n"
427                                         << command << endl;
428                 return;
429         }
430
431         // Store the generation process in a list of all such processes
432         inprogress.pid = call.pid();
433         in_progress_[command] = inprogress;
434 }
435
436
437 void PreviewLoader::Impl::finishedGenerating(string const & command,
438                                              pid_t /* pid */, int retval)
439 {
440         string const status = retval > 0 ? "failed" : "succeeded";
441         lyxerr[Debug::GRAPHICS] << "PreviewLoader::finishedInProgress("
442                                 << retval << "): processing " << status
443                                 << " for " << command << endl;
444         if (retval > 0)
445                 return;
446
447         InProgressMap::iterator git = in_progress_.find(command);
448         if (git == in_progress_.end()) {
449                 lyxerr << "PreviewLoader::finishedGenerating(): unable to find "
450                         "data for\n"
451                        << command << "!" << endl;
452                 return;
453         }
454
455         // Read the metrics file, if it exists
456         vector<double> ascent_fractions(git->second.snippets.size());
457         setAscentFractions(ascent_fractions, git->second.metrics_file);
458
459         // Add these newly generated bitmap files to the cache and
460         // start loading them into LyX.
461         InProgressStore::const_iterator it  = git->second.snippets.begin();
462         InProgressStore::const_iterator end = git->second.snippets.end();
463
464         int metrics_counter = 0;
465         for (; it != end; ++it, ++metrics_counter) {
466                 string const & snip = it->first;
467                 string const & file = it->second;
468                 double af = ascent_fractions[metrics_counter];
469
470                 PreviewImagePtr ptr(new PreviewImage(parent_, snip, file, af));
471                 cache_[snip] = ptr;
472
473                 ptr->startLoading();
474         }
475
476         // Remove the item from the list of still-executing processes.
477         in_progress_.erase(git);
478 }
479
480
481 void PreviewLoader::Impl::dumpPreamble(ostream & os) const
482 {
483         // Why on earth is Buffer::makeLaTeXFile a non-const method?
484         Buffer & tmp = const_cast<Buffer &>(buffer_);
485         // Dump the preamble only.
486         tmp.makeLaTeXFile(os, string(), true, false, true);
487
488         // Loop over the insets in the buffer and dump all the math-macros.
489         Buffer::inset_iterator it  = buffer_.inset_const_iterator_begin();
490         Buffer::inset_iterator end = buffer_.inset_const_iterator_end();
491
492         for (; it != end; ++it) {
493                 if ((*it)->lyxCode() == Inset::MATHMACRO_CODE) {
494                         (*it)->latex(&buffer_, os, true, true);
495                 }
496         }
497
498         // All equation lables appear as "(#)" + preview.sty's rendering of
499         // the label name
500         if (lyxrc.preview_hashed_labels)
501                 os << "\\renewcommand{\\theequation}{\\#}\n";
502
503         // Use the preview style file to ensure that each snippet appears on a
504         // fresh page.
505         os << "\n"
506            << "\\usepackage[active,delayed,dvips,tightpage,showlabels]{preview}\n"
507            << "\n";
508
509         // This piece of PostScript magic ensures that the foreground and
510         // background colors are the same as the LyX screen.
511         string fg = lyx_gui::hexname(LColor::preview);
512         if (fg.empty()) fg = "000000";
513
514         string bg = lyx_gui::hexname(LColor::background);
515         if (bg.empty()) bg = "ffffff";
516
517         os << "\\AtBeginDocument{\\AtBeginDvi{%\n"
518            << "\\special{!userdict begin/bop-hook{//bop-hook exec\n"
519            << "<" << fg << bg << ">{255 div}forall setrgbcolor\n"
520            << "clippath fill setrgbcolor}bind def end}}}\n";
521 }
522
523
524 void PreviewLoader::Impl::dumpData(ostream & os,
525                                    InProgressStore const & vec) const
526 {
527         if (vec.empty())
528                 return;
529
530         InProgressStore::const_iterator it  = vec.begin();
531         InProgressStore::const_iterator end = vec.end();
532
533         for (; it != end; ++it) {
534                 os << "\\begin{preview}\n"
535                    << it->first
536                    << "\n\\end{preview}\n\n";
537         }
538 }
539
540 } // namespace grfx
541
542
543 namespace {
544
545 string const unique_filename(string const bufferpath)
546 {
547         static int theCounter = 0;
548         string const filename = tostr(theCounter++) + "lyxpreview";
549         return AddName(bufferpath, filename);
550 }
551
552
553 Converter const * setConverter()
554 {
555         Converter const * converter = 0;
556
557         string const from = "lyxpreview";
558
559         Formats::FormatList::const_iterator it  = formats.begin();
560         Formats::FormatList::const_iterator end = formats.end();
561
562         for (; it != end; ++it) {
563                 string const to = it->name();
564                 if (from == to)
565                         continue;
566
567                 Converter const * ptr = converters.getConverter(from, to);
568                 if (ptr)
569                         return ptr;
570         }
571
572         static bool first = true;
573         if (first) {
574                 first = false;
575                 lyxerr << "PreviewLoader::startLoading()\n"
576                        << "No converter from \"lyxpreview\" format has been "
577                         "defined."
578                        << endl;
579         }
580         
581         return 0;
582 }
583
584
585 double setFontScalingFactor(Buffer & buffer)
586 {
587         double scale_factor = 0.01 * lyxrc.dpi * lyxrc.zoom *
588                 lyxrc.preview_scale_factor;
589
590         // Has the font size been set explicitly?
591         string const & fontsize = buffer.params.fontsize;
592         lyxerr[Debug::GRAPHICS] << "PreviewLoader::scaleToFitLyXView()\n"
593                                 << "font size is " << fontsize << endl;
594
595         if (isStrUnsignedInt(fontsize))
596                 return 10.0 * scale_factor / strToDbl(fontsize);
597
598         // No. We must extract it from the LaTeX class file.
599         LyXTextClass const & tclass = textclasslist[buffer.params.textclass];
600         string const textclass(tclass.latexname() + ".cls");
601         string const classfile(findtexfile(textclass, "cls"));
602
603         lyxerr[Debug::GRAPHICS] << "text class is " << textclass << '\n'
604                                 << "class file is " << classfile << endl;
605         
606         ifstream ifs(classfile.c_str());
607         if (!ifs.good()) {
608                 lyxerr[Debug::GRAPHICS] << "Unable to open class file!" << endl;
609                 return scale_factor;
610         }
611
612         string str;
613         double scaling = scale_factor;
614
615         while (ifs.good()) {
616                 getline(ifs, str);
617                 // To get the default font size, look for a line like
618                 // "\ExecuteOptions{letterpaper,10pt,oneside,onecolumn,final}"
619                 if (!prefixIs(frontStrip(str), "\\ExecuteOptions"))
620                         continue;
621
622                 // str contains just the options of \ExecuteOptions
623                 string const tmp = split(str, '{');
624                 split(tmp, str, '}');
625                 
626                 int count = 0;
627                 string tok = token(str, ',', count++);
628                 while (!isValidLength(tok) && !tok.empty())
629                         tok = token(str, ',', count++);
630
631                 if (!tok.empty()) {
632                         lyxerr[Debug::GRAPHICS]
633                                 << "Extracted default font size from "
634                                 "LaTeX class file successfully!" << endl;
635                         LyXLength fsize(tok);
636                         scaling *= 10.0 / fsize.value();
637                         break;
638                 }
639         }
640
641         return scaling;
642 }
643
644
645 void setAscentFractions(vector<double> & ascent_fractions,
646                         string const & metrics_file)
647 {
648         // If all else fails, then the images will have equal ascents and
649         // descents.
650         vector<double>::iterator it  = ascent_fractions.begin();
651         vector<double>::iterator end = ascent_fractions.end();
652         fill(it, end, 0.5);
653
654         ifstream ifs(metrics_file.c_str());
655         if (!ifs.good()) {
656                 lyxerr[Debug::GRAPHICS] << "setAscentFractions("
657                                         << metrics_file << ")\n"
658                                         << "Unable to open file!"
659                                         << endl;
660                 return;
661         }
662
663         for (; it != end; ++it) {
664                 string page;
665                 string page_id;
666                 int dummy;
667                 int ascent;
668                 int descent;
669
670                 ifs >> page >> page_id >> dummy >> dummy >> dummy >> dummy
671                     >> ascent >> descent >> dummy;
672
673                 if (!ifs.good() ||
674                     page != "%%Page" ||
675                     !isStrUnsignedInt(strip(page_id, ':'))) {
676                         lyxerr[Debug::GRAPHICS] << "setAscentFractions("
677                                                 << metrics_file << ")\n"
678                                                 << "Error reading file!"
679                                                 << endl;
680                         break;
681                 }
682                 
683                 *it = ascent / (ascent + descent);
684         }
685 }
686
687 } // namespace anon