Coverage Report

Created: 2026-01-25 07:18

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/kdegraphics-thumbnailers/ps/gscreator.cpp
Line
Count
Source
1
/*  This file is part of the KDE libraries
2
    SPDX-FileCopyrightText: 2001 Malte Starostik <malte@kde.org>
3
4
    Handling of EPS previews SPDX-FileCopyrightText: 2003 Philipp Hullmann <phull@gmx.de>
5
6
    SPDX-License-Identifier: LGPL-2.0-or-later
7
*/
8
9
/*  This function gets a path of a DVI, EPS, PS or PDF file and
10
    produces a PNG-Thumbnail which is stored as a QImage
11
12
    The program works as follows
13
14
    1. Test if file is a DVI file
15
16
    2. Create a child process (1), in which the
17
       file is to be changed into a PNG
18
19
    3. Child-process (1) :
20
21
    4. If file is DVI continue with 6
22
23
    5. If file is no DVI continue with 9
24
25
    6. Create another child process (2), in which the DVI is
26
       turned into PS using dvips
27
28
    7. Parent process (2) :
29
       Turn the recently created PS file into a PNG file using gs
30
31
    8. continue with 10
32
33
    9. Turn the PS,PDF or EPS file into a PNG file using gs
34
35
    10. Parent process (1)
36
        store data in a QImage
37
*/
38
39
#ifdef HAVE_CONFIG_H
40
#include <config.h>
41
#endif
42
43
44
#include <assert.h>
45
#include <ctype.h>
46
#include <stdlib.h>
47
#include <stdio.h>
48
#include <unistd.h>
49
#include <signal.h>
50
#ifdef HAVE_SYS_SELECT_H
51
#include <sys/select.h>
52
#endif
53
#include <sys/time.h>
54
#include <sys/wait.h>
55
#include <fcntl.h>
56
#include <errno.h>
57
58
#include <QColor>
59
#include <QFile>
60
#include <QImage>
61
#include <QVector>
62
63
64
#include "gscreator.h"
65
#include "dscparse.h"
66
67
#include <KPluginFactory>
68
69
0
K_PLUGIN_CLASS_WITH_JSON(GSCreator, "gsthumbnail.json")
Unexecuted instantiation: gsthumbnail_factory::tr(char const*, char const*, int)
Unexecuted instantiation: gsthumbnail_factory::~gsthumbnail_factory()
70
0
71
0
// This PS snippet will be prepended to the actual file so that only
72
0
// the first page is output.
73
0
static const char *psprolog =
74
0
    "%!PS-Adobe-3.0\n"
75
0
    "/.showpage.orig /showpage load def\n"
76
0
    "/.showpage.firstonly {\n"
77
0
    "    .showpage.orig\n"
78
0
    "    quit\n"
79
0
    "} def\n"
80
0
    "/showpage { .showpage.firstonly } def\n";
81
0
82
0
// This is the code recommended by Adobe tech note 5002 for including
83
0
// EPS files.
84
0
static const char *epsprolog =
85
0
    "%!PS-Adobe-3.0\n"
86
0
    "userdict begin /pagelevel save def /showpage { } def\n"
87
0
    "0 setgray 0 setlinecap 1 setlinewidth 0 setlinejoin 10 setmiterlimit\n"
88
0
    "[ ] 0 setdash newpath false setoverprint false setstrokeadjust\n";
