1/*
2 This file is part of the KDE project
3 SPDX-FileCopyrightText: 2025 Mirco Miranda <mircomir@outlook.com>
4
5 SPDX-License-Identifier: LGPL-2.0-or-later
6*/
7
8#include "chunks_p.h"
9#include "iff_p.h"
10#include "util_p.h"
11
12#include <QIODevice>
13#include <QImage>
14#include <QPainter>
15
16class IFFHandlerPrivate
17{
18public:
19 IFFHandlerPrivate()
20 : m_imageNumber(0)
21 , m_imageCount(0)
22 {
23
24 }
25 ~IFFHandlerPrivate()
26 {
27
28 }
29
30 bool readStructure(QIODevice *d)
31 {
32 if (d == nullptr) {
33 return {};
34 }
35
36 if (!m_chunks.isEmpty()) {
37 return true;
38 }
39
40 auto ok = false;
41 auto chunks = IFFChunk::fromDevice(d, ok: &ok);
42 if (ok) {
43 m_chunks = chunks;
44 }
45 return ok;
46 }
47
48 template <class T>
49 static QList<const T*> searchForms(const IFFChunk::ChunkList &chunks, bool supportedOnly = true)
50 {
51 QList<const T*> list;
52 auto cid = T::defaultChunkId();
53 auto forms = IFFChunk::search(cid, chunks);
54 for (auto &&form : forms) {
55 if (auto f = dynamic_cast<const T*>(form.data()))
56 if (!supportedOnly || f->isSupported())
57 list << f;
58 }
59 return list;
60 }
61
62 template <class T>
63 QList<const T*> searchForms(bool supportedOnly = true)
64 {
65 return searchForms<T>(m_chunks, supportedOnly);
66 }
67
68 IFFChunk::ChunkList m_chunks;
69
70 /*!
71 * \brief m_imageNumber
72 * Value set by QImageReader::jumpToImage() or QImageReader::jumpToNextImage().
73 * The number of view selected in a multiview image.
74 */
75 qint32 m_imageNumber;
76
77 /*!
78 * \brief m_imageCount
79 * The total number of views (cache value)
80 */
81 mutable qint32 m_imageCount;
82};
83
84
85IFFHandler::IFFHandler()
86 : QImageIOHandler()
87 , d(new IFFHandlerPrivate)
88{
89
90}
91
92bool IFFHandler::canRead() const
93{
94 if (canRead(device: device())) {
95 setFormat("iff");
96 return true;
97 }
98 return false;
99}
100
101bool IFFHandler::canRead(QIODevice *device)
102{
103 if (!device) {
104 qCWarning(LOG_IFFPLUGIN) << "IFFHandler::canRead() called with no device";
105 return false;
106 }
107
108 if (device->isSequential()) {
109 return false;
110 }
111
112 // I avoid parsing obviously incorrect files
113 auto cid = device->peek(maxlen: 4);
114 if (cid != CAT__CHUNK &&
115 cid != FORM_CHUNK &&
116 cid != LIST_CHUNK &&
117 cid != CAT4_CHUNK &&
118 cid != FOR4_CHUNK &&
119 cid != LIS4_CHUNK) {
120 return false;
121 }
122
123 auto ok = false;
124 auto pos = device->pos();
125 auto chunks = IFFChunk::fromDevice(d: device, ok: &ok);
126 if (!device->seek(pos)) {
127 qCWarning(LOG_IFFPLUGIN) << "IFFHandler::canRead() unable to reset device position";
128 }
129 if (ok) {
130 auto forms = IFFHandlerPrivate::searchForms<FORMChunk>(chunks, supportedOnly: true);
131 auto for4s = IFFHandlerPrivate::searchForms<FOR4Chunk>(chunks, supportedOnly: true);
132 ok = !forms.isEmpty() || !for4s.isEmpty();
133 }
134 return ok;
135}
136
137static void addMetadata(QImage &img, const IFOR_Chunk *form)
138{
139 // standard IFF metadata
140 auto annos = IFFChunk::searchT<ANNOChunk>(chunk: form);
141 if (!annos.isEmpty()) {
142 auto anno = annos.first()->value();
143 if (!anno.isEmpty()) {
144 img.setText(QStringLiteral(META_KEY_DESCRIPTION), value: anno);
145 }
146 }
147 auto auths = IFFChunk::searchT<AUTHChunk>(chunk: form);
148 if (!auths.isEmpty()) {
149 auto auth = auths.first()->value();
150 if (!auth.isEmpty()) {
151 img.setText(QStringLiteral(META_KEY_AUTHOR), value: auth);
152 }
153 }
154 auto dates = IFFChunk::searchT<DATEChunk>(chunk: form);
155 if (!dates.isEmpty()) {
156 auto dt = dates.first()->value();
157 if (dt.isValid()) {
158 img.setText(QStringLiteral(META_KEY_CREATIONDATE), value: dt.toString(format: Qt::ISODate));
159 }
160 }
161 auto copys = IFFChunk::searchT<COPYChunk>(chunk: form);
162 if (!copys.isEmpty()) {
163 auto cp = copys.first()->value();
164 if (!cp.isEmpty()) {
165 img.setText(QStringLiteral(META_KEY_COPYRIGHT), value: cp);
166 }
167 }
168 auto names = IFFChunk::searchT<NAMEChunk>(chunk: form);
169 if (!names.isEmpty()) {
170 auto name = names.first()->value();
171 if (!name.isEmpty()) {
172 img.setText(QStringLiteral(META_KEY_TITLE), value: name);
173 }
174 }
175
176 // software info
177 auto vers = IFFChunk::searchT<VERSChunk>(chunk: form);
178 if (!vers.isEmpty()) {
179 auto ver = vers.first()->value();
180 if (!vers.isEmpty()) {
181 img.setText(QStringLiteral(META_KEY_SOFTWARE), value: ver);
182 }
183 }
184
185 // SView5 metadata
186 auto resChanged = false;
187 auto exifs = IFFChunk::searchT<EXIFChunk>(chunk: form);
188 if (!exifs.isEmpty()) {
189 auto exif = exifs.first()->value();
190 exif.updateImageMetadata(targetImage&: img, replaceExisting: false);
191 resChanged = exif.updateImageResolution(targetImage&: img);
192 }
193
194 auto xmp0s = IFFChunk::searchT<XMP0Chunk>(chunk: form);
195 if (!xmp0s.isEmpty()) {
196 auto xmp = xmp0s.first()->value();
197 if (!xmp.isEmpty()) {
198 img.setText(QStringLiteral(META_KEY_XMP_ADOBE), value: xmp);
199 }
200 }
201
202 auto iccps = IFFChunk::searchT<ICCPChunk>(chunk: form);
203 if (!iccps.isEmpty()) {
204 auto cs = iccps.first()->value();
205 if (cs.isValid()) {
206 auto iccns = IFFChunk::searchT<ICCNChunk>(chunk: form);
207 if (!iccns.isEmpty()) {
208 auto desc = iccns.first()->value();
209 if (!desc.isEmpty())
210 cs.setDescription(desc);
211 }
212 img.setColorSpace(cs);
213 }
214 }
215
216 // resolution -> leave after set of EXIF chunk
217 auto dpis = IFFChunk::searchT<DPIChunk>(chunk: form);
218 if (!dpis.isEmpty()) {
219 auto &&dpi = dpis.first();
220 if (dpi->isValid()) {
221 img.setDotsPerMeterX(dpi->dotsPerMeterX());
222 img.setDotsPerMeterY(dpi->dotsPerMeterY());
223 resChanged = true;
224 }
225 }
226
227 // if no explicit resolution was found, apply the aspect ratio to the default one
228 if (!resChanged) {
229 auto headers = IFFChunk::searchT<BMHDChunk>(chunk: form);
230 if (!headers.isEmpty()) {
231 auto xr = headers.first()->xAspectRatio();
232 auto yr = headers.first()->yAspectRatio();
233 if (xr > 0 && yr > 0 && xr > yr) {
234 img.setDotsPerMeterX(img.dotsPerMeterX() * yr / xr);
235 } else if (xr > 0 && yr > 0 && xr < yr) {
236 img.setDotsPerMeterY(img.dotsPerMeterY() * xr / yr);
237 }
238 }
239 }
240}
241
242/*!
243 * \brief convertIPAL
244 * \param img The source image.
245 * \param ipal The per line palette.
246 * \return The new image converted or \a img if no conversion is needed or possible.
247 */
248static QImage convertIPAL(const QImage& img, const IPALChunk *ipal)
249{
250 if (img.format() != QImage::Format_Indexed8) {
251 qDebug(catFunc: LOG_IFFPLUGIN) << "convertIPAL(): the image is not indexed!";
252 return img;
253 }
254
255 auto tmp = img.convertToFormat(FORMAT_RGB_8BIT);
256 if (tmp.isNull()) {
257 qCritical(catFunc: LOG_IFFPLUGIN) << "convertIPAL(): error while converting the image!";
258 return img;
259 }
260
261 for (auto y = 0, h = img.height(); y < h; ++y) {
262 auto src = reinterpret_cast<const quint8 *>(img.constScanLine(y));
263 auto dst = tmp.scanLine(y);
264 auto lpal = ipal->palette(y, height: h);
265 for (auto x = 0, w = img.width(); x < w; ++x) {
266 if (src[x] < lpal.size()) {
267 auto x3 = x * 3;
268 dst[x3] = qRed(rgb: lpal.at(i: src[x]));
269 dst[x3 + 1] = qGreen(rgb: lpal.at(i: src[x]));
270 dst[x3 + 2] = qBlue(rgb: lpal.at(i: src[x]));
271 }
272 }
273 }
274
275 return tmp;
276}
277
278bool IFFHandler::readStandardImage(QImage *image)
279{
280 auto forms = d->searchForms<FORMChunk>();
281 if (forms.isEmpty()) {
282 return false;
283 }
284 auto cin = qBound(min: 0, val: currentImageNumber(), max: int(forms.size() - 1));
285 auto &&form = forms.at(i: cin);
286
287 // show the first one (I don't have a sample with many images)
288 auto headers = IFFChunk::searchT<BMHDChunk>(chunk: form);
289 if (headers.isEmpty()) {
290 qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readStandardImage() no supported image found";
291 return false;
292 }
293
294 // create the image
295 auto &&header = headers.first();
296 auto img = imageAlloc(size: header->size(), format: form->format());
297 if (img.isNull()) {
298 qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readStandardImage() error while allocating the image";
299 return false;
300 }
301
302 // set color table
303 const CAMGChunk *camg = nullptr;
304 auto camgs = IFFChunk::searchT<CAMGChunk>(chunk: form);
305 if (!camgs.isEmpty()) {
306 camg = camgs.first();
307 }
308
309 const CMAPChunk *cmap = nullptr;
310 auto cmaps = IFFChunk::searchT<CMAPChunk>(chunk: form);
311 if (cmaps.isEmpty()) {
312 auto cmyks = IFFChunk::searchT<CMYKChunk>(chunk: form);
313 for (auto &&cmyk : cmyks)
314 cmaps.append(t: cmyk);
315 }
316 if (!cmaps.isEmpty()) {
317 cmap = cmaps.first();
318 }
319 if (img.format() == QImage::Format_Indexed8) {
320 if (cmap) {
321 auto halfbride = BODYChunk::safeModeId(header, camg, cmap) & CAMGChunk::ModeId::HalfBrite ? true : false;
322 img.setColorTable(cmap->palette(halfbride));
323 }
324 }
325
326 // reading image data
327 auto ipal = form->searchIPal();
328 auto bodies = IFFChunk::searchT<BODYChunk>(chunk: form);
329 if (bodies.isEmpty()) {
330 auto abits = IFFChunk::searchT<ABITChunk>(chunk: form);
331 for (auto &&abit : abits)
332 bodies.append(t: abit);
333 }
334 if (bodies.isEmpty()) {
335 img.fill(pixel: 0);
336 } else {
337 auto &&body = bodies.first();
338 if (!body->resetStrideRead(d: device())) {
339 qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readStandardImage() error while reading image data";
340 return false;
341 }
342 for (auto y = 0, h = img.height(); y < h; ++y) {
343 auto line = reinterpret_cast<char*>(img.scanLine(y));
344 auto ba = body->strideRead(d: device(), y, header, camg, cmap, ipal, formType: form->formType());
345 if (ba.isEmpty()) {
346 qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readStandardImage() error while reading image scanline";
347 return false;
348 }
349 memcpy(dest: line, src: ba.constData(), n: std::min(a: img.bytesPerLine(), b: ba.size()));
350 }
351 }
352
353 // BEAM / CTBL conversion (if not already done)
354 if (ipal && img.format() == QImage::Format_Indexed8) {
355 img = convertIPAL(img, ipal);
356 }
357
358 // set metadata (including image resolution)
359 addMetadata(img, form);
360
361 *image = img;
362 return true;
363}
364
365bool IFFHandler::readMayaImage(QImage *image)
366{
367 auto forms = d->searchForms<FOR4Chunk>();
368 if (forms.isEmpty()) {
369 return false;
370 }
371 auto cin = qBound(min: 0, val: currentImageNumber(), max: int(forms.size() - 1));
372 auto &&form = forms.at(i: cin);
373
374 // show the first one (I don't have a sample with many images)
375 auto headers = IFFChunk::searchT<TBHDChunk>(chunk: form);
376 if (headers.isEmpty()) {
377 qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readMayaImage() no supported image found";
378 return false;
379 }
380
381 // create the image
382 auto &&header = headers.first();
383 auto img = imageAlloc(size: header->size(), format: form->format());
384 if (img.isNull()) {
385 qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readMayaImage() error while allocating the image";
386 return false;
387 }
388
389 auto &&tiles = IFFChunk::searchT<RGBAChunk>(chunk: form);
390 if ((tiles.size() & 0xFFFF) != header->tiles()) { // Photoshop, on large images saves more than 65535 tiles
391 qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readMayaImage() tile number mismatch: found" << tiles.size() << "while expected" << header->tiles();
392 return false;
393 }
394 for (auto &&tile : tiles) {
395 auto tp = tile->pos();
396 auto ts = tile->size();
397 if (tp.x() < 0 || tp.x() + ts.width() > img.width()) {
398 qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readMayaImage() wrong tile position or size";
399 return false;
400 }
401 if (tp.y() < 0 || tp.y() + ts.height() > img.height()) {
402 qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readMayaImage() wrong tile position or size";
403 return false;
404 }
405 // For future releases: it might be a good idea not to use a QPainter
406 auto ti = tile->tile(d: device(), header);
407 if (ti.isNull()) {
408 qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readMayaImage() error while decoding the tile";
409 return false;
410 }
411 QPainter painter(&img);
412 painter.setCompositionMode(QPainter::CompositionMode_Source);
413 painter.drawImage(p: tp, image: ti);
414 }
415#if QT_VERSION < QT_VERSION_CHECK(6, 9, 0)
416 img.mirror(horizontally: false, vertically: true);
417#else
418 img.flip(Qt::Orientation::Vertical);
419#endif
420 addMetadata(img, form);
421
422 *image = img;
423 return true;
424}
425
426bool IFFHandler::read(QImage *image)
427{
428 if (!d->readStructure(d: device())) {
429 qCWarning(LOG_IFFPLUGIN) << "IFFHandler::read() invalid IFF structure";
430 return false;
431 }
432
433 if (readStandardImage(image)) {
434 return true;
435 }
436
437 if (readMayaImage(image)) {
438 return true;
439 }
440
441 qCWarning(LOG_IFFPLUGIN) << "IFFHandler::read() no supported image found";
442 return false;
443}
444
445bool IFFHandler::supportsOption(ImageOption option) const
446{
447 if (option == QImageIOHandler::Size) {
448 return true;
449 }
450 if (option == QImageIOHandler::ImageFormat) {
451 return true;
452 }
453 if (option == QImageIOHandler::ImageTransformation) {
454 return true;
455 }
456 return false;
457}
458
459QVariant IFFHandler::option(ImageOption option) const
460{
461 if (!supportsOption(option)) {
462 return {};
463 }
464
465 const IFOR_Chunk *form = nullptr;
466 if (d->readStructure(d: device())) {
467 auto forms = d->searchForms<FORMChunk>();
468 auto for4s = d->searchForms<FOR4Chunk>();
469 auto cin = currentImageNumber();
470 if (!forms.isEmpty())
471 form = cin < forms.size() ? forms.at(i: cin) : forms.first();
472 else if (!for4s.isEmpty())
473 form = cin < for4s.size() ? for4s.at(i: cin) : for4s.first();
474 }
475 if (form == nullptr) {
476 return {};
477 }
478
479 if (option == QImageIOHandler::Size) {
480 return QVariant::fromValue(value: form->size());
481 }
482
483 if (option == QImageIOHandler::ImageFormat) {
484 return QVariant::fromValue(value: form->optionformat());
485 }
486
487 if (option == QImageIOHandler::ImageTransformation) {
488 return QVariant::fromValue(value: form->transformation());
489 }
490
491 return {};
492}
493
494bool IFFHandler::jumpToNextImage()
495{
496 return jumpToImage(imageNumber: d->m_imageNumber + 1);
497}
498
499bool IFFHandler::jumpToImage(int imageNumber)
500{
501 if (imageNumber < 0 || imageNumber >= imageCount()) {
502 return false;
503 }
504 d->m_imageNumber = imageNumber;
505 return true;
506}
507
508int IFFHandler::imageCount() const
509{
510 // NOTE: image count is cached for performance reason
511 auto &&count = d->m_imageCount;
512 if (count > 0) {
513 return count;
514 }
515
516 count = QImageIOHandler::imageCount();
517 if (!d->readStructure(d: device())) {
518 qCWarning(LOG_IFFPLUGIN) << "IFFHandler::imageCount() invalid IFF structure";
519 return count;
520 }
521
522 auto forms = d->searchForms<FORMChunk>();
523 auto for4s = d->searchForms<FOR4Chunk>();
524 if (!forms.isEmpty())
525 count = forms.size();
526 else if (!for4s.isEmpty())
527 count = for4s.size();
528
529 return count;
530}
531
532int IFFHandler::currentImageNumber() const
533{
534 return d->m_imageNumber;
535}
536
537QImageIOPlugin::Capabilities IFFPlugin::capabilities(QIODevice *device, const QByteArray &format) const
538{
539 if (format == "iff" || format == "ilbm" || format == "lbm") {
540 return Capabilities(CanRead);
541 }
542 if (!format.isEmpty()) {
543 return {};
544 }
545 if (!device->isOpen()) {
546 return {};
547 }
548
549 Capabilities cap;
550 if (device->isReadable() && IFFHandler::canRead(device)) {
551 cap |= CanRead;
552 }
553 return cap;
554}
555
556QImageIOHandler *IFFPlugin::create(QIODevice *device, const QByteArray &format) const
557{
558 QImageIOHandler *handler = new IFFHandler;
559 handler->setDevice(device);
560 handler->setFormat(format);
561 return handler;
562}
563
564#include "moc_iff_p.cpp"
565

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