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