]> git.lyx.org Git - lyx.git/blob - src/graphics/PreviewLoader.C
Smart loading of image files for previews and for the graphics inset.
[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 <leeming@lyx.org>
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         // Paranoia check!
448         InProgressMap::iterator git = in_progress_.find(command);
449         if (git == in_progress_.end()) {
450                 lyxerr << "PreviewLoader::finishedGenerating(): unable to find "
451                         "data for\n"
452                        << command << "!" << endl;
453                 return;
454         }
455
456         // Read the metrics file, if it exists
457         vector<double> ascent_fractions(git->second.snippets.size());
458         setAscentFractions(ascent_fractions, git->second.metrics_file);
459
460         // Add these newly generated bitmap files to the cache and
461         // start loading them into LyX.
462         InProgressStore::const_iterator it  = git->second.snippets.begin();
463         InProgressStore::const_iterator end = git->second.snippets.end();
464
465         std::list<PreviewImagePtr> newimages;
466
467         int metrics_counter = 0;
468         for (; it != end; ++it, ++metrics_counter) {
469                 string const & snip = it->first;
470                 string const & file = it->second;
471                 double af = ascent_fractions[metrics_counter];
472
473                 PreviewImagePtr ptr(new PreviewImage(parent_, snip, file, af));
474                 cache_[snip] = ptr;
475
476                 newimages.push_back(ptr);
477         }
478
479         // Remove the item from the list of still-executing processes.
480         in_progress_.erase(git);
481
482         // Tell the outside world
483         std::list<PreviewImagePtr>::const_iterator nit  = newimages.begin();
484         std::list<PreviewImagePtr>::const_iterator nend = newimages.end();
485         for (; nit != nend; ++nit) {
486                 parent_.imageReady(*nit->get());
487         }
488 }
489
490
491 void PreviewLoader::Impl::dumpPreamble(ostream & os) const
492 {
493         // Why on earth is Buffer::makeLaTeXFile a non-const method?
494         Buffer & tmp = const_cast<Buffer &>(buffer_);
495         // Dump the preamble only.
496         tmp.makeLaTeXFile(os, string(), true, false, true);
497
498         // Loop over the insets in the buffer and dump all the math-macros.
499         Buffer::inset_iterator it  = buffer_.inset_const_iterator_begin();
500         Buffer::inset_iterator end = buffer_.inset_const_iterator_end();
501
502         for (; it != end; ++it) {
503                 if ((*it)->lyxCode() == Inset::MATHMACRO_CODE) {
504                         (*it)->latex(&buffer_, os, true, true);
505                 }
506         }
507
508         // All equation lables appear as "(#)" + preview.sty's rendering of
509         // the label name
510         if (lyxrc.preview_hashed_labels)
511                 os << "\\renewcommand{\\theequation}{\\#}\n";
512
513         // Use the preview style file to ensure that each snippet appears on a
514         // fresh page.
515         os << "\n"
516            << "\\usepackage[active,delayed,dvips,tightpage,showlabels]{preview}\n"
517            << "\n";
518
519         // This piece of PostScript magic ensures that the foreground and
520         // background colors are the same as the LyX screen.
521         string fg = lyx_gui::hexname(LColor::preview);
522         if (fg.empty()) fg = "000000";
523
524         string bg = lyx_gui::hexname(LColor::background);
525         if (bg.empty()) bg = "ffffff";
526
527         os << "\\AtBeginDocument{\\AtBeginDvi{%\n"
528            << "\\special{!userdict begin/bop-hook{//bop-hook exec\n"
529            << "<" << fg << bg << ">{255 div}forall setrgbcolor\n"
530            << "clippath fill setrgbcolor}bind def end}}}\n";
531 }
532
533
534 void PreviewLoader::Impl::dumpData(ostream & os,
535                                    InProgressStore const & vec) const
536 {
537         if (vec.empty())
538                 return;
539
540         InProgressStore::const_iterator it  = vec.begin();
541         InProgressStore::const_iterator end = vec.end();
542
543         for (; it != end; ++it) {
544                 os << "\\begin{preview}\n"
545                    << it->first
546                    << "\n\\end{preview}\n\n";
547         }
548 }
549
550 } // namespace grfx
551
552
553 namespace {
554
555 string const unique_filename(string const bufferpath)
556 {
557         static int theCounter = 0;
558         string const filename = tostr(theCounter++) + "lyxpreview";
559         return AddName(bufferpath, filename);
560 }
561
562
563 Converter const * setConverter()
564 {
565         Converter const * converter = 0;
566
567         string const from = "lyxpreview";
568
569         Formats::FormatList::const_iterator it  = formats.begin();
570         Formats::FormatList::const_iterator end = formats.end();
571
572         for (; it != end; ++it) {
573                 string const to = it->name();
574                 if (from == to)
575                         continue;
576
577                 Converter const * ptr = converters.getConverter(from, to);
578                 if (ptr)
579                         return ptr;
580         }
581
582         static bool first = true;
583         if (first) {
584                 first = false;
585                 lyxerr << "PreviewLoader::startLoading()\n"
586                        << "No converter from \"lyxpreview\" format has been "
587                         "defined."
588                        << endl;
589         }
590         
591         return 0;
592 }
593
594
595 double setFontScalingFactor(Buffer & buffer)
596 {
597         double scale_factor = 0.01 * lyxrc.dpi * lyxrc.zoom *
598                 lyxrc.preview_scale_factor;
599
600         // Has the font size been set explicitly?
601         string const & fontsize = buffer.params.fontsize;
602         lyxerr[Debug::GRAPHICS] << "PreviewLoader::scaleToFitLyXView()\n"
603                                 << "font size is " << fontsize << endl;
604
605         if (isStrUnsignedInt(fontsize))
606                 return 10.0 * scale_factor / strToDbl(fontsize);
607
608         // No. We must extract it from the LaTeX class file.
609         LyXTextClass const & tclass = textclasslist[buffer.params.textclass];
610         string const textclass(tclass.latexname() + ".cls");
611         string const classfile(findtexfile(textclass, "cls"));
612
613         lyxerr[Debug::GRAPHICS] << "text class is " << textclass << '\n'
614                                 << "class file is " << classfile << endl;
615         
616         ifstream ifs(classfile.c_str());
617         if (!ifs.good()) {
618                 lyxerr[Debug::GRAPHICS] << "Unable to open class file!" << endl;
619                 return scale_factor;
620         }
621
622         string str;
623         double scaling = scale_factor;
624
625         while (ifs.good()) {
626                 getline(ifs, str);
627                 // To get the default font size, look for a line like
628                 // "\ExecuteOptions{letterpaper,10pt,oneside,onecolumn,final}"
629                 if (!prefixIs(frontStrip(str), "\\ExecuteOptions"))
630                         continue;
631
632                 // str contains just the options of \ExecuteOptions
633                 string const tmp = split(str, '{');
634                 split(tmp, str, '}');
635                 
636                 int count = 0;
637                 string tok = token(str, ',', count++);
638                 while (!isValidLength(tok) && !tok.empty())
639                         tok = token(str, ',', count++);
640
641                 if (!tok.empty()) {
642                         lyxerr[Debug::GRAPHICS]
643                                 << "Extracted default font size from "
644                                 "LaTeX class file successfully!" << endl;
645                         LyXLength fsize(tok);
646                         scaling *= 10.0 / fsize.value();
647                         break;
648                 }
649         }
650
651         return scaling;
652 }
653
654
655 void setAscentFractions(vector<double> & ascent_fractions,
656                         string const & metrics_file)
657 {
658         // If all else fails, then the images will have equal ascents and
659         // descents.
660         vector<double>::iterator it  = ascent_fractions.begin();
661         vector<double>::iterator end = ascent_fractions.end();
662         fill(it, end, 0.5);
663
664         ifstream ifs(metrics_file.c_str());
665         if (!ifs.good()) {
666                 lyxerr[Debug::GRAPHICS] << "setAscentFractions("
667                                         << metrics_file << ")\n"
668                                         << "Unable to open file!"
669                                         << endl;
670                 return;
671         }
672
673         for (; it != end; ++it) {
674                 string page;
675                 string page_id;
676                 int dummy;
677                 double ascent;
678                 double descent;
679
680                 ifs >> page >> page_id >> dummy >> dummy >> dummy >> dummy
681                     >> ascent >> descent >> dummy;
682
683                 if (!ifs.good() ||
684                     page != "%%Page" ||
685                     !isStrUnsignedInt(strip(page_id, ':'))) {
686                         lyxerr[Debug::GRAPHICS] << "setAscentFractions("
687                                                 << metrics_file << ")\n"
688                                                 << "Error reading file!"
689                                                 << endl;
690                         break;
691                 }
692
693                 if (ascent + descent != 0)
694                         *it = ascent / (ascent + descent);
695         }
696 }
697
698 } // namespace anon