KoPictureEps.cpp
1/* This file is part of the KDE project
2 Copyright (c) 2001 Simon Hausmann <hausmann@kde.org>
3 Copyright (C) 2002, 2003, 2004 Nicolas GOUTTE <goutte@kde.org>
4
5 This library is free software; you can redistribute it and/or
6 modify it under the terms of the GNU Library General Public
7 License as published by the Free Software Foundation; either
8 version 2 of the License, or (at your option) any later version.
9
10 This library is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 Library General Public License for more details.
14
15 You should have received a copy of the GNU Library General Public License
16 along with this library; see the file COPYING.LIB. If not, write to
17 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
18 * Boston, MA 02110-1301, USA.
19*/
20
21#include <unistd.h>
22#include <stdio.h>
23
24#include <tqbuffer.h>
25#include <tqpainter.h>
26#include <tqpaintdevicemetrics.h>
27#include <tqfile.h>
28#include <tqtextstream.h>
29#include <tqregexp.h>
30#include <tqimage.h>
31#include <tqpixmap.h>
32#include <tqapplication.h>
33#include <tqdragobject.h>
34
35#include <tdeglobal.h>
36#include <kdebug.h>
37#include <tdeversion.h>
38#if ! KDE_IS_VERSION( 3,1,90 )
39#include <kdebugclasses.h>
40#endif
41#include <tdetempfile.h>
42#include <kprocess.h>
43
44#include "KoPictureKey.h"
45#include "KoPictureBase.h"
46#include "KoPictureEps.h"
47
48
49KoPictureEps::KoPictureEps(void) : m_psStreamStart(0), m_psStreamLength(0), m_cacheIsInFastMode(true)
50{
51 // Forbid TQPixmap to cache the X-Window resources (Yes, it is slower!)
52 m_cachedPixmap.setOptimization(TQPixmap::MemoryOptim);
53}
54
55KoPictureEps::~KoPictureEps(void)
56{
57}
58
59KoPictureBase* KoPictureEps::newCopy(void) const
60{
61 return new KoPictureEps(*this);
62}
63
64KoPictureType::Type KoPictureEps::getType(void) const
65{
66 return KoPictureType::TypeEps;
67}
68
69bool KoPictureEps::isNull(void) const
70{
71 return m_rawData.isNull();
72}
73
74TQImage KoPictureEps::scaleWithGhostScript(const TQSize& size, const int resolutionx, const int resolutiony )
75{
76 if (!m_boundingBox.width() || !m_boundingBox.height())
77 {
78 kdDebug(30003) << "EPS image has a null size! (in KoPictureEps::scaleWithGhostScript)" << endl;
79 return TQImage();
80 }
81
82 // ### TODO: do not call GhostScript up to three times for each re-scaling (one call of GhostScript should be enough to know which device is available: gs --help)
83 // png16m is better, but not always available -> fallback to bmp16m, then fallback to ppm (256 colors)
84 // ### TODO: pcx24b is also a true colour format
85 // ### TODO: support alpha (other gs devices needed)
86
87 const char* deviceTable[] = { "png16m", "bmp16m", "ppm", 0 };
88
89 TQImage img;
90
91 for ( int i = 0; deviceTable[i]; ++i)
92 {
93 if ( tryScaleWithGhostScript( img, size, resolutionx, resolutiony, deviceTable[i] ) != -1 )
94 {
95 return img;
96 }
97
98 }
99
100 kdError(30003) << "Image from GhostScript cannot be loaded (in KoPictureEps::scaleWithGhostScript)" << endl;
101 return img;
102}
103
104// Helper method for scaleWithGhostScript. Returns 1 on success, 0 on error, -1 if nothing generated
105// (in which case another 'output device' can be tried)
106int KoPictureEps::tryScaleWithGhostScript(TQImage &image, const TQSize& size, const int resolutionx, const int resolutiony, const char* device )
107// Based on the code of the file tdelibs/kimgio/eps.cpp
108{
109 kdDebug(30003) << "Sampling with GhostScript, using device \"" << device << "\" (in KoPictureEps::tryScaleWithGhostScript)" << endl;
110
111 KTempFile tmpFile;
112 tmpFile.setAutoDelete(true);
113
114 if ( tmpFile.status() )
115 {
116 kdError(30003) << "No KTempFile! (in KoPictureEps::tryScaleWithGhostScript)" << endl;
117 return 0; // error
118 }
119
120 const int wantedWidth = size.width();
121 const int wantedHeight = size.height();
122 const double xScale = double(size.width()) / double(m_boundingBox.width());
123 const double yScale = double(size.height()) / double(m_boundingBox.height());
124
125 // create GS command line
126
127 TQString cmdBuf ( "gs -sOutputFile=" );
128 cmdBuf += TDEProcess::quote(tmpFile.name());
129 cmdBuf += " -q -g";
130 cmdBuf += TQString::number( wantedWidth );
131 cmdBuf += "x";
132 cmdBuf += TQString::number( wantedHeight );
133
134 if ( ( resolutionx > 0) && ( resolutiony > 0) )
135 {
136#if 0
137 // Do not play with resolution for now.
138 // It brings more problems at print than solutions
139 cmdBuf += " -r";
140 cmdBuf += TQString::number( resolutionx );
141 cmdBuf += "x";
142 cmdBuf += TQString::number( resolutiony );
143#endif
144 }
145
146 cmdBuf += " -dSAFER -dPARANOIDSAFER -dNOPAUSE -sDEVICE=";
147 cmdBuf += device;
148 //cmdBuf += " -c 255 255 255 setrgbcolor fill 0 0 0 setrgbcolor";
149 cmdBuf += " -";
150 cmdBuf += " -c showpage quit";
151
152 // run ghostview
153
154 FILE* ghostfd = popen (TQFile::encodeName(cmdBuf), "w");
155
156 if ( ghostfd == 0 )
157 {
158 kdError(30003) << "No connection to GhostScript (in KoPictureEps::tryScaleWithGhostScript)" << endl;
159 return 0; // error
160 }
161
162 // The translation is needed as GhostScript (7.07) cannot handle negative values in the bounding box otherwise.
163 fprintf (ghostfd, "\n%d %d translate\n", -tqRound(m_boundingBox.left()*xScale), -tqRound(m_boundingBox.top()*yScale));
164 fprintf (ghostfd, "%g %g scale\n", xScale, yScale);
165
166 // write image to gs
167
168 fwrite( m_rawData.data() + m_psStreamStart, sizeof(char), m_psStreamLength, ghostfd);
169
170 pclose ( ghostfd );
171
172 // load image
173 if( !image.load (tmpFile.name()) )
174 {
175 // It failed - maybe the device isn't supported by gs
176 return -1;
177 }
178 if ( image.size() != size ) // this can happen due to rounding problems
179 {
180 //kdDebug(30003) << "fixing size to " << size.width() << "x" << size.height()
181 // << " (was " << image.width() << "x" << image.height() << ")" << endl;
182 image = image.scale( size ); // hmm, smoothScale instead?
183 }
184 kdDebug(30003) << "Image parameters: " << image.width() << "x" << image.height() << "x" << image.depth() << endl;
185 return 1; // success
186}
187
188void KoPictureEps::scaleAndCreatePixmap(const TQSize& size, bool fastMode, const int resolutionx, const int resolutiony )
189{
190 kdDebug(30003) << "KoPictureEps::scaleAndCreatePixmap " << size << " " << (fastMode?TQString("fast"):TQString("slow"))
191 << " resolutionx: " << resolutionx << " resolutiony: " << resolutiony << endl;
192 if ((size==m_cachedSize)
193 && ((fastMode) || (!m_cacheIsInFastMode)))
194 {
195 // The cached pixmap has already the right size
196 // and:
197 // - we are in fast mode (We do not care if the re-size was done slowly previously)
198 // - the re-size was already done in slow mode
199 kdDebug(30003) << "Already cached!" << endl;
200 return;
201 }
202
203 // Slow mode can be very slow, especially at high zoom levels -> configurable
204 if ( !isSlowResizeModeAllowed() )
205 {
206 kdDebug(30003) << "User has disallowed slow mode!" << endl;
207 fastMode = true;
208 }
209
210 // We cannot use fast mode, if nothing was ever cached.
211 if ( fastMode && !m_cachedSize.isEmpty())
212 {
213 kdDebug(30003) << "Fast scaling!" << endl;
214 // Slower than caching a TQImage, but faster than re-sampling!
215 TQImage image( m_cachedPixmap.convertToImage() );
216 m_cachedPixmap=image.scale( size );
217 m_cacheIsInFastMode=true;
218 m_cachedSize=size;
219 }
220 else
221 {
222 TQTime time;
223 time.start();
224
225 TQApplication::setOverrideCursor( TQt::waitCursor );
226 m_cachedPixmap = scaleWithGhostScript( size, resolutionx, resolutiony );
227 TQApplication::restoreOverrideCursor();
228 m_cacheIsInFastMode=false;
229 m_cachedSize=size;
230
231 kdDebug(30003) << "Time: " << (time.elapsed()/1000.0) << " s" << endl;
232 }
233 kdDebug(30003) << "New size: " << size << endl;
234}
235
236void KoPictureEps::draw(TQPainter& painter, int x, int y, int width, int height, int sx, int sy, int sw, int sh, bool fastMode)
237{
238 if ( !width || !height )
239 return;
240
241 TQSize screenSize( width, height );
242 //kdDebug() << "KoPictureEps::draw screenSize=" << screenSize.width() << "x" << screenSize.height() << endl;
243
244 TQPaintDeviceMetrics metrics (painter.device());
245 kdDebug(30003) << "Metrics: X: " << metrics.logicalDpiX() << " x Y: " << metrics.logicalDpiX() << " (in KoPictureEps::draw)" << endl;
246
247 if ( painter.device()->isExtDev() ) // Is it an external device (i.e. printer)
248 {
249 kdDebug(30003) << "Drawing for a printer (in KoPictureEps::draw)" << endl;
250 // For printing, always re-sample the image, as a printer has never the same resolution than a display.
251 TQImage image( scaleWithGhostScript( screenSize, metrics.logicalDpiX(), metrics.logicalDpiY() ) );
252 // sx,sy,sw,sh is meant to be used as a cliprect on the pixmap, but drawImage
253 // translates it to the (x,y) point -> we need (x+sx, y+sy).
254 painter.drawImage( x + sx, y + sy, image, sx, sy, sw, sh );
255 }
256 else // No, it is simply a display
257 {
258 scaleAndCreatePixmap(screenSize, fastMode, metrics.logicalDpiX(), metrics.logicalDpiY() );
259
260 // sx,sy,sw,sh is meant to be used as a cliprect on the pixmap, but drawPixmap
261 // translates it to the (x,y) point -> we need (x+sx, y+sy).
262 painter.drawPixmap( x + sx, y + sy, m_cachedPixmap, sx, sy, sw, sh );
263 }
264}
265
266bool KoPictureEps::extractPostScriptStream( void )
267{
268 kdDebug(30003) << "KoPictureEps::extractPostScriptStream" << endl;
269 TQDataStream data( m_rawData, IO_ReadOnly );
270 data.setByteOrder( TQDataStream::LittleEndian );
271 TQ_UINT32 magic, offset, length;
272 data >> magic;
273 data >> offset;
274 data >> length;
275 if ( !length )
276 {
277 kdError(30003) << "Length of PS stream is zero!" << endl;
278 return false;
279 }
280 if ( offset+length>m_rawData.size() )
281 {
282 kdError(30003) << "Data stream of the EPSF file is longer than file: " << offset << "+" << length << ">" << m_rawData.size() << endl;
283 return false;
284 }
285 m_psStreamStart = offset;
286 m_psStreamLength = length;
287 return true;
288}
289
290TQString KoPictureEps::readLine( const TQByteArray& array, const uint start, const uint length, uint& pos, bool& lastCharWasCr )
291{
292 TQString strLine;
293 const uint finish = kMin( start + length, (uint)array.size() );
294 for ( ; pos < finish; ++pos ) // We are starting at pos
295 {
296 const char ch = array[ pos ]; // Read one character
297 if ( ch == '\n' )
298 {
299 if ( lastCharWasCr )
300 {
301 // We have a line feed following a Carriage Return
302 // As the Carriage Return has already ended the previous line,
303 // discard this Line Feed.
304 lastCharWasCr = false;
305 }
306 else
307 {
308 // We have a normal Line Feed, therefore we end the line
309 break;
310 }
311 }
312 else if ( ch == '\r' )
313 {
314 // We have a Carriage Return, therefore we end the line
315 lastCharWasCr = true;
316 break;
317 }
318 else if ( ch == char(12) ) // Form Feed
319 { // ### TODO: can a FF happen in PostScript?
320 // Ignore the form feed
321 continue;
322 }
323 else
324 {
325 strLine += ch;
326 lastCharWasCr = false;
327 }
328 }
329 return strLine;
330}
331
332
333bool KoPictureEps::loadData(const TQByteArray& array, const TQString& /* extension */ )
334{
335
336 kdDebug(30003) << "KoPictureEps::load" << endl;
337 // First, read the raw data
338 m_rawData=array;
339
340 if (m_rawData.isNull())
341 {
342 kdError(30003) << "No data was loaded!" << endl;
343 return false;
344 }
345
346 if ( ( m_rawData[0]==char(0xc5) ) && ( m_rawData[1]==char(0xd0) )
347 && ( m_rawData[2]==char(0xd3) ) && ( m_rawData[3]==char(0xc6) ) )
348 {
349 // We have a so-called "MS-DOS EPS file", we have to extract the PostScript stream
350 if (!extractPostScriptStream()) // Changes m_rawData
351 return false;
352 }
353 else
354 {
355 m_psStreamStart = 0;
356 m_psStreamLength = m_rawData.size();
357 }
358
359 TQString lineBox; // Line with the bounding box
360 bool lastWasCr = false; // Was the last character of the line a carriage return?
361 uint pos = m_psStreamStart; // We start to search the bounding box at the start of the PostScript stream
362 TQString line( readLine( m_rawData, m_psStreamStart, m_psStreamLength, pos, lastWasCr ) );
363 kdDebug(30003) << "Header: " << line << endl;
364 if (!line.startsWith("%!"))
365 {
366 kdError(30003) << "Not a PostScript file!" << endl;
367 return false;
368 }
369 TQRect rect;
370 bool lineIsBoundingBox = false; // Does "line" has a %%BoundingBox line?
371 for(;;)
372 {
373 ++pos; // Get over the previous line end (CR or LF)
374 line = readLine( m_rawData, m_psStreamStart, m_psStreamLength, pos, lastWasCr );
375 kdDebug(30003) << "Checking line: " << line << endl;
376 // ### TODO: it seems that the bounding box can be delayed with "(atend)" in the trailer (GhostScript 7.07 does not support it either.)
377 if (line.startsWith("%%BoundingBox:"))
378 {
379 lineIsBoundingBox = true;
380 break;
381 }
382 // ### TODO: also abort on %%EndComments
383 // ### TODO: %? , where ? is non-white-space printable, does not end the comment!
384 else if (!line.startsWith("%%"))
385 break; // Not a EPS comment anymore, so abort as we are not in the EPS header anymore
386 }
387 if ( !lineIsBoundingBox )
388 {
389 kdError(30003) << "KoPictureEps::load: could not find a bounding box!" << endl;
390 return false;
391 }
392 // Floating point values are not allowed in a Bounding Box, but ther are many such files out there...
393 TQRegExp exp("(\\-?[0-9]+\\.?[0-9]*)\\s(\\-?[0-9]+\\.?[0-9]*)\\s(\\-?[0-9]+\\.?[0-9]*)\\s(\\-?[0-9]+\\.?[0-9]*)");
394 if ( exp.search(line) == -1 )
395 {
396 // ### TODO: it might be an "(atend)" and the bounding box is in the trailer
397 // (but GhostScript 7.07 does not support a bounding box in the trailer.)
398 // Note: in Trailer, it is the last BoundingBox that counts not the first!
399 kdError(30003) << "Not standard bounding box: " << line << endl;
400 return false;
401 }
402 kdDebug(30003) << "Reg. Exp. Found: " << exp.capturedTexts() << endl;
403 rect.setLeft((int)exp.cap(1).toDouble());
404 rect.setTop((int)exp.cap(2).toDouble());
405 rect.setRight((int)exp.cap(3).toDouble());
406 rect.setBottom((int)exp.cap(4).toDouble());
407 m_boundingBox=rect;
408 m_originalSize=rect.size();
409 kdDebug(30003) << "Rect: " << rect << " Size: " << m_originalSize << endl;
410 return true;
411}
412
413bool KoPictureEps::save(TQIODevice* io) const
414{
415 // We save the raw data, to avoid damaging the file by many load/save cycles
416 TQ_ULONG size=io->writeBlock(m_rawData); // WARNING: writeBlock returns TQ_LONG but size() TQ_ULONG!
417 return (size==m_rawData.size());
418}
419
420TQSize KoPictureEps::getOriginalSize(void) const
421{
422 return m_originalSize;
423}
424
425TQPixmap KoPictureEps::generatePixmap(const TQSize& size, bool smoothScale)
426{
427 scaleAndCreatePixmap(size,!smoothScale, 0, 0);
428 return m_cachedPixmap;
429}
430
431TQString KoPictureEps::getMimeType(const TQString&) const
432{
433 return "image/x-eps";
434}
435
436TQImage KoPictureEps::generateImage(const TQSize& size)
437{
438 // 0, 0 == resolution unknown
439 return scaleWithGhostScript(size, 0, 0);
440}
441
442void KoPictureEps::clearCache(void)
443{
444 m_cachedPixmap.resize(0, 0);
445 m_cacheIsInFastMode=true;
446 m_cachedSize=TQSize();
447}