89
0
90
0
static const char * gsargs_ps[] = {
91
0
    "gs",
92
0
    "-sDEVICE=png16m",
93
0
    "-sOutputFile=-",
94
0
    "-dSAFER",
95
0
    "-dPARANOIDSAFER",
96
0
    "-dNOPAUSE",
97
0
    "-dFirstPage=1",
98
0
    "-dLastPage=1",
99
0
    "-q",
100
0
    "-",
101
0
    nullptr, // file name
102
0
    "-c",
103
0
    "showpage",
104
0
    "-c",
105
0
    "quit",
106
0
    nullptr
107
0
};
108
0
109
0
static const char * gsargs_eps[] = {
110
0
    "gs",
111
0
    "-sDEVICE=png16m",
112
0
    "-sOutputFile=-",
113
0
    "-dSAFER",
114
0
    "-dPARANOIDSAFER",
115
0
    "-dNOPAUSE",
116
0
    nullptr, // page size
117
0
    nullptr, // resolution
118
0
    "-q",
119
0
    "-",
120
0
    nullptr, // file name
121
0
    "-c",
122
0
    "pagelevel",
123
0
    "-c",
124
0
    "restore",
125
0
    "-c",
126
0
    "end",
127
0
    "-c",
128
0
    "showpage",
129
0
    "-c",
130
0
    "quit",
131
0
    nullptr
132
0
};
133
0
134
0
static const char *dvipsargs[] = {
135
0
    "dvips",
136
0
    "-n",
137
0
    "1",
138
0
    "-q",
139
0
    "-o",
140
0
    "-",
141
0
    nullptr, // file name
142
0
    nullptr
143
0
};
144
0
145
0
static bool correctDVI(const QString& filename);
146
0
147
0
148
0
namespace {
149
0
  bool got_sig_term = false;
150
0
  void handle_sigterm( int ) {
151
0
    got_sig_term = true;
152
0
  }
153
}
154
155
GSCreator::GSCreator(QObject *parent, const QVariantList &args)
156
5.38k
  : KIO::ThumbnailCreator(parent, args)
