]> git.lyx.org Git - lyx.git/blob - src/insets/insetgraphics.C
FormExternal patch from Rob,
[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 #include "graphics/GraphicsCache.h"
95 #include "graphics/GraphicsCacheItem.h"
96 #include "graphics/GraphicsImage.h"
97
98 #include "frontends/Dialogs.h"
99 #include "frontends/Alert.h"
100 #include "LyXView.h"
101 #include "buffer.h"
102 #include "BufferView.h"
103 #include "converter.h"
104 #include "Painter.h"
105 #include "lyx_gui_misc.h"
106 #include "support/FileInfo.h"
107 #include "support/filetools.h"
108 #include "frontends/controllers/helper_funcs.h"
109 #include "support/lyxlib.h"
110 #include "lyxtext.h"
111 #include "lyxrc.h"
112 #include "font.h" // For the lyxfont class.
113 #include "fstream" // for ifstream in isEPS
114 #include <algorithm> // For the std::max
115 #include "support/lyxmanip.h"
116 #include "debug.h"
117 #include "gettext.h"
118
119 extern string system_tempdir;
120
121 using std::ifstream;
122 using std::ostream;
123 using std::endl;
124
125 ///////////////////////////////////////////////////////////////////////////
126 int const VersionNumber = 1;
127 ///////////////////////////////////////////////////////////////////////////
128
129 // This function is a utility function
130 // ... that should be with ChangeExtension ...
131 inline
132 string const RemoveExtension(string const & filename)
133 {
134         return ChangeExtension(filename, string());
135 }
136
137
138 InsetGraphics::InsetGraphics()
139 {}
140
141
142 InsetGraphics::InsetGraphics(InsetGraphics const & ig, bool same_id)
143 {
144         setParams(ig.getParams());
145         if (same_id)
146                 id_ = ig.id_;
147 }
148
149
150 InsetGraphics::~InsetGraphics()
151 {
152         grfx::GCache & gc = grfx::GCache::get();
153         gc.remove(*this);
154
155         // Emits the hide signal to the dialog connected (if any)
156         hideDialog();
157 }
158
159
160 string const InsetGraphics::statusMessage() const
161 {
162         string msg;
163         grfx::GCache const & gc = grfx::GCache::get();
164         grfx::ImageStatus const status = gc.status(*this);
165
166         switch (status) {
167         case grfx::WaitingToLoad:
168                 msg = _("Waiting for draw request to start loading...");
169                 break;
170         case grfx::Loading:
171                 msg = _("Loading...");
172                 break;
173         case grfx::Converting:
174                 msg = _("Converting to loadable format...");
175                 break;
176         case grfx::ScalingEtc:
177                 msg = _("Loaded. Scaling etc...");
178                 break;
179         case grfx::ErrorLoading:
180                 msg = _("Error loading");
181                 break;
182         case grfx::ErrorConverting:
183                 msg = _("Error converting to loadable format");
184                 break;
185         case grfx::ErrorScalingEtc:
186                 msg = _("Error scaling etc");
187                 break;
188         case grfx::ErrorUnknown:
189                 msg = _("No image associated with this inset is in the cache!");
190                 break;
191         case grfx::Loaded:
192                 msg = _("Loaded");
193                 break;
194         }
195
196         return msg;
197 }
198
199
200 int InsetGraphics::ascent(BufferView *, LyXFont const &) const
201 {
202         grfx::GCache const & gc = grfx::GCache::get();
203         grfx::ImagePtr const image_ptr = gc.image(*this);
204
205         if (image_ptr.get() && image_ptr->getPixmap())
206                 return image_ptr->getHeight();
207         else
208                 return 50;
209 }
210
211
212 int InsetGraphics::descent(BufferView *, LyXFont const &) const
213 {
214         // this is not true if viewport is used and clip is not.
215         return 0;
216 }
217
218
219 int InsetGraphics::width(BufferView *, LyXFont const & font) const
220 {
221         grfx::GCache const & gc = grfx::GCache::get();
222         grfx::ImagePtr const image_ptr = gc.image(*this);
223
224         if (image_ptr.get() && image_ptr->getPixmap())
225                 return image_ptr->getWidth();
226         else {
227                 int font_width = 0;
228
229                 LyXFont msgFont(font);
230                 msgFont.setFamily(LyXFont::SANS_FAMILY);
231
232                 string const justname = OnlyFilename (params.filename);
233                 if (!justname.empty()) {
234                         msgFont.setSize(LyXFont::SIZE_FOOTNOTE);
235                         font_width = lyxfont::width(justname, msgFont);
236                 }
237
238                 string const msg = statusMessage();
239                 if (!msg.empty()) {
240                         msgFont.setSize(LyXFont::SIZE_TINY);
241                         int const msg_width = lyxfont::width(msg, msgFont);
242                         font_width = std::max(font_width, msg_width);
243                 }
244                 
245                 return std::max(50, font_width + 15);
246         }
247 }
248
249
250 void InsetGraphics::draw(BufferView * bv, LyXFont const & font,
251                          int baseline, float & x, bool) const
252 {
253         Painter & paint = bv->painter();
254         grfx::GCache & gc = grfx::GCache::get();
255         grfx::ImageStatus const status = gc.status(*this);
256
257         // Better to be paranoid and safe!
258         grfx::ImagePtr i_ptr = gc.image(*this);
259         Pixmap const pixmap = (status == grfx::Loaded && i_ptr.get()) ?
260                 i_ptr->getPixmap() : 0;
261
262         int ldescent = descent(bv, font);
263         int lascent  = ascent(bv, font);
264         int lwidth   = width(bv, font);
265
266         Pixmap const check_pixmap = (status == grfx::Loaded && i_ptr.get()) ?
267                 i_ptr->getPixmap() : 0;
268         
269         if (pixmap != check_pixmap) {
270                 // Tell BufferView we need to be updated!
271                 bv->text->status(bv, LyXText::CHANGED_IN_DRAW);
272         }
273
274         // Make sure x is updated upon exit from this routine
275         int old_x = int(x);
276         x += lwidth;
277
278         // Initiate the loading of the graphics file
279         if (status == grfx::WaitingToLoad) {
280                 gc.startLoading(*this);
281         }
282
283         // This will draw the graphics. If the graphics has not been loaded yet,
284         // we draw just a rectangle.
285
286         if (pixmap != 0) {
287
288                 paint.image(old_x + 2, baseline - lascent,
289                             lwidth - 4, lascent + ldescent, i_ptr);
290
291         } else {        
292
293                 paint.rectangle(old_x + 2, baseline - lascent,
294                                 lwidth - 4,
295                                 lascent + ldescent);
296
297                 // Print the file name.
298                 LyXFont msgFont(font);
299                 msgFont.setFamily(LyXFont::SANS_FAMILY);
300                 string const justname = OnlyFilename (params.filename);
301                 if (!justname.empty()) {
302                         msgFont.setSize(LyXFont::SIZE_FOOTNOTE);
303                         paint.text(old_x + 8, 
304                                    baseline - lyxfont::maxAscent(msgFont) - 4,
305                                    justname, msgFont);
306                 }
307
308                 // Print the message.
309                 string const msg = statusMessage();
310                 if (!msg.empty()) {
311                         msgFont.setSize(LyXFont::SIZE_TINY);
312                         paint.text(old_x + 8, baseline - 4, msg, msgFont);
313                 }
314         }
315 }
316
317
318 // Update the inset after parameters changed (read from file or changed in
319 // dialog. The grfx::GCache makes the decisions about whether or not to draw
320 // (interogates lyxrc, ascertains whether file exists etc)
321 void InsetGraphics::updateInset() const
322 {
323         grfx::GCache & gc = grfx::GCache::get();
324         gc.update(*this);
325 }
326
327
328 void InsetGraphics::edit(BufferView *bv, int, int, unsigned int)
329 {
330         bv->owner()->getDialogs()->showGraphics(this);
331 }
332
333
334 void InsetGraphics::edit(BufferView * bv, bool)
335 {
336         edit(bv, 0, 0, 0);
337 }
338
339
340 Inset::EDITABLE InsetGraphics::editable() const
341 {
342         return IS_EDITABLE;
343 }
344
345
346 void InsetGraphics::write(Buffer const * buf, ostream & os) const
347 {
348         os << "Graphics FormatVersion " << VersionNumber << '\n';
349         params.Write(buf, os);
350 }
351
352
353 void InsetGraphics::read(Buffer const * buf, LyXLex & lex)
354 {
355         string const token = lex.getString();
356
357         if (token == "Graphics")
358                 readInsetGraphics(buf, lex);
359         else if (token == "Figure") // Compatibility reading of FigInset figures.
360                 readFigInset(buf, lex);
361         else
362                 lyxerr[Debug::INFO] << "Not a Graphics or Figure inset!\n";
363
364         updateInset();
365 }
366
367 void InsetGraphics::readInsetGraphics(Buffer const * buf, LyXLex & lex)
368 {
369         bool finished = false;
370
371         while (lex.isOK() && !finished) {
372                 lex.next();
373
374                 string const token = lex.getString();
375                 lyxerr[Debug::INFO] << "Token: '" << token << '\'' 
376                                     << std::endl;
377
378                 if (token.empty()) {
379                         continue;
380                 } else if (token == "\\end_inset") {
381                         finished = true;
382                 } else if (token == "FormatVersion") {
383                         lex.next();
384                         int version = lex.getInteger();
385                         if (version > VersionNumber)
386                                 lyxerr
387                                 << "This document was created with a newer Graphics widget"
388                                 ", You should use a newer version of LyX to read this"
389                                 " file."
390                                 << std::endl;
391                         // TODO: Possibly open up a dialog?
392                 }
393                 else {
394                         if (! params.Read(buf, lex, token))
395                                 lyxerr << "Unknown token, " << token << ", skipping." 
396                                         << std::endl;
397                 }
398         }
399 }
400
401 // FormatVersion < 1.0  (LyX < 1.2)
402 void InsetGraphics::readFigInset(Buffer const * buf, LyXLex & lex)
403 {
404         std::vector<string> const oldUnits =
405                 getVectorFromString("pt,cm,in,p%,c%");
406         bool finished = false;
407         // set the display default      
408         if (lyxrc.display_graphics == "mono") 
409             params.display = InsetGraphicsParams::MONOCHROME;
410         else if (lyxrc.display_graphics == "gray") 
411             params.display = InsetGraphicsParams::GRAYSCALE;
412         else if (lyxrc.display_graphics == "color") 
413             params.display = InsetGraphicsParams::COLOR;
414         else
415             params.display = InsetGraphicsParams::NONE;
416         while (lex.isOK() && !finished) {
417                 lex.next();
418
419                 string const token = lex.getString();
420                 lyxerr[Debug::INFO] << "Token: " << token << endl;
421                 
422                 if (token.empty())
423                         continue;
424                 else if (token == "\\end_inset") {
425                         finished = true;
426                 } else if (token == "file") {
427                         if (lex.next()) {
428                                 string const name = lex.getString();
429                                 string const path = buf->filePath();
430                                 params.filename = MakeAbsPath(name, path);
431                         }
432                 } else if (token == "extra") {
433                         if (lex.next());
434                         // kept for backwards compability. Delete in 0.13.x
435                 } else if (token == "subcaption") {
436                         if (lex.eatLine())
437                                 params.subcaptionText = lex.getString();
438                         params.subcaption = true;
439                 } else if (token == "label") {
440                         if (lex.next());
441                         // kept for backwards compability. Delete in 0.13.x
442                 } else if (token == "angle") {
443                         if (lex.next())
444                                 params.rotate = true;
445                                 params.rotateAngle = lex.getFloat();
446                 } else if (token == "size") {
447                         if (lex.next())
448                                 params.lyxwidth = LyXLength(lex.getString()+"pt");
449                         if (lex.next())
450                                 params.lyxheight = LyXLength(lex.getString()+"pt");
451                 } else if (token == "flags") {
452                         if (lex.next())
453                                 switch (lex.getInteger()) {
454                                 case 1: params.display = InsetGraphicsParams::MONOCHROME; 
455                                     break;
456                                 case 2: params.display = InsetGraphicsParams::GRAYSCALE; 
457                                     break;
458                                 case 3: params.display = InsetGraphicsParams::COLOR; 
459                                     break;
460                                 }
461                 } else if (token == "subfigure") {
462                         params.subcaption = true;
463                 } else if (token == "width") {
464                     if (lex.next()) {
465                         int i = lex.getInteger();
466                         if (lex.next()) {
467                             if (i == 5) {
468                                 params.scale = lex.getInteger();
469                                 params.size_type = InsetGraphicsParams::SCALE;
470                             } else {
471                                 params.width = LyXLength(lex.getString()+oldUnits[i]);
472                                 params.size_type = InsetGraphicsParams::WH;
473                             }
474                         }
475                     }
476                 } else if (token == "height") {
477                     if (lex.next()) {
478                         int i = lex.getInteger();
479                         if (lex.next()) {
480                             params.height = LyXLength(lex.getString()+oldUnits[i]);
481                             params.size_type = InsetGraphicsParams::WH;
482                         }
483                     }
484                 }
485         }
486 }
487
488 string const InsetGraphics::createLatexOptions() const
489 {
490         // Calculate the options part of the command, we must do it to a string
491         // stream since we might have a trailing comma that we would like to remove
492         // before writing it to the output stream.
493         ostringstream options;
494         if (!params.bb.empty())
495             options << "  bb=" << strip(params.bb) << ",\n";
496         if (params.draft)
497             options << "  draft,\n";
498         if (params.clip)
499             options << "  clip,\n";
500         if (params.size_type == InsetGraphicsParams::WH) {
501             if (!params.width.zero())
502                 options << "  width=" << params.width.asLatexString() << ",\n";
503             if (!params.height.zero())
504                 options << "  height=" << params.height.asLatexString() << ",\n";
505         } else if (params.size_type == InsetGraphicsParams::SCALE) {
506             if (params.scale > 0)
507                 options << "  scale=" << double(params.scale)/100.0 << ",\n";
508         }
509         if (params.keepAspectRatio)
510             options << "  keepaspectratio,\n";
511         // Make sure it's not very close to zero, a float can be effectively
512         // zero but not exactly zero.
513         if (!lyx::float_equal(params.rotateAngle, 0, 0.001) && params.rotate) {
514             options << "  angle=" << params.rotateAngle << ",\n";
515             if (!params.rotateOrigin.empty()) {
516                 options << "  origin=" << params.rotateOrigin[0];
517                 if (contains(params.rotateOrigin,"Top"))
518                     options << 't';
519                 else if (contains(params.rotateOrigin,"Bottom"))
520                     options << 'b';
521                 else if (contains(params.rotateOrigin,"Baseline"))
522                     options << 'B';
523                 options << ",\n";
524             }
525         }
526         if (!params.special.empty())
527             options << params.special << ",\n";
528         string opts = options.str().c_str();
529         return opts.substr(0,opts.size()-2);    // delete last ",\n"
530 }
531
532 namespace {
533 string decideOutputImageFormat(string const & suffix)
534 {
535         // lyxrc.pdf_mode means:
536         // Are we creating a PDF or a PS file?
537         // (Should actually mean, are we using latex or pdflatex).      
538         lyxerr << "decideOutput::lyxrc.pdf_mode = " << lyxrc.pdf_mode << "\n";
539         if (lyxrc.pdf_mode) {
540                 if (contains(suffix,"ps") || suffix == "pdf")
541                         return "pdf";
542                 else if (suffix == "jpg")
543                         return suffix;
544                 else
545                         return "png";
546         }
547         // If it's postscript, we always do eps.
548         lyxerr << "decideOutput: we have PostScript mode\n";
549         if (suffix != "ps")
550             return "eps";
551         else
552             return "ps";
553 }
554
555 } // Anon. namespace
556
557 string const InsetGraphics::prepareFile(Buffer const *buf) const
558 {
559         // do_convert = Do we need to convert the file?
560         // nice = Do we create a nice version?
561         //        This is used when exporting the latex file only.
562         // if (!do_convert)
563         //   return original filename
564         // if (!nice)
565         //   convert_place = temp directory
566         //   return new filename in temp directory
567         // else
568         //   convert_place = original file directory
569         //   return original filename without the extension
570         //
571         // first check if file is viewed in LyX. First local
572         // than global
573         // if it's a zipped one, than let LaTeX do the rest!!!
574         if ((zippedFile(params.filename) && params.noUnzip) || buf->niceFile) {
575             lyxerr << "don't unzip file or export latex" 
576                     << params.filename << endl;
577             return params.filename;
578         }
579         string filename_ = params.filename;
580         if (zippedFile(filename_))
581             filename_ = unzipFile(filename_);
582         // now we have unzipped files
583         // Get the extension (format) of the original file.
584         // we handle it like a virtual one, so we can have
585         // different extensions with the same type.
586         string const extension = getExtFromContents(filename_);
587         // are we usind latex ((e)ps) or pdflatex (pdf,jpg,png)
588         string const image_target = decideOutputImageFormat(extension);
589         if (extension == image_target)          // :-)
590                 return filename_;
591         if (!IsFileReadable(filename_)) {       // :-(
592                 Alert::alert(_("File") + params.filename,
593                            _("isn't readable or doesn't exists!"));
594                 return filename_;
595         }
596         string outfile;
597         string const temp = AddName(buf->tmppath, filename_);
598         outfile = RemoveExtension(temp);
599         lyxerr << "tempname = " << temp << "\n";
600         lyxerr << "buf::tmppath = " << buf->tmppath << "\n";
601         lyxerr << "filename_ = " << filename_ << "\n";
602         lyxerr << "outfile = " << outfile << endl;
603         converters.convert(buf, filename_, outfile, extension, image_target);
604         return outfile;
605 }
606
607
608 int InsetGraphics::latex(Buffer const *buf, ostream & os,
609                          bool /*fragile*/, bool/*fs*/) const
610 {
611         // If there is no file specified, just output a message about it in
612         // the latex output.
613         if (params.filename.empty()) {
614                 os  << "\\fbox{\\rule[-0.5in]{0pt}{1in}"
615                         << _("empty figure path") << "}\n";
616                 return 1; // One end of line marker added to the stream.
617         }
618         // Keep count of newlines that we issued.
619 #warning the newlines=0 were in the original code. where is the sense? (Herbert)
620         int newlines = 0;
621         // This variables collect all the latex code that should be before and
622         // after the actual includegraphics command.
623         string before;
624         string after;
625         // Do we want subcaptions?
626         if (params.subcaption) {
627                 before += "\\subfigure[" + params.subcaptionText + "]{";
628                 after = '}' + after;
629         }
630         // We never use the starred form, we use the "clip" option instead.
631         os << before << "\\includegraphics";
632         // Write the options if there are any.
633         string const opts = createLatexOptions();
634         if (!opts.empty()) {
635                 os << "[%\n" << opts << ']';
636         }
637         // Make the filename relative to the lyx file
638         // and remove the extension so the LaTeX will use whatever is
639         // appropriate (when there are several versions in different formats)
640         string const filename = prepareFile(buf);
641         os << '{' << filename << '}' << after;
642         // Return how many newlines we issued.
643         return newlines;
644 }
645
646
647 int InsetGraphics::ascii(Buffer const *, ostream & os, int) const
648 {
649         // No graphics in ascii output. Possible to use gifscii to convert
650         // images to ascii approximation.
651         // 1. Convert file to ascii using gifscii
652         // 2. Read ascii output file and add it to the output stream.
653         // at least we send the filename
654         os << '<' << _("Graphicfile:") << params.filename << ">\n";
655         return 0;
656 }
657
658
659 int InsetGraphics::linuxdoc(Buffer const *, ostream &) const
660 {
661         // No graphics in LinuxDoc output. Should check how/what to add.
662         return 0;
663 }
664
665
666 // For explanation on inserting graphics into DocBook checkout:
667 // http://linuxdoc.org/LDP/LDP-Author-Guide/inserting-pictures.html
668 // See also the docbook guide at http://www.docbook.org/
669 int InsetGraphics::docbook(Buffer const * buf, ostream & os) const
670 {
671         // Change the path to be relative to the main file.
672         string const buffer_dir = buf->filePath();
673         string filename = RemoveExtension(
674                 MakeRelPath(params.filename, buffer_dir));
675
676         if (suffixIs(filename, ".eps"))
677                 filename.erase(filename.length() - 4);
678
679         // In DocBook v5.0, the graphic tag will be eliminated from DocBook, will 
680         // need to switch to MediaObject. However, for now this is sufficient and 
681         // easier to use.
682         os << "<graphic fileref=\"" << filename << "\"></graphic>";
683         return 0;
684 }
685
686
687 void InsetGraphics::validate(LaTeXFeatures & features) const
688 {
689         // If we have no image, we should not require anything.
690         if (params.filename.empty())
691                 return ;
692
693         features.require("graphicx");
694
695         if (params.subcaption)
696                 features.require("subfigure");
697 }
698
699
700 bool InsetGraphics::setParams(InsetGraphicsParams const & p)
701 {
702         // If nothing is changed, just return and say so.
703         if (params == p)
704                 return false;
705
706         // Copy the new parameters.
707         params = p;
708
709         // Update the inset with the new parameters.
710         updateInset();
711
712         // We have changed data, report it.
713         return true;
714 }
715
716
717 InsetGraphicsParams InsetGraphics::getParams() const
718 {
719         return params;
720 }
721
722
723 Inset * InsetGraphics::clone(Buffer const &, bool same_id) const
724 {
725         return new InsetGraphics(*this, same_id);
726 }
727