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#include "rle_p.h"
18#include "util_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 */
38static 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, len: 80);
47 header.comment = QByteArray(comment);
48
49 header.id.resize(size: 4);
50 const int bytesRead = s.readRawData(header.id.data(), len: 4);
51 if (bytesRead != 4) {
52 header.id.resize(size: 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 */
75static 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(dest: comment, src: header.comment.constData(), n: sizeof(comment));
83 s.writeRawData(comment, len: sizeof(comment));
84
85 char id[4] = {};
86 strncpy(dest: id, src: header.id.constData(), n: sizeof(id));
87 s.writeRawData(id, len: 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 */
111static 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 */
146static 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
162static 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(r: red, g: green, b: blue, a: alpha);
183 };
184 auto updatePixel = [&](QRgb oldPixel, QRgb newPixel) -> QRgb {
185 return qRgba(r: qRed(rgb: (channel.code & RED) ? newPixel : oldPixel),
186 g: qGreen(rgb: (channel.code & GREEN) ? newPixel : oldPixel),
187 b: qBlue(rgb: (channel.code & BLUE) ? newPixel : oldPixel),
188 a: qAlpha(rgb: (channel.code & ALPHA) ? newPixel : oldPixel));
189 };
190 if (channel.encoding == MixedRLE) {
191 bool success = decodeRLEData(variant: RLEVariant::PIC, stream, dest: row, length: width, readData: readPixel, updateItem: 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
213bool SoftimagePICHandler::canRead() const
214{
215 if (!SoftimagePICHandler::canRead(device: device())) {
216 return false;
217 }
218 setFormat("pic");
219 return true;
220}
221
222bool 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(t&: 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 = imageAlloc(width: m_header.width, height: m_header.height, format: 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(pixel: qRgb(r: 0, g: 0, b: 0));
248
249 for (int y = 0; y < m_header.height; y++) {
250 QRgb *row = reinterpret_cast<QRgb *>(img.scanLine(y));
251 if (!readRow(stream&: m_dataStream, row, width: m_header.width, channels: 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
264bool SoftimagePICHandler::write(const QImage &_image)
265{
266 bool alpha = _image.hasAlphaChannel();
267 const QImage image = _image.convertToFormat(f: 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(rgb: p1) == qRed(rgb: p2) && qGreen(rgb: p1) == qGreen(rgb: p2) && qBlue(rgb: p1) == qBlue(rgb: p2);
297 };
298 auto writeRgb = [](QDataStream &str, QRgb pixel) -> void {
299 str << quint8(qRed(rgb: pixel)) << quint8(qGreen(rgb: pixel)) << quint8(qBlue(rgb: pixel));
300 };
301 if (m_compression) {
302 encodeRLEData(variant: RLEVariant::PIC, stream, data: row, length: image.width(), itemsEqual: rgbEqual, writeItem: 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(rgb: p1) == qAlpha(rgb: p2);
313 };
314 auto writeAlpha = [](QDataStream &str, QRgb pixel) -> void {
315 str << quint8(qAlpha(rgb: pixel));
316 };
317 if (m_compression) {
318 encodeRLEData(variant: RLEVariant::PIC, stream, data: row, length: image.width(), itemsEqual: alphaEqual, writeItem: 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
329bool SoftimagePICHandler::canRead(QIODevice *device)
330{
331 char data[4];
332 if (device->peek(data, maxlen: 4) != 4) {
333 return false;
334 }
335 return qFromBigEndian<qint32>(src: reinterpret_cast<uchar *>(data)) == PIC_MAGIC_NUMBER;
336}
337
338bool 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
352bool 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
365void 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(position: 13).simplified().toUtf8();
377 }
378 }
379 break;
380 }
381 default:
382 break;
383 }
384}
385
386QVariant 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(ba: 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(t: 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
421bool SoftimagePICHandler::supportsOption(ImageOption option) const
422{
423 return (option == CompressionRatio || option == Description || option == ImageFormat || option == Size);
424}
425
426QImageIOPlugin::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
448QImageIOHandler *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
456#include "moc_pic_p.cpp"
457

source code of kimageformats/src/imageformats/pic.cpp