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 | |
16 | class IFFHandlerPrivate |
17 | { |
18 | public: |
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 | |
85 | IFFHandler::IFFHandler() |
86 | : QImageIOHandler() |
87 | , d(new IFFHandlerPrivate) |
88 | { |
89 | |
90 | } |
91 | |
92 | bool IFFHandler::canRead() const |
93 | { |
94 | if (canRead(device: device())) { |
95 | setFormat("iff" ); |
96 | return true; |
97 | } |
98 | return false; |
99 | } |
100 | |
101 | bool 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 | |
137 | static 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 = 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 | */ |
248 | static 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 | |
278 | bool 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 = 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 && = 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 | |
365 | bool 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 = 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 && = 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 | |
426 | bool 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 | |
445 | bool 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 | |
459 | QVariant 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 | |
494 | bool IFFHandler::jumpToNextImage() |
495 | { |
496 | return jumpToImage(imageNumber: d->m_imageNumber + 1); |
497 | } |
498 | |
499 | bool 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 | |
508 | int 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 | |
532 | int IFFHandler::currentImageNumber() const |
533 | { |
534 | return d->m_imageNumber; |
535 | } |
536 | |
537 | QImageIOPlugin::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 | |
556 | QImageIOHandler *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 | |