157
5.38k
{
158
5.38k
}
159
160
KIO::ThumbnailResult GSCreator::create(const KIO::ThumbnailRequest &request)
161
5.38k
{
162
5.38k
  const QString path = request.url().toLocalFile();
163
5.38k
  const int width = request.targetSize().width();
164
5.38k
  const int height = request.targetSize().height();
165
// The code in the loop (when testing whether got_sig_term got set)
166
// should read some variation of:
167
//    parentJob()->wasKilled()
168
//
169
// Unfortunatelly, that's currently impossible without breaking BIC.
170
// So we need to catch the signal ourselves.
171
// Otherwise, on certain funny PS files (for example
172
// http://www.tjhsst.edu/~Eedanaher/pslife/life.ps )
173
// gs would run forever after we were dead.
174
// #### Reconsider for KDE 4 ###
175
// (24/12/03 - luis_pedro)
176
//
177
5.38k
  typedef void ( *sighandler_t )( int );
178
  // according to linux's "man signal" the above typedef is a gnu extension
179
5.38k
  sighandler_t oldhandler = signal( SIGTERM, handle_sigterm );
180
181
5.38k
  int input[2];
182
5.38k
  int output[2];
183
5.38k
  int dvipipe[2];
184
185
5.38k
  QByteArray data(1024, '\0');
186
187
5.38k
  bool ok = false;
188
189
  // Test if file is DVI
190
5.38k
  bool no_dvi =!correctDVI(request.url().toLocalFile());
191
192
5.38k
  if (pipe(input) == -1) {
193
0
    return KIO::ThumbnailResult::fail();
194
0
  }
195
5.38k
  if (pipe(output) == -1) {
196
0
    close(input[0]);
197
0
    close(input[1]);
198
0
    return KIO::ThumbnailResult::fail();
199
0
  }
200
201
5.38k
  KDSC dsc;
202
5.38k
  endComments = false;
203
5.38k
  dsc.setCommentHandler(this);
204
205
5.38k
  if (no_dvi)
206
5.38k
  {
207
5.38k
    FILE* fp = fopen(QFile::encodeName(path), "r");
208
5.38k
    if (fp == nullptr) return KIO::ThumbnailResult::fail();
209
210
5.38k
    char buf[4096];
211
5.38k
    int count;
212
23.4k
    while ((count = fread(buf, sizeof(char), 4096, fp)) != 0
213
18.1k
           && !endComments) {
214
18.0k
      dsc.scanData(buf, count);
215
18.0k
    }
216
5.38k
    fclose(fp);
217
218
5.38k
    if (dsc.pjl() || dsc.ctrld()) {
219
      // this file is a mess.
220
209
      return KIO::ThumbnailResult::fail();
221
209
    }
222
5.38k
  }
223
224
5.17k
  std::unique_ptr<KDSCBBOX> bbox = dsc.bbox();
225
226
5.17k
  const bool is_encapsulated = no_dvi
227
5.17k
    && (path.endsWith(QLatin1String(".eps"), Qt::CaseInsensitive)
228
5.17k
        || path.endsWith(QLatin1String(".epsi"), Qt::CaseInsensitive))
229
0
    && bbox.get() != nullptr
230
0
    && (bbox->width() > 0)
231
0
    && (bbox->height() > 0)
232
0
    && (dsc.page_count() <= 1);
233
234
5.17k
  char translation[64] = "";
235
5.17k
  char pagesize[32] = "";
236
5.17k
  char resopt[32] = "";
237
238
5.17k
  if (is_encapsulated) {
239
    // GhostScript's rendering at the extremely low resolutions
240
    // required for thumbnails leaves something to be desired. To
241
    // get nicer images, we render to four times the required
242
    // resolution and let QImage scale the result.
243
0
    const int hres = (width * 72) / bbox->width();
244
0
    const int vres = (height * 72) / bbox->height();
245
0
    const int resolution = (hres > vres ? vres : hres) * 4;
246
0
    const int gswidth = ((bbox->urx() - bbox->llx()) * resolution) / 72;
247
0
    const int gsheight = ((bbox->ury() - bbox->lly()) * resolution) / 72;
248
249
0
    snprintf(pagesize, 31, "-g%ix%i", gswidth, gsheight);
250
0
    snprintf(resopt, 31, "-r%i", resolution);
251
0
    snprintf(translation, 63,
252
0
       " 0 %i sub 0 %i sub translate\n", bbox->llx(),
253
0
       bbox->lly());
254
0
  }
255
256
5.17k
  const CDSC_PREVIEW_TYPE previewType =
257
5.17k
    static_cast<CDSC_PREVIEW_TYPE>(dsc.preview());
258
259
5.17k
  switch (previewType) {
260
3
  case CDSC_TIFF:
261
219
  case CDSC_WMF:
262
219
  case CDSC_PICT:
263
    // FIXME: these should take precedence, since they can hold
264
    // color previews, which EPSI can't (or can it?).
265
219
     break;
266
0
  case CDSC_EPSI:
267
0
    {
268
0
      const int xscale = bbox->width() / width;
269
0
      const int yscale = bbox->height() / height;
270
0
      const int scale = xscale < yscale ? xscale : yscale;
271
0
      if (scale == 0) break;
272
0
      if (auto result = getEPSIPreview(path,
273
0
                         dsc.beginpreview(),
274
0
                         dsc.endpreview(),
275
0
                         bbox->width() / scale,
276
0
                         bbox->height() / scale); result.isValid())
277
0
        return result;
278
      // If the preview extraction routine fails, gs is used to
279
      // create a thumbnail.
280
0
    }
281
0
    break;
282
4.95k
  case CDSC_NOPREVIEW:
283
4.95k
  default:
284
    // need to run ghostscript in these cases
285
4.95k
    break;
286
5.17k
  }
287
288
5.17k
  pid_t pid = fork();
289
5.17k
  if (pid == 0) {
290
    // Child process (1)
291
292
    //    close(STDERR_FILENO);
293
294
    // find first zero entry in gsargs and put the filename
295
    // or - (stdin) there, if DVI
296
0
    const char **gsargs = gsargs_ps;
297
0
    const char **arg = gsargs;
298
299
0
    if (no_dvi && is_encapsulated) {
300
0
      gsargs = gsargs_eps;
301
0
      arg = gsargs;
302
303
      // find first zero entry and put page size there
304
0
      while (*arg) ++arg;
305
0
      *arg = pagesize;
306
307
      // find second zero entry and put resolution there
308
0
      while (*arg) ++arg;
309
0
      *arg = resopt;
310
0
    }
311
312
    // find next zero entry and put the filename there
313
0
    QByteArray fname = QFile::encodeName( path );
314
0
    while (*arg)
315
0
      ++arg;
316
0
    if( no_dvi )
317
0
      *arg = fname.data();
318
0
    else
319
0
      *arg = "-";
320
321
    // find first zero entry in dvipsargs and put the filename there
322
0
    arg = dvipsargs;
323
0
    while (*arg)
324
0
      ++arg;
325
0
    *arg = fname.data();
326
327
0
    if( !no_dvi ){
328
0
      pipe(dvipipe);
329
0
      pid_t pid_two = fork();
330
0
      if( pid_two == 0 ){
331
  // Child process (2), reopen stdout on the pipe "dvipipe" and exec dvips
332
333
0
  close(input[0]);
334
0
  close(input[1]);
335
0
  close(output[0]);
336
0
  close(output[1]);
337
0
  close(dvipipe[0]);
338
339
0
  dup2( dvipipe[1], STDOUT_FILENO);
340
341
0
  execvp(dvipsargs[0], const_cast<char *const *>(dvipsargs));
342
0
  _exit(1);
343
0
      }
344
0
      else if(pid_two != -1){
345
0
  close(input[1]);
346
0
  close(output[0]);
347
0
  close(dvipipe[1]);
348
349
0
  dup2( dvipipe[0], STDIN_FILENO);
350
0
  dup2( output[1], STDOUT_FILENO);
351
352
0
  execvp(gsargs[0], const_cast<char *const *>(gsargs));
353
0
  _exit(1);
354
0
      }
355
0
      else{
356
  // fork() (2) failed, close these
357
0
  close(dvipipe[0]);
358
0
  close(dvipipe[1]);
359
0
      }
360
361
0
    }
362
0
    else if( no_dvi ){
363
      // Reopen stdin/stdout on the pipes and exec gs
364
0
      close(input[1]);
365
0
      close(output[0]);
366
367
0
      dup2(input[0], STDIN_FILENO);
368
0
      dup2(output[1], STDOUT_FILENO);
369
370
0
      execvp(gsargs[0], const_cast<char *const *>(gsargs));
371
0
      _exit(1);
372
0
    }
373
0
  }
374
5.17k
  else if (pid != -1) {
375
    // Parent process, write first-page-only-hack (the hack is not
376
    // used if DVI) and read the png output
377
5.17k
    close(input[0]);
378
5.17k
    close(output[1]);
379
5.17k
    const char *prolog;
380
5.17k
    if (is_encapsulated)
381
0
      prolog = epsprolog;
382
5.17k
    else
383
5.17k
      prolog = psprolog;
384
5.17k
    int count = write(input[1], prolog, strlen(prolog));
385
5.17k
    if (is_encapsulated)
386
0
      write(input[1], translation, strlen(translation));
387
388
5.17k
    close(input[1]);
389
5.17k
    if (count == static_cast<int>(strlen(prolog))) {
390
5.17k
      int offset = 0;
391
10.3k
  while (!ok) {
392
5.17k
    fd_set fds;
393
5.17k
    FD_ZERO(&fds);
394
5.17k
    FD_SET(output[0], &fds);
395
5.17k
    struct timeval tv;
396
5.17k
    tv.tv_sec = 20;
397
5.17k
    tv.tv_usec = 0;
398
399
5.17k
    got_sig_term = false;
400
5.17k
    if (select(output[0] + 1, &fds, nullptr, nullptr, &tv) <= 0) {
401
0
            if ( ( errno == EINTR || errno == EAGAIN ) && !got_sig_term ) continue;
402
0
      break; // error, timeout or master wants us to quit (SIGTERM)
403
0
          }
404
5.17k
    if (FD_ISSET(output[0], &fds)) {
405
5.17k
      count = read(output[0], data.data() + offset, 1024);
406
5.17k
      if (count == -1)
407
0
        break;
408
5.17k
      else
409
5.17k
        if (count) // prepare for next block
410
0
    {
411
0
      offset += count;
412
0
      data.resize(offset + 1024);
413
0
    }
414
5.17k
        else // got all data
415
5.17k
    {
416
5.17k
      data.resize(offset);
417
5.17k
      ok = true;
418
5.17k
    }
419
5.17k
    }
420
5.17k
  }
421
5.17k
    }
422
5.17k
    if (!ok) // error or timeout, gs probably didn't exit yet
423
0
    {
424
0
      kill(pid, SIGTERM);
425
0
    }
426
427
5.17k
    int status = 0;
428
5.17k
    int ret;
429
5.17k
    do {
430
5.17k
      ret = waitpid(pid, &status, 0);
431
5.17k
    } while (ret == -1 && errno == EINTR);
432
5.17k
    if (ret != pid || (status != 0  && status != 256) )
433
0
      ok = false;
434
5.17k
  }
435
0
  else {
436
    // fork() (1) failed, close these
437
0
    close(input[0]);
438
0
    close(input[1]);
439
0
    close(output[1]);
440
0
  }
441
5.17k
  close(output[0]);
442
443
5.17k
  QImage img;
444
5.17k
  bool loaded = img.loadFromData( data );
445
446
5.17k
  if (!loaded) {
447
    // Sometimes gs spits some warning messages before the actual image
448
    // try to skip them
449
5.17k
    const QByteArray pngHeader = "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A";
450
5.17k
    const int pngMarkerIndex = data.indexOf(pngHeader);
451
5.17k
    if (pngMarkerIndex > 0) {
452
0
      data = data.mid(pngMarkerIndex);
453
0
      loaded = img.loadFromData( data );
454
0
    }
455
5.17k
  }
456
457
5.17k
  if ( got_sig_term &&
458
0
  oldhandler != SIG_ERR &&
459
0
  oldhandler != SIG_DFL &&
460
0
  oldhandler != SIG_IGN ) {
461
0
    oldhandler( SIGTERM ); // propagate the signal. Other things might rely on it
462
0
  }
463
5.17k
  if ( oldhandler != SIG_ERR ) signal( SIGTERM, oldhandler );
464
465
5.17k
  if (loaded) {
466
0
    return KIO::ThumbnailResult::pass(img);
467
0
  }
468
469
5.17k
  return KIO::ThumbnailResult::fail();
470
5.17k
}
471
472
void GSCreator::comment(Name name)
473
1.97M
{
474
1.97M
    switch (name) {
475
0
    case EndPreview:
476
1.24k
    case BeginProlog:
477
3.76k
    case Page:
478
3.76k
      endComments = true;
479
3.76k
      break;
480
481
1.97M
    default:
482
1.97M
      break;
483
1.97M
    }
484
1.97M
}
485
486
// Quick function to check if the filename corresponds to a valid DVI
487
// file. Returns true if <filename> is a DVI file, false otherwise.
488
489
static bool correctDVI(const QString& filename)
490
5.38k
{
491
5.38k
  QFile f(filename);
492
5.38k
  if (!f.open(QIODevice::ReadOnly))
493
0
    return false;
494
495
5.38k
  unsigned char test[4];
496
5.38k
  if ( f.read( (char *)test,2)<2 || test[0] != 247 || test[1] != 2  )
497
5.30k
    return false;
498
499
82
  int n = f.size();
500
82
  if ( n < 134 ) // Too short for a dvi file
501
12
    return false;
502
70
  f.seek( n-4 );
503
504
70
  unsigned char trailer[4] = { 0xdf,0xdf,0xdf,0xdf };
505
506
70
  if ( f.read( (char *)test, 4 )<4 || strncmp( (char *)test, (char*) trailer, 4 ) )
507
70
    return false;
508
  // We suppose now that the dvi file is complete and OK
509
0
  return true;
510
70
}
511
512
KIO::ThumbnailResult GSCreator::getEPSIPreview(const QString &path, long start, long
513
             end, int imgwidth, int imgheight)
