1 /*
2     Softimage PIC support for QImage.
3 
4     SPDX-FileCopyrightText: 1998 Halfdan Ingvarsson
5     SPDX-FileCopyrightText: 2007 Ruben Lopez <r.lopez@bren.es>
6     SPDX-FileCopyrightText: 2014 Alex Merry <alex.merry@kde.org>
7 
8     SPDX-License-Identifier: LGPL-2.0-or-later
9 */
10 
11 /* This code is based on the GIMP-PIC plugin by Halfdan Ingvarsson,
12  * and relicensed from GPL to LGPL to accommodate the KDE licensing policy
13  * with his permission.
14  */
15 
16 #include "pic_p.h"
17 
18 #include "rle_p.h"
19 
20 #include <QDataStream>
21 #include <QDebug>
22 #include <QImage>
23 #include <QVariant>
24 #include <algorithm>
25 #include <functional>
26 #include <qendian.h>
27 #include <utility>
28 
29 /**
30  * Reads a PIC file header from a data stream.
31  *
32  * @param s         The data stream to read from.
33  * @param channels  Where the read header will be stored.
34  * @returns  @p s
35  *
36  * @relates PicHeader
37  */
operator >>(QDataStream & s,PicHeader & header)38 static QDataStream &operator>>(QDataStream &s, PicHeader &header)
39 {
40     s.setFloatingPointPrecision(QDataStream::SinglePrecision);
41     s >> header.magic;
42     s >> header.version;
43 
44     // the comment should be truncated to the first null byte
45     char comment[81] = {};
46     s.readRawData(comment, 80);
47     header.comment = QByteArray(comment);
48 
49     header.id.resize(4);
50     const int bytesRead = s.readRawData(header.id.data(), 4);
51     if (bytesRead != 4) {
52         header.id.resize(bytesRead);
53     }
54 
55     s >> header.width;
56     s >> header.height;
57     s >> header.ratio;
58     qint16 fields;
59     s >> fields;
60     header.fields = static_cast<PicFields>(fields);
61     qint16 pad;
62     s >> pad;
63     return s;
64 }
65 
66 /**
67  * Writes a PIC file header to a data stream.
68  *
69  * @param s         The data stream to write to.
70  * @param channels  The header to write.
71  * @returns  @p s
72  *
73  * @relates PicHeader
74  */
operator <<(QDataStream & s,const PicHeader & header)75 static QDataStream &operator<<(QDataStream &s, const PicHeader &header)
76 {
77     s.setFloatingPointPrecision(QDataStream::SinglePrecision);
78     s << header.magic;
79     s << header.version;
80 
81     char comment[80] = {};
82     strncpy(comment, header.comment.constData(), sizeof(comment));
83     s.writeRawData(comment, sizeof(comment));
84 
85     char id[4] = {};
86     strncpy(id, header.id.constData(), sizeof(id));
87     s.writeRawData(id, sizeof(id));
88 
89     s << header.width;
90     s << header.height;
91     s << header.ratio;
92     s << quint16(header.fields);
93     s << quint16(0);
94     return s;
95 }
96 
97 /**
98  * Reads a series of channel descriptions from a data stream.
99  *
100  * If the stream contains more than 8 channel descriptions, the status of @p s
101  * will be set to QDataStream::ReadCorruptData (note that more than 4 channels
102  * - one for each component - does not really make sense anyway).
103  *
104  * @param s         The data stream to read from.
105  * @param channels  The location to place the read channel descriptions; any
106  *                  existing entries will be cleared.
107  * @returns  @p s
108  *
109  * @relates PicChannel
110  */
operator >>(QDataStream & s,QList<PicChannel> & channels)111 static QDataStream &operator>>(QDataStream &s, QList<PicChannel> &channels)
112 {
113     const unsigned maxChannels = 8;
114     unsigned count = 0;
115     quint8 chained = 1;
116     channels.clear();
117     while (chained && count < maxChannels && s.status() == QDataStream::Ok) {
118         PicChannel channel;
119         s >> chained;
120         s >> channel.size;
121         s >> channel.encoding;
122         s >> channel.code;
123         channels << channel;
124         ++count;
125     }
126     if (chained) {
127         // too many channels!
128         s.setStatus(QDataStream::ReadCorruptData);
129     }
130     return s;
131 }
132 
133 /**
134  * Writes a series of channel descriptions to a data stream.
135  *
136  * Note that the corresponding read operation will not read more than 8 channel
137  * descriptions, although there should be no reason to have more than 4 channels
138  * anyway.
139  *
140  * @param s         The data stream to write to.
141  * @param channels  The channel descriptions to write.
142  * @returns  @p s
143  *
144  * @relates PicChannel
145  */
operator <<(QDataStream & s,const QList<PicChannel> & channels)146 static QDataStream &operator<<(QDataStream &s, const QList<PicChannel> &channels)
147 {
148     Q_ASSERT(channels.size() > 0);
149     for (int i = 0; i < channels.size() - 1; ++i) {
150         s << quint8(1); // chained
151         s << channels[i].size;
152         s << quint8(channels[i].encoding);
153         s << channels[i].code;
154     }
155     s << quint8(0); // chained
156     s << channels.last().size;
157     s << quint8(channels.last().encoding);
158     s << channels.last().code;
159     return s;
160 }
161 
readRow(QDataStream & stream,QRgb * row,quint16 width,const QList<PicChannel> & channels)162 static bool readRow(QDataStream &stream, QRgb *row, quint16 width, const QList<PicChannel> &channels)
163 {
164     for (const PicChannel &channel : channels) {
165         auto readPixel = [&](QDataStream &str) -> QRgb {
166             quint8 red = 0;
167             if (channel.code & RED) {
168                 str >> red;
169             }
170             quint8 green = 0;
171             if (channel.code & GREEN) {
172                 str >> green;
173             }
174             quint8 blue = 0;
175             if (channel.code & BLUE) {
176                 str >> blue;
177             }
178             quint8 alpha = 0;
179             if (channel.code & ALPHA) {
180                 str >> alpha;
181             }
182             return qRgba(red, green, blue, alpha);
183         };
184         auto updatePixel = [&](QRgb oldPixel, QRgb newPixel) -> QRgb {
185             return qRgba(qRed((channel.code & RED) ? newPixel : oldPixel),
186                          qGreen((channel.code & GREEN) ? newPixel : oldPixel),
187                          qBlue((channel.code & BLUE) ? newPixel : oldPixel),
188                          qAlpha((channel.code & ALPHA) ? newPixel : oldPixel));
189         };
190         if (channel.encoding == MixedRLE) {
191             bool success = decodeRLEData(RLEVariant::PIC, stream, row, width, readPixel, updatePixel);
192             if (!success) {
193                 qDebug() << "decodeRLEData failed";
194                 return false;
195             }
196         } else if (channel.encoding == Uncompressed) {
197             for (quint16 i = 0; i < width; ++i) {
198                 QRgb pixel = readPixel(stream);
199                 row[i] = updatePixel(row[i], pixel);
200             }
201         } else {
202             // unknown encoding
203             qDebug() << "Unknown encoding";
204             return false;
205         }
206     }
207     if (stream.status() != QDataStream::Ok) {
208         qDebug() << "DataStream status was" << stream.status();
209     }
210     return stream.status() == QDataStream::Ok;
211 }
212 
canRead() const213 bool SoftimagePICHandler::canRead() const
214 {
215     if (!SoftimagePICHandler::canRead(device())) {
216         return false;
217     }
218     setFormat("pic");
219     return true;
220 }
221 
read(QImage * image)222 bool SoftimagePICHandler::read(QImage *image)
223 {
224     if (!readChannels()) {
225         return false;
226     }
227 
228     QImage::Format fmt = QImage::Format_RGB32;
229     for (const PicChannel &channel : std::as_const(m_channels)) {
230         if (channel.size != 8) {
231             // we cannot read images that do not come in bytes
232             qDebug() << "Channel size was" << channel.size;
233             m_state = Error;
234             return false;
235         }
236         if (channel.code & ALPHA) {
237             fmt = QImage::Format_ARGB32;
238         }
239     }
240 
241     QImage img(m_header.width, m_header.height, fmt);
242     if (img.isNull()) {
243         qDebug() << "Failed to allocate image, invalid dimensions?" << QSize(m_header.width, m_header.height) << fmt;
244         return false;
245     }
246 
247     img.fill(qRgb(0, 0, 0));
248 
249     for (int y = 0; y < m_header.height; y++) {
250         QRgb *row = reinterpret_cast<QRgb *>(img.scanLine(y));
251         if (!readRow(m_dataStream, row, m_header.width, m_channels)) {
252             qDebug() << "readRow failed";
253             m_state = Error;
254             return false;
255         }
256     }
257 
258     *image = img;
259     m_state = Ready;
260 
261     return true;
262 }
263 
write(const QImage & _image)264 bool SoftimagePICHandler::write(const QImage &_image)
265 {
266     bool alpha = _image.hasAlphaChannel();
267     const QImage image = _image.convertToFormat(alpha ? QImage::Format_ARGB32 : QImage::Format_RGB32);
268 
269     if (image.width() < 0 || image.height() < 0) {
270         qDebug() << "Image size invalid:" << image.width() << image.height();
271         return false;
272     }
273     if (image.width() > 65535 || image.height() > 65535) {
274         qDebug() << "Image too big:" << image.width() << image.height();
275         // there are only two bytes for each dimension
276         return false;
277     }
278 
279     QDataStream stream(device());
280 
281     stream << PicHeader(image.width(), image.height(), m_description);
282 
283     PicChannelEncoding encoding = m_compression ? MixedRLE : Uncompressed;
284     QList<PicChannel> channels;
285     channels << PicChannel(encoding, RED | GREEN | BLUE);
286     if (alpha) {
287         channels << PicChannel(encoding, ALPHA);
288     }
289     stream << channels;
290 
291     for (int r = 0; r < image.height(); r++) {
292         const QRgb *row = reinterpret_cast<const QRgb *>(image.scanLine(r));
293 
294         /* Write the RGB part of the scanline */
295         auto rgbEqual = [](QRgb p1, QRgb p2) -> bool {
296             return qRed(p1) == qRed(p2) && qGreen(p1) == qGreen(p2) && qBlue(p1) == qBlue(p2);
297         };
298         auto writeRgb = [](QDataStream &str, QRgb pixel) -> void {
299             str << quint8(qRed(pixel)) << quint8(qGreen(pixel)) << quint8(qBlue(pixel));
300         };
301         if (m_compression) {
302             encodeRLEData(RLEVariant::PIC, stream, row, image.width(), rgbEqual, writeRgb);
303         } else {
304             for (int i = 0; i < image.width(); ++i) {
305                 writeRgb(stream, row[i]);
306             }
307         }
308 
309         /* Write the alpha channel */
310         if (alpha) {
311             auto alphaEqual = [](QRgb p1, QRgb p2) -> bool {
312                 return qAlpha(p1) == qAlpha(p2);
313             };
314             auto writeAlpha = [](QDataStream &str, QRgb pixel) -> void {
315                 str << quint8(qAlpha(pixel));
316             };
317             if (m_compression) {
318                 encodeRLEData(RLEVariant::PIC, stream, row, image.width(), alphaEqual, writeAlpha);
319             } else {
320                 for (int i = 0; i < image.width(); ++i) {
321                     writeAlpha(stream, row[i]);
322                 }
323             }
324         }
325     }
326     return stream.status() == QDataStream::Ok;
327 }
328 
canRead(QIODevice * device)329 bool SoftimagePICHandler::canRead(QIODevice *device)
330 {
331     char data[4];
332     if (device->peek(data, 4) != 4) {
333         return false;
334     }
335     return qFromBigEndian<qint32>(reinterpret_cast<uchar *>(data)) == PIC_MAGIC_NUMBER;
336 }
337 
readHeader()338 bool SoftimagePICHandler::readHeader()
339 {
340     if (m_state == Ready) {
341         m_state = Error;
342         m_dataStream.setDevice(device());
343         m_dataStream >> m_header;
344         if (m_header.isValid() && m_dataStream.status() == QDataStream::Ok) {
345             m_state = ReadHeader;
346         }
347     }
348 
349     return m_state != Error;
350 }
351 
readChannels()352 bool SoftimagePICHandler::readChannels()
353 {
354     readHeader();
355     if (m_state == ReadHeader) {
356         m_state = Error;
357         m_dataStream >> m_channels;
358         if (m_dataStream.status() == QDataStream::Ok) {
359             m_state = ReadChannels;
360         }
361     }
362     return m_state != Error;
363 }
364 
setOption(ImageOption option,const QVariant & value)365 void SoftimagePICHandler::setOption(ImageOption option, const QVariant &value)
366 {
367     switch (option) {
368     case CompressionRatio:
369         m_compression = value.toBool();
370         break;
371     case Description: {
372         m_description.clear();
373         const QStringList entries = value.toString().split(QStringLiteral("\n\n"));
374         for (const QString &entry : entries) {
375             if (entry.startsWith(QStringLiteral("Description: "))) {
376                 m_description = entry.mid(13).simplified().toUtf8();
377             }
378         }
379         break;
380     }
381     default:
382         break;
383     }
384 }
385 
option(ImageOption option) const386 QVariant SoftimagePICHandler::option(ImageOption option) const
387 {
388     const_cast<SoftimagePICHandler *>(this)->readHeader();
389     switch (option) {
390     case Size:
391         if (const_cast<SoftimagePICHandler *>(this)->readHeader()) {
392             return QSize(m_header.width, m_header.height);
393         } else {
394             return QVariant();
395         }
396     case CompressionRatio:
397         return m_compression;
398     case Description:
399         if (const_cast<SoftimagePICHandler *>(this)->readHeader()) {
400             QString descStr = QString::fromUtf8(m_header.comment);
401             if (!descStr.isEmpty()) {
402                 return QString(QStringLiteral("Description: ") + descStr + QStringLiteral("\n\n"));
403             }
404         }
405         return QString();
406     case ImageFormat:
407         if (const_cast<SoftimagePICHandler *>(this)->readChannels()) {
408             for (const PicChannel &channel : std::as_const(m_channels)) {
409                 if (channel.code & ALPHA) {
410                     return QImage::Format_ARGB32;
411                 }
412             }
413             return QImage::Format_RGB32;
414         }
415         return QVariant();
416     default:
417         return QVariant();
418     }
419 }
420 
supportsOption(ImageOption option) const421 bool SoftimagePICHandler::supportsOption(ImageOption option) const
422 {
423     return (option == CompressionRatio || option == Description || option == ImageFormat || option == Size);
424 }
425 
capabilities(QIODevice * device,const QByteArray & format) const426 QImageIOPlugin::Capabilities SoftimagePICPlugin::capabilities(QIODevice *device, const QByteArray &format) const
427 {
428     if (format == "pic") {
429         return Capabilities(CanRead | CanWrite);
430     }
431     if (!format.isEmpty()) {
432         return {};
433     }
434     if (!device->isOpen()) {
435         return {};
436     }
437 
438     Capabilities cap;
439     if (device->isReadable() && SoftimagePICHandler::canRead(device)) {
440         cap |= CanRead;
441     }
442     if (device->isWritable()) {
443         cap |= CanWrite;
444     }
445     return cap;
446 }
447 
create(QIODevice * device,const QByteArray & format) const448 QImageIOHandler *SoftimagePICPlugin::create(QIODevice *device, const QByteArray &format) const
449 {
450     QImageIOHandler *handler = new SoftimagePICHandler();
451     handler->setDevice(device);
452     handler->setFormat(format);
453     return handler;
454 }
455