]> git.lyx.org Git - lyx.git/blob - src/insets/insetgraphics.C
The graphics inset now has:
[lyx.git] / src / insets / insetgraphics.C
1 /* This file is part of
2  * ====================================================== 
3  * 
4  *           LyX, The Document Processor
5  *       
6  *           Copyright 1995-2002 the LyX Team.
7  *           
8  * \author Baruch Even
9  * \author Herbert Voss <voss@lyx.org>
10  * ====================================================== */
11
12 /*
13 Known BUGS:
14     
15     * If the image is from the clipart, and the document is moved to another
16        directory, the user is screwed. Need a way to handle it.
17        This amounts to a problem of when to use relative or absolute file paths
18        We should probably use what the user asks to use... but when he chooses
19        by the file dialog we normally get an absolute path and this may not be 
20        what the user meant.
21        [Note that browseRelFile in helper_funcs.* provides a file name
22         which is relative if it is at reference path (here puffer path)
23         level or below, and an absolute path if the file name is not a
24         `natural' relative file name. In any case,
25             MakeAbsPath(filename, buf->filePath())
26         is guaranteed to provide the correct absolute path. This is what is
27         done know for include insets. Feel free to ask me -- JMarc
28         14/01/2002]
29         
30         * If we are trying to create a file in a read-only directory and there
31                 are graphics that need converting, the converting will fail because
32                 it is done in-place, into the same directory as the original image.
33                 This needs to be fixed in the src/converter.C file
34                 [ This is presumed to be fixed, needs testing.]
35
36         * We do not dither or resize the image in a WYSIWYM way, we load it at
37                 its original size and color, resizing is done in the final output,
38                 but not in the LyX window.
39
40 TODO Before initial production release:
41     * Replace insetfig everywhere
42         * Search for comments of the form
43             // INSET_GRAPHICS: remove this when InsetFig is thrown.
44           And act upon them. Make sure not to remove InsetFig code for the 
45                   1.2.0 release, only afterwards, after deployment shows InsetGraphics
46                   to be ok.
47         * What advanced features the users want to do?
48             Implement them in a non latex dependent way, but a logical way.
49             LyX should translate it to latex or any other fitting format.
50     * Add a way to roll the image file into the file format.
51     * When loading, if the image is not found in the expected place, try
52        to find it in the clipart, or in the same directory with the image.
53     * Keep a tab on the image file, if it changes, update the lyx view.
54         * The image choosing dialog could show thumbnails of the image formats
55           it knows of, thus selection based on the image instead of based on
56           filename.
57         * Add support for the 'picins' package.
58         * Add support for the 'picinpar' package.
59         * Improve support for 'subfigure' - Allow to set the various options
60                 that are possible.
61  */
62
63 /* NOTES:
64  * Fileformat:
65  * Current version is 1 (inset file format version), when changing it
66  * it should be changed in the Write() function when writing in one place
67  * and when reading one should change the version check and the error message.
68  * The filename is kept in  the lyx file in a relative way, so as to allow
69  * moving the document file and its images with no problem.
70  * 
71  *
72  * Conversions:
73  *   Postscript output means EPS figures.
74  *
75  *   PDF output is best done with PDF figures if it's a direct conversion
76  *   or PNG figures otherwise.
77  *      Image format
78  *      from        to
79  *      EPS         epstopdf
80  *      PS          ps2pdf
81  *      JPG/PNG     direct
82  *      PDF         direct
83  *      others      PNG
84  */
85
86 #include <config.h> 
87
88 #ifdef __GNUG__
89 #pragma implementation
90 #endif 
91
92 #include "insets/insetgraphics.h"
93 #include "insets/insetgraphicsParams.h"
94
95 #include "graphics/GraphicsCache.h"
96 #include "graphics/GraphicsImage.h"
97
98 #include "LyXView.h"
99 #include "buffer.h"
100 #include "BufferView.h"
101 #include "converter.h"
102 #include "Painter.h"
103 #include "lyxrc.h"
104 #include "font.h"    // For the lyxfont class.
105 #include "debug.h"
106 #include "gettext.h"
107 #include "LaTeXFeatures.h"
108
109 #include "frontends/Dialogs.h"
110 #include "frontends/Alert.h"
111 #include "frontends/controllers/helper_funcs.h" // getVectorFromString
112
113 #include "support/LAssert.h"
114 #include "support/filetools.h"
115 #include "support/lyxalgo.h" // lyx::count
116
117 #include <algorithm> // For the std::max
118
119 extern string system_tempdir;
120
121 using std::ostream;
122 using std::endl;
123
124 ///////////////////////////////////////////////////////////////////////////
125 int const VersionNumber = 1;
126 ///////////////////////////////////////////////////////////////////////////
127
128 namespace {
129
130 // This function is a utility function
131 // ... that should be with ChangeExtension ...
132 inline
133 string const RemoveExtension(string const & filename)
134 {
135         return ChangeExtension(filename, string());
136 }
137  
138 } // namespace anon
139
140
141 namespace {
142
143 string const unique_id()
144 {
145         static unsigned int seed = 1000;
146
147         ostringstream ost;
148         ost << "graph" << ++seed;
149
150         // Needed if we use lyxstring.
151         return ost.str().c_str();
152 }
153
154 } // namespace anon
155
156
157 InsetGraphics::InsetGraphics()
158         : cached_status_(grfx::ErrorUnknown), cache_filled_(false),
159           graphic_label(unique_id())
160 {}
161
162
163 InsetGraphics::InsetGraphics(InsetGraphics const & ig, bool same_id)
164         : cached_status_(grfx::ErrorUnknown), cache_filled_(false),
165           graphic_label(unique_id())
166 {
167         setParams(ig.params());
168         if (same_id)
169                 id_ = ig.id_;
170 }
171
172
173 InsetGraphics::~InsetGraphics()
174 {
175         cached_image_.reset(0);
176         grfx::GCache & gc = grfx::GCache::get();
177         gc.remove(*this);
178
179         // Emits the hide signal to the dialog connected (if any)
180         hideDialog();
181 }
182
183
184 string const InsetGraphics::statusMessage() const
185 {
186         string msg;
187
188         switch (cached_status_) {
189         case grfx::WaitingToLoad:
190                 msg = _("Waiting for draw request to start loading...");
191                 break;
192         case grfx::Loading:
193                 msg = _("Loading...");
194                 break;
195         case grfx::Converting:
196                 msg = _("Converting to loadable format...");
197                 break;
198         case grfx::ScalingEtc:
199                 msg = _("Loaded. Scaling etc...");
200                 break;
201         case grfx::ErrorNoFile:
202                 msg = _("No file found!");
203                 break;
204         case grfx::ErrorLoading:
205                 msg = _("Error loading file into memory");
206                 break;
207         case grfx::ErrorConverting:
208                 msg = _("Error converting to loadable format");
209                 break;
210         case grfx::ErrorScalingEtc:
211                 msg = _("Error scaling etc");
212                 break;
213         case grfx::ErrorUnknown:
214                 msg = _("No image associated with this inset is in the cache!");
215                 break;
216         case grfx::Loaded:
217                 msg = _("Loaded but not displaying");
218                 break;
219         }
220
221         return msg;
222 }
223
224
225 void InsetGraphics::setCache() const
226 {
227         if (cache_filled_)
228                 return;
229
230         grfx::GCache & gc = grfx::GCache::get();
231         cached_status_ = gc.status(*this);
232         cached_image_  = gc.image(*this);
233 }
234
235
236 bool InsetGraphics::drawImage() const
237 {
238         setCache();
239         Pixmap const pixmap =
240                 (cached_status_ == grfx::Loaded && cached_image_.get() != 0) ?
241                 cached_image_->getPixmap() : 0;
242
243         return pixmap != 0;
244 }
245
246         
247 int InsetGraphics::ascent(BufferView *, LyXFont const &) const
248 {
249         if (drawImage())
250                 return cached_image_->getHeight();
251         else
252                 return 50;
253 }
254
255
256 int InsetGraphics::descent(BufferView *, LyXFont const &) const
257 {
258         return 0;
259 }
260
261
262 int InsetGraphics::width(BufferView *, LyXFont const & font) const
263 {
264         if (drawImage())
265                 return cached_image_->getWidth();
266         else {
267                 int font_width = 0;
268
269                 LyXFont msgFont(font);
270                 msgFont.setFamily(LyXFont::SANS_FAMILY);
271
272                 string const justname = OnlyFilename (params().filename);
273                 if (!justname.empty()) {
274                         msgFont.setSize(LyXFont::SIZE_FOOTNOTE);
275                         font_width = lyxfont::width(justname, msgFont);
276                 }
277
278                 string const msg = statusMessage();
279                 if (!msg.empty()) {
280                         msgFont.setSize(LyXFont::SIZE_TINY);
281                         int const msg_width = lyxfont::width(msg, msgFont);
282                         font_width = std::max(font_width, msg_width);
283                 }
284                 
285                 return std::max(50, font_width + 15);
286         }
287 }
288
289
290 void InsetGraphics::draw(BufferView * bv, LyXFont const & font,
291                          int baseline, float & x, bool) const
292 {
293         int ldescent = descent(bv, font);
294         int lascent  = ascent(bv, font);
295         int lwidth   = width(bv, font);
296
297         // Make sure now that x is updated upon exit from this routine
298         int old_x = int(x);
299         x += lwidth;
300
301         // Initiate the loading of the graphics file
302         if (cached_status_ == grfx::WaitingToLoad) {
303                 grfx::GCache & gc = grfx::GCache::get();
304                 gc.startLoading(*this);
305         }
306
307         // This will draw the graphics. If the graphics has not been loaded yet,
308         // we draw just a rectangle.
309         Painter & paint = bv->painter();
310
311         if (drawImage()) {
312
313                 paint.image(old_x + 2, baseline - lascent,
314                             lwidth - 4, lascent + ldescent,
315                             *cached_image_.get());
316
317         } else {        
318
319                 paint.rectangle(old_x + 2, baseline - lascent,
320                                 lwidth - 4,
321                                 lascent + ldescent);
322
323                 // Print the file name.
324                 LyXFont msgFont(font);
325                 msgFont.setFamily(LyXFont::SANS_FAMILY);
326                 string const justname = OnlyFilename (params().filename);
327                 if (!justname.empty()) {
328                         msgFont.setSize(LyXFont::SIZE_FOOTNOTE);
329                         paint.text(old_x + 8, 
330                                    baseline - lyxfont::maxAscent(msgFont) - 4,
331                                    justname, msgFont);
332                 }
333
334                 // Print the message.
335                 string const msg = statusMessage();
336                 if (!msg.empty()) {
337                         msgFont.setSize(LyXFont::SIZE_TINY);
338                         paint.text(old_x + 8, baseline - 4, msg, msgFont);
339                 }
340         }
341
342         // Reset the cache, ready for the next draw request
343         cached_status_ = grfx::ErrorUnknown;
344         cached_image_.reset(0);
345         cache_filled_ = false;
346 }
347
348
349 // Update the inset after parameters changed (read from file or changed in
350 // dialog. The grfx::GCache makes the decisions about whether or not to draw
351 // (interogates lyxrc, ascertains whether file exists etc)
352 void InsetGraphics::updateInset() const
353 {
354         grfx::GCache & gc = grfx::GCache::get();
355         gc.update(*this);
356 }
357
358
359 void InsetGraphics::edit(BufferView *bv, int, int, unsigned int)
360 {
361         bv->owner()->getDialogs()->showGraphics(this);
362 }
363
364
365 void InsetGraphics::edit(BufferView * bv, bool)
366 {
367         edit(bv, 0, 0, 0);
368 }
369
370
371 Inset::EDITABLE InsetGraphics::editable() const
372 {
373         return IS_EDITABLE;
374 }
375
376
377 void InsetGraphics::write(Buffer const * buf, ostream & os) const
378 {
379         os << "Graphics FormatVersion " << VersionNumber << '\n';
380         params().Write(buf, os);
381 }
382
383
384 void InsetGraphics::read(Buffer const * buf, LyXLex & lex)
385 {
386         string const token = lex.getString();
387
388         if (token == "Graphics")
389                 readInsetGraphics(buf, lex);
390         else if (token == "Figure") // Compatibility reading of FigInset figures.
391                 readFigInset(buf, lex);
392         else
393                 lyxerr[Debug::GRAPHICS] << "Not a Graphics or Figure inset!\n";
394
395         updateInset();
396 }
397
398 void InsetGraphics::readInsetGraphics(Buffer const * buf, LyXLex & lex)
399 {
400         bool finished = false;
401
402         while (lex.isOK() && !finished) {
403                 lex.next();
404
405                 string const token = lex.getString();
406                 lyxerr[Debug::GRAPHICS] << "Token: '" << token << '\'' 
407                                     << std::endl;
408
409                 if (token.empty()) {
410                         continue;
411                 } else if (token == "\\end_inset") {
412                         finished = true;
413                 } else if (token == "FormatVersion") {
414                         lex.next();
415                         int version = lex.getInteger();
416                         if (version > VersionNumber)
417                                 lyxerr
418                                 << "This document was created with a newer Graphics widget"
419                                 ", You should use a newer version of LyX to read this"
420                                 " file."
421                                 << std::endl;
422                         // TODO: Possibly open up a dialog?
423                 }
424                 else {
425                         if (! params_.Read(buf, lex, token))
426                                 lyxerr << "Unknown token, " << token << ", skipping." 
427                                         << std::endl;
428                 }
429         }
430 }
431
432 // FormatVersion < 1.0  (LyX < 1.2)
433 void InsetGraphics::readFigInset(Buffer const * buf, LyXLex & lex)
434 {
435         std::vector<string> const oldUnits =
436                 getVectorFromString("pt,cm,in,p%,c%");
437         bool finished = false;
438         // set the display default      
439         if (lyxrc.display_graphics == "mono") 
440             params_.display = InsetGraphicsParams::MONOCHROME;
441         else if (lyxrc.display_graphics == "gray") 
442             params_.display = InsetGraphicsParams::GRAYSCALE;
443         else if (lyxrc.display_graphics == "color") 
444             params_.display = InsetGraphicsParams::COLOR;
445         else
446             params_.display = InsetGraphicsParams::NONE;
447         while (lex.isOK() && !finished) {
448                 lex.next();
449
450                 string const token = lex.getString();
451                 lyxerr[Debug::GRAPHICS] << "Token: " << token << endl;
452                 
453                 if (token.empty())
454                         continue;
455                 else if (token == "\\end_inset") {
456                         finished = true;
457                 } else if (token == "file") {
458                         if (lex.next()) {
459                                 string const name = lex.getString();
460                                 string const path = buf->filePath();
461                                 params_.filename = MakeAbsPath(name, path);
462                         }
463                 } else if (token == "extra") {
464                         if (lex.next());
465                         // kept for backwards compability. Delete in 0.13.x
466                 } else if (token == "subcaption") {
467                         if (lex.eatLine())
468                                 params_.subcaptionText = lex.getString();
469                         params_.subcaption = true;
470                 } else if (token == "label") {
471                         if (lex.next());
472                         // kept for backwards compability. Delete in 0.13.x
473                 } else if (token == "angle") {
474                         if (lex.next())
475                                 params_.rotate = true;
476                                 params_.rotateAngle = lex.getFloat();
477                 } else if (token == "size") {
478                         if (lex.next())
479                                 params_.lyxwidth = LyXLength(lex.getString()+"pt");
480                         if (lex.next())
481                                 params_.lyxheight = LyXLength(lex.getString()+"pt");
482                 } else if (token == "flags") {
483                         if (lex.next())
484                                 switch (lex.getInteger()) {
485                                 case 1: params_.display = InsetGraphicsParams::MONOCHROME; 
486                                     break;
487                                 case 2: params_.display = InsetGraphicsParams::GRAYSCALE; 
488                                     break;
489                                 case 3: params_.display = InsetGraphicsParams::COLOR; 
490                                     break;
491                                 }
492                 } else if (token == "subfigure") {
493                         params_.subcaption = true;
494                 } else if (token == "width") {
495                     if (lex.next()) {
496                         int i = lex.getInteger();
497                         if (lex.next()) {
498                             if (i == 5) {
499                                 params_.scale = lex.getInteger();
500                                 params_.size_type = InsetGraphicsParams::SCALE;
501                             } else {
502                                 params_.width = LyXLength(lex.getString()+oldUnits[i]);
503                                 params_.size_type = InsetGraphicsParams::WH;
504                             }
505                         }
506                     }
507                 } else if (token == "height") {
508                     if (lex.next()) {
509                         int i = lex.getInteger();
510                         if (lex.next()) {
511                             params_.height = LyXLength(lex.getString()+oldUnits[i]);
512                             params_.size_type = InsetGraphicsParams::WH;
513                         }
514                     }
515                 }
516         }
517 }
518
519 string const InsetGraphics::createLatexOptions() const
520 {
521         // Calculate the options part of the command, we must do it to a string
522         // stream since we might have a trailing comma that we would like to remove
523         // before writing it to the output stream.
524         ostringstream options;
525         if (!params().bb.empty())
526             options << "  bb=" << strip(params().bb) << ",\n";
527         if (params().draft)
528             options << "  draft,\n";
529         if (params().clip)
530             options << "  clip,\n";
531         if (params().size_type == InsetGraphicsParams::WH) {
532             if (!params().width.zero())
533                 options << "  width=" << params().width.asLatexString() << ",\n";
534             if (!params().height.zero())
535                 options << "  height=" << params().height.asLatexString() << ",\n";
536         } else if (params().size_type == InsetGraphicsParams::SCALE) {
537             if (params().scale > 0)
538                 options << "  scale=" << double(params().scale)/100.0 << ",\n";
539         }
540         if (params().keepAspectRatio)
541             options << "  keepaspectratio,\n";
542         // Make sure it's not very close to zero, a float can be effectively
543         // zero but not exactly zero.
544         if (!lyx::float_equal(params().rotateAngle, 0, 0.001) && params().rotate) {
545             options << "  angle=" << params().rotateAngle << ",\n";
546             if (!params().rotateOrigin.empty()) {
547                 options << "  origin=" << params().rotateOrigin[0];
548                 if (contains(params().rotateOrigin,"Top"))
549                     options << 't';
550                 else if (contains(params().rotateOrigin,"Bottom"))
551                     options << 'b';
552                 else if (contains(params().rotateOrigin,"Baseline"))
553                     options << 'B';
554                 options << ",\n";
555             }
556         }
557         if (!params().special.empty())
558             options << params().special << ",\n";
559         string opts = options.str().c_str();
560         return opts.substr(0,opts.size()-2);    // delete last ",\n"
561 }
562
563 namespace {
564 string findTargetFormat(string const & suffix)
565 {
566         // lyxrc.pdf_mode means:
567         // Are we creating a PDF or a PS file?
568         // (Should actually mean, are we using latex or pdflatex).      
569         lyxerr[Debug::GRAPHICS] << "decideOutput: lyxrc.pdf_mode = "
570                             << lyxrc.pdf_mode << std::endl;
571         if (lyxrc.pdf_mode) {
572                 if (contains(suffix,"ps") || suffix == "pdf")
573                         return "pdf";
574                 else if (suffix == "jpg")
575                         return suffix;
576                 else
577                         return "png";
578         }
579         // If it's postscript, we always do eps.
580         lyxerr[Debug::GRAPHICS] << "decideOutput: we have PostScript mode\n";
581         if (suffix != "ps")
582             return "eps";
583         else
584             return "ps";
585 }
586
587 } // Anon. namespace
588
589
590 string const InsetGraphics::prepareFile(Buffer const *buf) const
591 {
592         // do_convert = Do we need to convert the file?
593         // nice = Do we create a nice version?
594         //        This is used when exporting the latex file only.
595         // if (!do_convert)
596         //   return original filename
597         // if (!nice)
598         //   convert_place = temp directory
599         //   return new filename in temp directory
600         // else
601         //   convert_place = original file directory
602         //   return original filename without the extension
603         //
604         // if it's a zipped one, than let LaTeX do the rest!!!
605         string filename_  = params().filename;
606         bool const zipped = zippedFile(filename_);
607
608         if ((zipped && params().noUnzip) || buf->niceFile) {
609                 lyxerr[Debug::GRAPHICS] << "don't unzip file or export latex" 
610                                     << filename_ << endl;
611                 return filename_;
612         }
613
614         if (zipped)
615                 filename_ = unzipFile(filename_);
616
617         string const from = getExtFromContents(filename_);
618         string const to   = findTargetFormat(from);
619
620         if (from == to) {
621                 // No conversion needed!
622                 return filename_;
623         }
624
625         string const temp = AddName(buf->tmppath, filename_);
626         string const outfile_base = RemoveExtension(temp);
627
628         lyxerr[Debug::GRAPHICS] << "tempname = " << temp << "\n";
629         lyxerr[Debug::GRAPHICS] << "buf::tmppath = " << buf->tmppath << "\n";
630         lyxerr[Debug::GRAPHICS] << "filename_ = " << filename_ << "\n";
631         lyxerr[Debug::GRAPHICS] << "outfile_base = " << outfile_base << endl;
632
633         converters.convert(buf, filename_, outfile_base, from, to);
634         return outfile_base;
635 }
636
637
638 int InsetGraphics::latex(Buffer const *buf, ostream & os,
639                          bool /*fragile*/, bool/*fs*/) const
640 {
641         // If there is no file specified, just output a message about it in
642         // the latex output.
643         if (params().filename.empty()) {
644                 os  << "\\fbox{\\rule[-0.5in]{0pt}{1in}"
645                         << _("empty figure path") << "}\n";
646                 return 1; // One end of line marker added to the stream.
647         }
648         // These variables collect all the latex code that should be before and
649         // after the actual includegraphics command.
650         string before;
651         string after;
652         // Do we want subcaptions?
653         if (params().subcaption) {
654                 before += "\\subfigure[" + params().subcaptionText + "]{";
655                 after = '}';
656         }
657         // We never use the starred form, we use the "clip" option instead.
658         before += "\\includegraphics";
659         // Write the options if there are any.
660         string const opts = createLatexOptions();
661         if (!opts.empty()) {
662                 before += ("[%\n" + opts +']');
663         }
664         // Make the filename relative to the lyx file
665         // and remove the extension so the LaTeX will use whatever is
666         // appropriate (when there are several versions in different formats)
667         string const latex_str = before + '{' + prepareFile(buf) + '}' + after;
668         os << latex_str;
669
670         // Return how many newlines we issued.
671         int const newlines =
672                 int(lyx::count(latex_str.begin(), latex_str.end(),'\n') + 1);
673                 
674         // lyxerr << "includegraphics: " << newlines << " lines of text"
675         //        << endl; 
676         return newlines;
677 }
678
679
680 int InsetGraphics::ascii(Buffer const *, ostream & os, int) const
681 {
682         // No graphics in ascii output. Possible to use gifscii to convert
683         // images to ascii approximation.
684         // 1. Convert file to ascii using gifscii
685         // 2. Read ascii output file and add it to the output stream.
686         // at least we send the filename
687         os << '<' << _("Graphicfile:") << params().filename << ">\n";
688         return 0;
689 }
690
691
692 int InsetGraphics::linuxdoc(Buffer const *, ostream &) const
693 {
694         // No graphics in LinuxDoc output. Should check how/what to add.
695         return 0;
696 }
697
698
699 // For explanation on inserting graphics into DocBook checkout:
700 // http://linuxdoc.org/LDP/LDP-Author-Guide/inserting-pictures.html
701 // See also the docbook guide at http://www.docbook.org/
702 int InsetGraphics::docbook(Buffer const *, ostream & os) const
703 {
704         // In DocBook v5.0, the graphic tag will be eliminated from DocBook, will 
705         // need to switch to MediaObject. However, for now this is sufficient and 
706         // easier to use.
707         os << "<graphic fileref=\"&" << graphic_label << ";\">";
708         return 0;
709 }
710
711
712 void InsetGraphics::validate(LaTeXFeatures & features) const
713 {
714         // If we have no image, we should not require anything.
715         if (params().filename.empty())
716                 return ;
717
718         features.includeFile(graphic_label, RemoveExtension(params_.filename));
719
720         features.require("graphicx");
721
722         if (params().subcaption)
723                 features.require("subfigure");
724 }
725
726
727 bool InsetGraphics::setParams(InsetGraphicsParams const & p)
728 {
729         // If nothing is changed, just return and say so.
730         if (params() == p && !p.filename.empty()) {
731                 return false;
732         }
733
734         // Copy the new parameters.
735         params_ = p;
736
737         // Update the inset with the new parameters.
738         updateInset();
739
740         // We have changed data, report it.
741         return true;
742 }
743
744
745 InsetGraphicsParams const & InsetGraphics::params() const
746 {
747         return params_;
748 }
749
750
751 Inset * InsetGraphics::clone(Buffer const &, bool same_id) const
752 {
753         return new InsetGraphics(*this, same_id);
754 }