514
0
{
515
0
  FILE *fp;
516
0
  fp = fopen(QFile::encodeName(path), "r");
517
0
  if (fp == nullptr) return KIO::ThumbnailResult::fail();
518
519
0
  const long previewsize = end - start + 1;
520
521
0
  char *buf = (char *) malloc(previewsize);
522
0
  fseek(fp, start, SEEK_SET);
523
0
  int count = fread(buf, sizeof(char), previewsize - 1, fp);
524
0
  fclose(fp);
525
0
  buf[previewsize - 1] = 0;
526
0
  if (count != previewsize - 1)
527
0
  {
528
0
    free(buf);
529
0
    return KIO::ThumbnailResult::fail();
530
0
  }
531
532
0
  QString previewstr = QString::fromLatin1(buf);
533
0
  free(buf);
534
535
0
  int offset = 0;
536
0
  while ((offset < previewsize) && !(previewstr[offset].isDigit())) offset++;
537
0
  int digits = 0;
538
0
  while ((offset + digits < previewsize) && previewstr[offset + digits].isDigit()) digits++;
539
0
  int width = previewstr.mid(offset, digits).toInt();
540
0
  offset += digits + 1;
541
0
  while ((offset < previewsize) && !(previewstr[offset].isDigit())) offset++;
542
0
  digits = 0;
543
0
  while ((offset + digits < previewsize) && previewstr[offset + digits].isDigit()) digits++;
544
0
  int height = previewstr.mid(offset, digits).toInt();
545
0
  offset += digits + 1;
546
0
  while ((offset < previewsize) && !(previewstr[offset].isDigit())) offset++;
547
0
  digits = 0;
548
0
  while ((offset + digits < previewsize) && previewstr[offset + digits].isDigit()) digits++;
549
0
  int depth = previewstr.mid(offset, digits).toInt();
550
551
  // skip over the rest of the BeginPreview comment
552
0
  while ((offset < previewsize) &&
553
0
         previewstr[offset] != QLatin1Char('\n') &&
554
0
     previewstr[offset] != QLatin1Char('\r')) offset++;
555
0
  while ((offset < previewsize) && previewstr[offset] != QLatin1Char('%')) offset++;
556
557
0
  unsigned int imagedepth;
558
0
  switch (depth) {
559
0
  case 1:
560
0
  case 2:
561
0
  case 4:
562
0
  case 8:
563
0
    imagedepth = 8;
564
0
    break;
565
0
  case 12: // valid, but not (yet) supported
566
0
  default: // illegal value
567
0
    return KIO::ThumbnailResult::fail();
568
0
  }
569
570
0
  unsigned int colors = (1U << depth);
571
0
  QImage img(width, height, QImage::Format_Indexed8);
572
0
  img.setColorCount(colors);
573
574
0
  if (imagedepth <= 8) {
575
0
    for (unsigned int gray = 0; gray < colors; gray++) {
576
0
      unsigned int grayvalue = (255U * (colors - 1 - gray)) /
577
0
  (colors - 1);
578
0
      img.setColor(gray, qRgb(grayvalue, grayvalue, grayvalue));
579
0
    }
580
0
  }
581
582
0
  const unsigned int bits_per_scan_line = width * depth;
583
0
  unsigned int bytes_per_scan_line = bits_per_scan_line / 8;
584
0
  if (bits_per_scan_line % 8) bytes_per_scan_line++;
585
0
  const unsigned int bindatabytes = height * bytes_per_scan_line;
586
0
  QVector<unsigned char> bindata(bindatabytes);
587
588
0
  for (unsigned int i = 0; i < bindatabytes; i++) {
589
0
    if (offset >= previewsize)
590
0
      return KIO::ThumbnailResult::fail();
591
592
0
    while (!isxdigit(previewstr[offset].toLatin1()) &&
593
0
     offset < previewsize)
594
0
      offset++;
595
596
0
    bool ok = false;
597
0
    bindata[i] = static_cast<unsigned char>(previewstr.mid(offset, 2).toUInt(&ok, 16));
598
0
    if (!ok)
599
0
      return KIO::ThumbnailResult::fail();
600
601
0
    offset += 2;
602
0
  }
603
604
0
  for (int scanline = 0; scanline < height; scanline++) {
605
0
    unsigned char *scanlineptr = img.scanLine(scanline);
606
607
0
    for (int pixelindex = 0; pixelindex < width; pixelindex++) {
608
0
      unsigned char pixelvalue = 0;
609
0
      const unsigned int bitoffset =
610
0
        scanline * bytes_per_scan_line * 8U + pixelindex * depth;
611
0
      for (int depthindex = 0; depthindex < depth;
612
0
           depthindex++) {
613
0
        const unsigned int byteindex = (bitoffset + depthindex) / 8U;
614
0
        const unsigned int bitindex =
615
0
          7 - ((bitoffset + depthindex) % 8U);
616
0
        const unsigned char bitvalue =
617
0
          (bindata[byteindex] & static_cast<unsigned char>(1U << bitindex)) >> bitindex;
618
0
        pixelvalue |= (bitvalue << depthindex);
619
0
      }
620
0
      scanlineptr[pixelindex] = pixelvalue;
621
0
    }
622
0
  }
623
624
0
  QImage outimg = img.convertToFormat(QImage::Format_RGB32).scaled(imgwidth, imgheight, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
625
626
0
  return !outimg.isNull() ? KIO::ThumbnailResult::pass(outimg) : KIO::ThumbnailResult::fail();
627
0
}
628
629
#include "gscreator.moc"