File indexing completed on 2024-05-12 08:17:51

0001 /*  This file is part of the KDE libraries
0002     Copyright (C) 2001 Malte Starostik <malte@kde.org>
0003 
0004     Handling of EPS previews Copyright (C) 2003 Philipp Hullmann <phull@gmx.de>
0005 
0006     This library is free software; you can redistribute it and/or
0007     modify it under the terms of the GNU Library General Public
0008     License as published by the Free Software Foundation; either
0009     version 2 of the License, or (at your option) any later version.
0010 
0011     This library is distributed in the hope that it will be useful,
0012     but WITHOUT ANY WARRANTY; without even the implied warranty of
0013     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
0014     Library General Public License for more details.
0015 
0016     You should have received a copy of the GNU Library General Public License
0017     along with this library; see the file COPYING.LIB.  If not, write to
0018     the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
0019     Boston, MA 02110-1301, USA.
0020 */
0021 
0022 /*  This function gets a path of a DVI, EPS, PS or PDF file and
0023     produces a PNG-Thumbnail which is stored as a QImage
0024 
0025     The program works as follows
0026 
0027     1. Test if file is a DVI file
0028 
0029     2. Create a child process (1), in which the
0030        file is to be changed into a PNG
0031 
0032     3. Child-process (1) :
0033 
0034     4. If file is DVI continue with 6
0035 
0036     5. If file is no DVI continue with 9
0037 
0038     6. Create another child process (2), in which the DVI is
0039        turned into PS using dvips
0040 
0041     7. Parent process (2) :
0042        Turn the recently created PS file into a PNG file using gs
0043 
0044     8. continue with 10
0045 
0046     9. Turn the PS,PDF or EPS file into a PNG file using gs
0047 
0048     10. Parent process (1)
0049         store data in a QImage
0050 */
0051 
0052 #ifdef HAVE_CONFIG_H
0053 #include <config.h>
0054 #endif
0055 
0056 
0057 #include <assert.h>
0058 #include <ctype.h>
0059 #include <stdlib.h>
0060 #include <stdio.h>
0061 #include <unistd.h>
0062 #include <signal.h>
0063 #ifdef HAVE_SYS_SELECT_H
0064 #include <sys/select.h>
0065 #endif
0066 #include <sys/time.h>
0067 #include <sys/wait.h>
0068 #include <fcntl.h>
0069 #include <errno.h>
0070 
0071 #include <QColor>
0072 #include <QFile>
0073 #include <QImage>
0074 #include <QVector>
0075 
0076 
0077 #include "gscreator.h"
0078 #include "dscparse.h"
0079 
0080 #include <KPluginFactory>
0081 
0082 K_PLUGIN_CLASS_WITH_JSON(GSCreator, "gsthumbnail.json")
0083 
0084 // This PS snippet will be prepended to the actual file so that only
0085 // the first page is output.
0086 static const char *psprolog =
0087     "%!PS-Adobe-3.0\n"
0088     "/.showpage.orig /showpage load def\n"
0089     "/.showpage.firstonly {\n"
0090     "    .showpage.orig\n"
0091     "    quit\n"
0092     "} def\n"
0093     "/showpage { .showpage.firstonly } def\n";
0094 
0095 // This is the code recommended by Adobe tech note 5002 for including
0096 // EPS files.
0097 static const char *epsprolog =
0098     "%!PS-Adobe-3.0\n"
0099     "userdict begin /pagelevel save def /showpage { } def\n"
0100     "0 setgray 0 setlinecap 1 setlinewidth 0 setlinejoin 10 setmiterlimit\n"
0101     "[ ] 0 setdash newpath false setoverprint false setstrokeadjust\n";
0102 
0103 static const char * gsargs_ps[] = {
0104     "gs",
0105     "-sDEVICE=png16m",
0106     "-sOutputFile=-",
0107     "-dSAFER",
0108     "-dPARANOIDSAFER",
0109     "-dNOPAUSE",
0110     "-dFirstPage=1",
0111     "-dLastPage=1",
0112     "-q",
0113     "-",
0114     nullptr, // file name
0115     "-c",
0116     "showpage",
0117     "-c",
0118     "quit",
0119     nullptr
0120 };
0121 
0122 static const char * gsargs_eps[] = {
0123     "gs",
0124     "-sDEVICE=png16m",
0125     "-sOutputFile=-",
0126     "-dSAFER",
0127     "-dPARANOIDSAFER",
0128     "-dNOPAUSE",
0129     nullptr, // page size
0130     nullptr, // resolution
0131     "-q",
0132     "-",
0133     nullptr, // file name
0134     "-c",
0135     "pagelevel",
0136     "-c",
0137     "restore",
0138     "-c",
0139     "end",
0140     "-c",
0141     "showpage",
0142     "-c",
0143     "quit",
0144     nullptr
0145 };
0146 
0147 static const char *dvipsargs[] = {
0148     "dvips",
0149     "-n",
0150     "1",
0151     "-q",
0152     "-o",
0153     "-",
0154     nullptr, // file name
0155     nullptr
0156 };
0157 
0158 static bool correctDVI(const QString& filename);
0159 
0160 
0161 namespace {
0162     bool got_sig_term = false;
0163     void handle_sigterm( int ) {
0164         got_sig_term = true;
0165     }
0166 }
0167 
0168 GSCreator::GSCreator(QObject *parent, const QVariantList &args)
0169   : KIO::ThumbnailCreator(parent, args)
0170 {
0171 }
0172 
0173 KIO::ThumbnailResult GSCreator::create(const KIO::ThumbnailRequest &request)
0174 {
0175   const QString path = request.url().toLocalFile();
0176   const int width = request.targetSize().width();
0177   const int height = request.targetSize().height();
0178 // The code in the loop (when testing whether got_sig_term got set)
0179 // should read some variation of:
0180 //      parentJob()->wasKilled()
0181 //
0182 // Unfortunatelly, that's currently impossible without breaking BIC.
0183 // So we need to catch the signal ourselves.
0184 // Otherwise, on certain funny PS files (for example
0185 // http://www.tjhsst.edu/~Eedanaher/pslife/life.ps )
0186 // gs would run forever after we were dead.
0187 // #### Reconsider for KDE 4 ###
0188 // (24/12/03 - luis_pedro)
0189 //
0190   typedef void ( *sighandler_t )( int );
0191   // according to linux's "man signal" the above typedef is a gnu extension
0192   sighandler_t oldhandler = signal( SIGTERM, handle_sigterm );
0193 
0194   int input[2];
0195   int output[2];
0196   int dvipipe[2];
0197 
0198   QByteArray data(1024, '\0');
0199 
0200   bool ok = false;
0201 
0202   // Test if file is DVI
0203   bool no_dvi =!correctDVI(request.url().toLocalFile());
0204 
0205   if (pipe(input) == -1) {
0206     return KIO::ThumbnailResult::fail();
0207   }
0208   if (pipe(output) == -1) {
0209     close(input[0]);
0210     close(input[1]);
0211     return KIO::ThumbnailResult::fail();
0212   }
0213 
0214   KDSC dsc;
0215   endComments = false;
0216   dsc.setCommentHandler(this);
0217 
0218   if (no_dvi)
0219   {
0220     FILE* fp = fopen(QFile::encodeName(path), "r");
0221     if (fp == nullptr) return KIO::ThumbnailResult::fail();
0222 
0223     char buf[4096];
0224     int count;
0225     while ((count = fread(buf, sizeof(char), 4096, fp)) != 0
0226            && !endComments) {
0227       dsc.scanData(buf, count);
0228     }
0229     fclose(fp);
0230 
0231     if (dsc.pjl() || dsc.ctrld()) {
0232       // this file is a mess.
0233       return KIO::ThumbnailResult::fail();
0234     }
0235   }
0236 
0237   std::unique_ptr<KDSCBBOX> bbox = dsc.bbox();
0238 
0239   const bool is_encapsulated = no_dvi
0240     && (path.endsWith(QLatin1String(".eps"), Qt::CaseInsensitive)
0241         || path.endsWith(QLatin1String(".epsi"), Qt::CaseInsensitive))
0242     && bbox.get() != nullptr
0243     && (bbox->width() > 0)
0244     && (bbox->height() > 0)
0245     && (dsc.page_count() <= 1);
0246 
0247   char translation[64] = "";
0248   char pagesize[32] = "";
0249   char resopt[32] = "";
0250 
0251   if (is_encapsulated) {
0252     // GhostScript's rendering at the extremely low resolutions
0253     // required for thumbnails leaves something to be desired. To
0254     // get nicer images, we render to four times the required
0255     // resolution and let QImage scale the result.
0256     const int hres = (width * 72) / bbox->width();
0257     const int vres = (height * 72) / bbox->height();
0258     const int resolution = (hres > vres ? vres : hres) * 4;
0259     const int gswidth = ((bbox->urx() - bbox->llx()) * resolution) / 72;
0260     const int gsheight = ((bbox->ury() - bbox->lly()) * resolution) / 72;
0261 
0262     snprintf(pagesize, 31, "-g%ix%i", gswidth, gsheight);
0263     snprintf(resopt, 31, "-r%i", resolution);
0264     snprintf(translation, 63,
0265        " 0 %i sub 0 %i sub translate\n", bbox->llx(),
0266        bbox->lly());
0267   }
0268 
0269   const CDSC_PREVIEW_TYPE previewType =
0270     static_cast<CDSC_PREVIEW_TYPE>(dsc.preview());
0271 
0272   switch (previewType) {
0273   case CDSC_TIFF:
0274   case CDSC_WMF:
0275   case CDSC_PICT:
0276     // FIXME: these should take precedence, since they can hold
0277     // color previews, which EPSI can't (or can it?).
0278      break;
0279   case CDSC_EPSI:
0280     {
0281       const int xscale = bbox->width() / width;
0282       const int yscale = bbox->height() / height;
0283       const int scale = xscale < yscale ? xscale : yscale;
0284       if (scale == 0) break;
0285       if (auto result = getEPSIPreview(path,
0286                          dsc.beginpreview(),
0287                          dsc.endpreview(),
0288                          bbox->width() / scale,
0289                          bbox->height() / scale); result.isValid())
0290         return result;
0291       // If the preview extraction routine fails, gs is used to
0292       // create a thumbnail.
0293     }
0294     break;
0295   case CDSC_NOPREVIEW:
0296   default:
0297     // need to run ghostscript in these cases
0298     break;
0299   }
0300 
0301   pid_t pid = fork();
0302   if (pid == 0) {
0303     // Child process (1)
0304 
0305     //    close(STDERR_FILENO);
0306 
0307     // find first zero entry in gsargs and put the filename
0308     // or - (stdin) there, if DVI
0309     const char **gsargs = gsargs_ps;
0310     const char **arg = gsargs;
0311 
0312     if (no_dvi && is_encapsulated) {
0313       gsargs = gsargs_eps;
0314       arg = gsargs;
0315 
0316       // find first zero entry and put page size there
0317       while (*arg) ++arg;
0318       *arg = pagesize;
0319 
0320       // find second zero entry and put resolution there
0321       while (*arg) ++arg;
0322       *arg = resopt;
0323     }
0324 
0325     // find next zero entry and put the filename there
0326     QByteArray fname = QFile::encodeName( path );
0327     while (*arg)
0328       ++arg;
0329     if( no_dvi )
0330       *arg = fname.data();
0331     else
0332       *arg = "-";
0333 
0334     // find first zero entry in dvipsargs and put the filename there
0335     arg = dvipsargs;
0336     while (*arg)
0337       ++arg;
0338     *arg = fname.data();
0339 
0340     if( !no_dvi ){
0341       pipe(dvipipe);
0342       pid_t pid_two = fork();
0343       if( pid_two == 0 ){
0344     // Child process (2), reopen stdout on the pipe "dvipipe" and exec dvips
0345 
0346     close(input[0]);
0347     close(input[1]);
0348     close(output[0]);
0349     close(output[1]);
0350     close(dvipipe[0]);
0351 
0352     dup2( dvipipe[1], STDOUT_FILENO);
0353 
0354     execvp(dvipsargs[0], const_cast<char *const *>(dvipsargs));
0355     _exit(1);
0356       }
0357       else if(pid_two != -1){
0358     close(input[1]);
0359     close(output[0]);
0360     close(dvipipe[1]);
0361 
0362     dup2( dvipipe[0], STDIN_FILENO);
0363     dup2( output[1], STDOUT_FILENO);
0364 
0365     execvp(gsargs[0], const_cast<char *const *>(gsargs));
0366     _exit(1);
0367       }
0368       else{
0369     // fork() (2) failed, close these
0370     close(dvipipe[0]);
0371     close(dvipipe[1]);
0372       }
0373 
0374     }
0375     else if( no_dvi ){
0376       // Reopen stdin/stdout on the pipes and exec gs
0377       close(input[1]);
0378       close(output[0]);
0379 
0380       dup2(input[0], STDIN_FILENO);
0381       dup2(output[1], STDOUT_FILENO);
0382 
0383       execvp(gsargs[0], const_cast<char *const *>(gsargs));
0384       _exit(1);
0385     }
0386   }
0387   else if (pid != -1) {
0388     // Parent process, write first-page-only-hack (the hack is not
0389     // used if DVI) and read the png output
0390     close(input[0]);
0391     close(output[1]);
0392     const char *prolog;
0393     if (is_encapsulated)
0394       prolog = epsprolog;
0395     else
0396       prolog = psprolog;
0397     int count = write(input[1], prolog, strlen(prolog));
0398     if (is_encapsulated)
0399       write(input[1], translation, strlen(translation));
0400 
0401     close(input[1]);
0402     if (count == static_cast<int>(strlen(prolog))) {
0403       int offset = 0;
0404     while (!ok) {
0405       fd_set fds;
0406       FD_ZERO(&fds);
0407       FD_SET(output[0], &fds);
0408       struct timeval tv;
0409       tv.tv_sec = 20;
0410       tv.tv_usec = 0;
0411 
0412       got_sig_term = false;
0413       if (select(output[0] + 1, &fds, nullptr, nullptr, &tv) <= 0) {
0414             if ( ( errno == EINTR || errno == EAGAIN ) && !got_sig_term ) continue;
0415         break; // error, timeout or master wants us to quit (SIGTERM)
0416           }
0417       if (FD_ISSET(output[0], &fds)) {
0418         count = read(output[0], data.data() + offset, 1024);
0419         if (count == -1)
0420           break;
0421         else
0422           if (count) // prepare for next block
0423         {
0424           offset += count;
0425           data.resize(offset + 1024);
0426         }
0427           else // got all data
0428         {
0429           data.resize(offset);
0430           ok = true;
0431         }
0432       }
0433     }
0434     }
0435     if (!ok) // error or timeout, gs probably didn't exit yet
0436     {
0437       kill(pid, SIGTERM);
0438     }
0439 
0440     int status = 0;
0441     int ret;
0442     do {
0443       ret = waitpid(pid, &status, 0);
0444     } while (ret == -1 && errno == EINTR);
0445     if (ret != pid || (status != 0  && status != 256) )
0446       ok = false;
0447   }
0448   else {
0449     // fork() (1) failed, close these
0450     close(input[0]);
0451     close(input[1]);
0452     close(output[1]);
0453   }
0454   close(output[0]);
0455 
0456   QImage img;
0457   bool loaded = img.loadFromData( data );
0458 
0459   if (!loaded) {
0460     // Sometimes gs spits some warning messages before the actual image
0461     // try to skip them
0462     const QByteArray pngHeader = "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A";
0463     const int pngMarkerIndex = data.indexOf(pngHeader);
0464     if (pngMarkerIndex > 0) {
0465       data = data.mid(pngMarkerIndex);
0466       loaded = img.loadFromData( data );
0467     }
0468   }
0469 
0470   if ( got_sig_term &&
0471     oldhandler != SIG_ERR &&
0472     oldhandler != SIG_DFL &&
0473     oldhandler != SIG_IGN ) {
0474       oldhandler( SIGTERM ); // propagate the signal. Other things might rely on it
0475   }
0476   if ( oldhandler != SIG_ERR ) signal( SIGTERM, oldhandler );
0477 
0478   if (loaded) {
0479     return KIO::ThumbnailResult::pass(img);
0480   }
0481 
0482   return KIO::ThumbnailResult::fail();
0483 }
0484 
0485 void GSCreator::comment(Name name)
0486 {
0487     switch (name) {
0488     case EndPreview:
0489     case BeginProlog:
0490     case Page:
0491       endComments = true;
0492       break;
0493 
0494     default:
0495       break;
0496     }
0497 }
0498 
0499 // Quick function to check if the filename corresponds to a valid DVI
0500 // file. Returns true if <filename> is a DVI file, false otherwise.
0501 
0502 static bool correctDVI(const QString& filename)
0503 {
0504   QFile f(filename);
0505   if (!f.open(QIODevice::ReadOnly))
0506     return false;
0507 
0508   unsigned char test[4];
0509   if ( f.read( (char *)test,2)<2 || test[0] != 247 || test[1] != 2  )
0510     return false;
0511 
0512   int n = f.size();
0513   if ( n < 134 ) // Too short for a dvi file
0514     return false;
0515   f.seek( n-4 );
0516 
0517   unsigned char trailer[4] = { 0xdf,0xdf,0xdf,0xdf };
0518 
0519   if ( f.read( (char *)test, 4 )<4 || strncmp( (char *)test, (char*) trailer, 4 ) )
0520     return false;
0521   // We suppose now that the dvi file is complete and OK
0522   return true;
0523 }
0524 
0525 KIO::ThumbnailResult GSCreator::getEPSIPreview(const QString &path, long start, long
0526                    end, int imgwidth, int imgheight)
0527 {
0528   FILE *fp;
0529   fp = fopen(QFile::encodeName(path), "r");
0530   if (fp == nullptr) return KIO::ThumbnailResult::fail();
0531 
0532   const long previewsize = end - start + 1;
0533 
0534   char *buf = (char *) malloc(previewsize);
0535   fseek(fp, start, SEEK_SET);
0536   int count = fread(buf, sizeof(char), previewsize - 1, fp);
0537   fclose(fp);
0538   buf[previewsize - 1] = 0;
0539   if (count != previewsize - 1)
0540   {
0541     free(buf);
0542     return KIO::ThumbnailResult::fail();
0543   }
0544 
0545   QString previewstr = QString::fromLatin1(buf);
0546   free(buf);
0547 
0548   int offset = 0;
0549   while ((offset < previewsize) && !(previewstr[offset].isDigit())) offset++;
0550   int digits = 0;
0551   while ((offset + digits < previewsize) && previewstr[offset + digits].isDigit()) digits++;
0552   int width = previewstr.mid(offset, digits).toInt();
0553   offset += digits + 1;
0554   while ((offset < previewsize) && !(previewstr[offset].isDigit())) offset++;
0555   digits = 0;
0556   while ((offset + digits < previewsize) && previewstr[offset + digits].isDigit()) digits++;
0557   int height = previewstr.mid(offset, digits).toInt();
0558   offset += digits + 1;
0559   while ((offset < previewsize) && !(previewstr[offset].isDigit())) offset++;
0560   digits = 0;
0561   while ((offset + digits < previewsize) && previewstr[offset + digits].isDigit()) digits++;
0562   int depth = previewstr.mid(offset, digits).toInt();
0563 
0564   // skip over the rest of the BeginPreview comment
0565   while ((offset < previewsize) &&
0566          previewstr[offset] != QLatin1Char('\n') &&
0567      previewstr[offset] != QLatin1Char('\r')) offset++;
0568   while ((offset < previewsize) && previewstr[offset] != QLatin1Char('%')) offset++;
0569 
0570   unsigned int imagedepth;
0571   switch (depth) {
0572   case 1:
0573   case 2:
0574   case 4:
0575   case 8:
0576     imagedepth = 8;
0577     break;
0578   case 12: // valid, but not (yet) supported
0579   default: // illegal value
0580     return KIO::ThumbnailResult::fail();
0581   }
0582 
0583   unsigned int colors = (1U << depth);
0584   QImage img(width, height, QImage::Format_Indexed8);
0585   img.setColorCount(colors);
0586 
0587   if (imagedepth <= 8) {
0588     for (unsigned int gray = 0; gray < colors; gray++) {
0589       unsigned int grayvalue = (255U * (colors - 1 - gray)) /
0590     (colors - 1);
0591       img.setColor(gray, qRgb(grayvalue, grayvalue, grayvalue));
0592     }
0593   }
0594 
0595   const unsigned int bits_per_scan_line = width * depth;
0596   unsigned int bytes_per_scan_line = bits_per_scan_line / 8;
0597   if (bits_per_scan_line % 8) bytes_per_scan_line++;
0598   const unsigned int bindatabytes = height * bytes_per_scan_line;
0599   QVector<unsigned char> bindata(bindatabytes);
0600 
0601   for (unsigned int i = 0; i < bindatabytes; i++) {
0602     if (offset >= previewsize)
0603       return KIO::ThumbnailResult::fail();
0604 
0605     while (!isxdigit(previewstr[offset].toLatin1()) &&
0606        offset < previewsize)
0607       offset++;
0608 
0609     bool ok = false;
0610     bindata[i] = static_cast<unsigned char>(previewstr.mid(offset, 2).toUInt(&ok, 16));
0611     if (!ok)
0612       return KIO::ThumbnailResult::fail();
0613 
0614     offset += 2;
0615   }
0616 
0617   for (int scanline = 0; scanline < height; scanline++) {
0618     unsigned char *scanlineptr = img.scanLine(scanline);
0619 
0620     for (int pixelindex = 0; pixelindex < width; pixelindex++) {
0621       unsigned char pixelvalue = 0;
0622       const unsigned int bitoffset =
0623         scanline * bytes_per_scan_line * 8U + pixelindex * depth;
0624       for (int depthindex = 0; depthindex < depth;
0625            depthindex++) {
0626         const unsigned int byteindex = (bitoffset + depthindex) / 8U;
0627         const unsigned int bitindex =
0628           7 - ((bitoffset + depthindex) % 8U);
0629         const unsigned char bitvalue =
0630           (bindata[byteindex] & static_cast<unsigned char>(1U << bitindex)) >> bitindex;
0631         pixelvalue |= (bitvalue << depthindex);
0632       }
0633       scanlineptr[pixelindex] = pixelvalue;
0634     }
0635   }
0636 
0637   QImage outimg = img.convertToFormat(QImage::Format_RGB32).scaled(imgwidth, imgheight, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
0638 
0639   return !outimg.isNull() ? KIO::ThumbnailResult::pass(outimg) : KIO::ThumbnailResult::fail();
0640 }
0641 
0642 #include "gscreator.moc"