1/*
2 This file is part of the KDE project
3 SPDX-FileCopyrightText: 2025-2026 Mirco Miranda <mircomir@outlook.com>
4
5 SPDX-License-Identifier: LGPL-2.0-or-later
6*/
7
8#include "chunks_p.h"
9#include "packbits_p.h"
10#include "util_p.h"
11
12#include <QBuffer>
13#include <QColor>
14#include <QDataStream>
15
16#ifdef QT_DEBUG
17Q_LOGGING_CATEGORY(LOG_IFFPLUGIN, "kf.imageformats.plugins.iff", QtDebugMsg)
18#else
19Q_LOGGING_CATEGORY(LOG_IFFPLUGIN, "kf.imageformats.plugins.iff", QtWarningMsg)
20#endif
21
22#define RECURSION_PROTECTION 10
23
24#define BITPLANES_HAM_MAX 8
25#define BITPLANES_HAM_MIN 5
26#define BITPLANES_HALFBRIDE_MAX 8
27#define BITPLANES_HALFBRIDE_MIN 1
28
29static QString dataToString(const IFFChunk *chunk)
30{
31 if (chunk == nullptr || !chunk->isValid()) {
32 return {};
33 }
34 auto dt = chunk->data();
35 for (; dt.endsWith(c: char()); dt = dt.removeLast());
36 return QString::fromUtf8(ba: dt).trimmed();
37}
38
39IFFChunk::~IFFChunk()
40{
41
42}
43
44IFFChunk::IFFChunk()
45 : _chunkId{0}
46 , _size{0}
47 , _align{2}
48 , _dataPos{0}
49 , _recursionCnt{0}
50{
51}
52
53bool IFFChunk::operator ==(const IFFChunk &other) const
54{
55 if (chunkId() != other.chunkId()) {
56 return false;
57 }
58 return _size == other._size && _dataPos == other._dataPos;
59}
60
61bool IFFChunk::isValid() const
62{
63 auto cid = chunkId();
64 if (cid.isEmpty()) {
65 return false;
66 }
67 // A “type ID”, “property name”, “FORM type”, or any other IFF
68 // identifier is a 32-bit value: the concatenation of four ASCII
69 // characters in the range “ ” (SP, hex 20) through “~” (hex 7E).
70 // Spaces (hex 20) should not precede printing characters;
71 // trailing spaces are OK. Control characters are forbidden.
72 if (cid.at(i: 0) == ' ') {
73 return false;
74 }
75 for (auto &&c : cid) {
76 if (c < ' ' || c > '~')
77 return false;
78 }
79 return true;
80}
81
82qint32 IFFChunk::alignBytes() const
83{
84 return _align;
85}
86
87bool IFFChunk::readStructure(QIODevice *d)
88{
89 auto ok = readInfo(d);
90 if (recursionCounter() > RECURSION_PROTECTION - 1) {
91 ok = ok && IFFChunk::innerReadStructure(d); // force default implementation (no more recursion)
92 } else {
93 ok = ok && innerReadStructure(d);
94 }
95 if (ok) {
96 ok = d->seek(pos: nextChunkPos());
97 }
98 return ok;
99}
100
101QByteArray IFFChunk::chunkId() const
102{
103 return QByteArray(_chunkId, 4);
104}
105
106quint32 IFFChunk::bytes() const
107{
108 return _size;
109}
110
111const QByteArray &IFFChunk::data() const
112{
113 return _data;
114}
115
116const IFFChunk::ChunkList &IFFChunk::chunks() const
117{
118 return _chunks;
119}
120
121quint8 IFFChunk::chunkVersion(const QByteArray &cid)
122{
123 if (cid.size() != 4) {
124 return 0;
125 }
126 if (cid.at(i: 3) >= char('2') && cid.at(i: 3) <= char('9')) {
127 return quint8(cid.at(i: 3) - char('0'));
128 }
129 return 1;
130}
131
132bool IFFChunk::isChunkType(const QByteArray &cid) const
133{
134 if (chunkId() == cid) {
135 return true;
136 }
137 if (chunkId().startsWith(bv: cid.left(n: 3)) && IFFChunk::chunkVersion(cid) > 1) {
138 return true;
139 }
140 return false;
141}
142
143bool IFFChunk::readInfo(QIODevice *d)
144{
145 if (d == nullptr || d->read(data: _chunkId, maxlen: 4) != 4) {
146 return false;
147 }
148 if (!IFFChunk::isValid()) {
149 return false;
150 }
151 auto sz = d->read(maxlen: 4);
152 if (sz.size() != 4) {
153 return false;
154 }
155 _size = ui32(c1: sz.at(i: 3), c2: sz.at(i: 2), c3: sz.at(i: 1), c4: sz.at(i: 0));
156 _dataPos = d->pos();
157 return true;
158}
159
160QByteArray IFFChunk::readRawData(QIODevice *d, qint64 relPos, qint64 size) const
161{
162 if (!seek(d, relPos)) {
163 return{};
164 }
165 if (size == -1) {
166 size = _size;
167 }
168 auto read = std::min(a: size, b: _size - relPos);
169 return d->read(maxlen: read);
170}
171
172bool IFFChunk::seek(QIODevice *d, qint64 relPos) const
173{
174 if (d == nullptr) {
175 return false;
176 }
177 return d->seek(pos: _dataPos + relPos);
178}
179
180bool IFFChunk::innerReadStructure(QIODevice *)
181{
182 return true;
183}
184
185void IFFChunk::setAlignBytes(qint32 bytes)
186{
187 _align = bytes;
188}
189
190qint64 IFFChunk::nextChunkPos() const
191{
192 auto pos = _dataPos + _size;
193 if (auto align = pos % alignBytes())
194 pos += alignBytes() - align;
195 return pos;
196}
197
198IFFChunk::ChunkList IFFChunk::search(const QByteArray &cid, const QSharedPointer<IFFChunk> &chunk)
199{
200 return search(cid, chunks: ChunkList() << chunk);
201}
202
203IFFChunk::ChunkList IFFChunk::search(const QByteArray &cid, const ChunkList &chunks)
204{
205 IFFChunk::ChunkList list;
206 for (auto &&chunk : chunks) {
207 if (chunk->chunkId() == cid)
208 list << chunk;
209 list << IFFChunk::search(cid, chunks: chunk->_chunks);
210 }
211 return list;
212}
213
214bool IFFChunk::cacheData(QIODevice *d)
215{
216 if (bytes() > 8 * 1024 * 1024) {
217 return false;
218 }
219 _data = readRawData(d);
220 return _data.size() == _size;
221}
222
223void IFFChunk::setChunks(const ChunkList &chunks)
224{
225 _chunks = chunks;
226}
227
228qint32 IFFChunk::recursionCounter() const
229{
230 return _recursionCnt;
231}
232
233void IFFChunk::setRecursionCounter(qint32 cnt)
234{
235 _recursionCnt = cnt;
236}
237
238quint32 IFFChunk::dataBytes() const
239{
240 return std::min(a: bytes(), b: quint32(data().size()));
241}
242
243IFFChunk::ChunkList IFFChunk::innerFromDevice(QIODevice *d, bool *ok, IFFChunk *parent)
244{
245 auto tmp = false;
246 if (ok == nullptr) {
247 ok = &tmp;
248 }
249 *ok = false;
250
251 if (d == nullptr) {
252 return {};
253 }
254
255 auto alignBytes = qint32(2);
256 auto recursionCnt = qint32();
257 auto nextChunkPos = qint64();
258 if (parent) {
259 alignBytes = parent->alignBytes();
260 recursionCnt = parent->recursionCounter();
261 nextChunkPos = parent->nextChunkPos();
262 }
263
264 if (recursionCnt > RECURSION_PROTECTION) {
265 return {};
266 }
267
268 IFFChunk::ChunkList list;
269 for (; !d->atEnd() && (nextChunkPos == 0 || d->pos() < nextChunkPos);) {
270 auto cid = d->peek(maxlen: 4);
271 QSharedPointer<IFFChunk> chunk;
272 if (cid == ABIT_CHUNK) {
273 chunk = QSharedPointer<IFFChunk>(new ABITChunk());
274 } else if (cid == ANNO_CHUNK) {
275 chunk = QSharedPointer<IFFChunk>(new ANNOChunk());
276 } else if (cid == AUTH_CHUNK) {
277 chunk = QSharedPointer<IFFChunk>(new AUTHChunk());
278 } else if (cid == BEAM_CHUNK) {
279 chunk = QSharedPointer<IFFChunk>(new BEAMChunk());
280 } else if (cid == BMHD_CHUNK) {
281 chunk = QSharedPointer<IFFChunk>(new BMHDChunk());
282 } else if (cid == BODY_CHUNK) {
283 chunk = QSharedPointer<IFFChunk>(new BODYChunk());
284 } else if (cid == CAMG_CHUNK) {
285 chunk = QSharedPointer<IFFChunk>(new CAMGChunk());
286 } else if (cid == CAT__CHUNK) {
287 chunk = QSharedPointer<IFFChunk>(new CATChunk());
288 } else if (cid == CMAP_CHUNK) {
289 chunk = QSharedPointer<IFFChunk>(new CMAPChunk());
290 } else if (cid == CMYK_CHUNK) {
291 chunk = QSharedPointer<IFFChunk>(new CMYKChunk());
292 } else if (cid == COPY_CHUNK) {
293 chunk = QSharedPointer<IFFChunk>(new COPYChunk());
294 } else if (cid == CTBL_CHUNK) {
295 chunk = QSharedPointer<IFFChunk>(new CTBLChunk());
296 } else if (cid == DATE_CHUNK) {
297 chunk = QSharedPointer<IFFChunk>(new DATEChunk());
298 } else if (cid == DPI__CHUNK) {
299 chunk = QSharedPointer<IFFChunk>(new DPIChunk());
300 } else if (cid == EXIF_CHUNK) {
301 chunk = QSharedPointer<IFFChunk>(new EXIFChunk());
302 } else if (cid == FOR4_CHUNK) {
303 chunk = QSharedPointer<IFFChunk>(new FOR4Chunk());
304 } else if (cid == FORM_CHUNK) {
305 chunk = QSharedPointer<IFFChunk>(new FORMChunk());
306 } else if (cid == FVER_CHUNK) {
307 chunk = QSharedPointer<IFFChunk>(new FVERChunk());
308 } else if (cid == HIST_CHUNK) {
309 chunk = QSharedPointer<IFFChunk>(new HISTChunk());
310 } else if (cid == ICCN_CHUNK) {
311 chunk = QSharedPointer<IFFChunk>(new ICCNChunk());
312 } else if (cid == ICCP_CHUNK) {
313 chunk = QSharedPointer<IFFChunk>(new ICCPChunk());
314 } else if (cid == IDAT_CHUNK) {
315 chunk = QSharedPointer<IFFChunk>(new IDATChunk());
316 } else if (cid == IHDR_CHUNK) {
317 chunk = QSharedPointer<IFFChunk>(new IHDRChunk());
318 } else if (cid == IPAR_CHUNK) {
319 chunk = QSharedPointer<IFFChunk>(new IPARChunk());
320 } else if (cid == NAME_CHUNK) {
321 chunk = QSharedPointer<IFFChunk>(new NAMEChunk());
322 } else if (cid == PCHG_CHUNK) {
323 chunk = QSharedPointer<IFFChunk>(new PCHGChunk());
324 } else if (cid == PLTE_CHUNK) {
325 chunk = QSharedPointer<IFFChunk>(new PLTEChunk());
326 } else if (cid == RAST_CHUNK) {
327 chunk = QSharedPointer<IFFChunk>(new RASTChunk());
328 } else if (cid == RBOD_CHUNK) {
329 chunk = QSharedPointer<IFFChunk>(new RBODChunk());
330 } else if (cid == RCOL_CHUNK) {
331 chunk = QSharedPointer<IFFChunk>(new RCOLChunk());
332 } else if (cid == RFLG_CHUNK) {
333 chunk = QSharedPointer<IFFChunk>(new RFLGChunk());
334 } else if (cid == RGBA_CHUNK) {
335 chunk = QSharedPointer<IFFChunk>(new RGBAChunk());
336 } else if (cid == RGHD_CHUNK) {
337 chunk = QSharedPointer<IFFChunk>(new RGHDChunk());
338 } else if (cid == RSCM_CHUNK) {
339 chunk = QSharedPointer<IFFChunk>(new RSCMChunk());
340 } else if (cid == SHAM_CHUNK) {
341 chunk = QSharedPointer<IFFChunk>(new SHAMChunk());
342 } else if (cid == TBHD_CHUNK) {
343 chunk = QSharedPointer<IFFChunk>(new TBHDChunk());
344 } else if (cid == VDAT_CHUNK) {
345 chunk = QSharedPointer<IFFChunk>(new VDATChunk());
346 } else if (cid == VERS_CHUNK) {
347 chunk = QSharedPointer<IFFChunk>(new VERSChunk());
348 } else if (cid == XBMI_CHUNK) {
349 chunk = QSharedPointer<IFFChunk>(new XBMIChunk());
350 } else if (cid == XMP0_CHUNK) {
351 chunk = QSharedPointer<IFFChunk>(new XMP0Chunk());
352 } else if (cid == YUVS_CHUNK) {
353 chunk = QSharedPointer<IFFChunk>(new YUVSChunk());
354 } else { // unknown chunk
355 chunk = QSharedPointer<IFFChunk>(new IFFChunk());
356 qCDebug(LOG_IFFPLUGIN) << "IFFChunk::innerFromDevice(): unknown chunk" << cid;
357 }
358
359 // change the alignment to the one of main chunk (required for unknown Maya IFF chunks)
360 if (chunk->isChunkType(CAT__CHUNK)
361 || chunk->isChunkType(FILL_CHUNK)
362 || chunk->isChunkType(FORM_CHUNK)
363 || chunk->isChunkType(LIST_CHUNK)
364 || chunk->isChunkType(PROP_CHUNK)) {
365 alignBytes = chunk->alignBytes();
366 } else {
367 chunk->setAlignBytes(alignBytes);
368 }
369
370 chunk->setRecursionCounter(recursionCnt + 1);
371 if (!chunk->readStructure(d)) {
372 *ok = false;
373 return {};
374 }
375
376 // skip any non-IFF data at the end of the file.
377 // NOTE: there should be no more chunks after the first (root)
378 if (nextChunkPos == 0) {
379 nextChunkPos = chunk->nextChunkPos();
380 }
381
382 list << chunk;
383 }
384
385 *ok = true;
386 return list;
387}
388
389IFFChunk::ChunkList IFFChunk::fromDevice(QIODevice *d, bool *ok)
390{
391 return innerFromDevice(d, ok, parent: nullptr);
392}
393
394
395/* ******************
396 * *** BMHD Chunk ***
397 * ****************** */
398
399BMHDChunk::~BMHDChunk()
400{
401
402}
403
404BMHDChunk::BMHDChunk() : IFFChunk()
405{
406}
407
408bool BMHDChunk::isValid() const
409{
410 if (dataBytes() < 20) {
411 return false;
412 }
413 return chunkId() == BMHDChunk::defaultChunkId();
414}
415
416bool BMHDChunk::innerReadStructure(QIODevice *d)
417{
418 return cacheData(d);
419}
420
421qint32 BMHDChunk::width() const
422{
423 if (!isValid()) {
424 return 0;
425 }
426 return qint32(ui16(c1: data().at(i: 1), c2: data().at(i: 0)));
427}
428
429qint32 BMHDChunk::height() const
430{
431 if (!isValid()) {
432 return 0;
433 }
434 return qint32(ui16(c1: data().at(i: 3), c2: data().at(i: 2)));
435}
436
437QSize BMHDChunk::size() const
438{
439 return QSize(width(), height());
440}
441
442qint32 BMHDChunk::left() const
443{
444 if (!isValid()) {
445 return 0;
446 }
447 return qint32(ui16(c1: data().at(i: 5), c2: data().at(i: 4)));
448}
449
450qint32 BMHDChunk::top() const
451{
452 if (!isValid()) {
453 return 0;
454 }
455 return qint32(ui16(c1: data().at(i: 7), c2: data().at(i: 6)));
456}
457
458quint8 BMHDChunk::bitplanes() const
459{
460 if (!isValid()) {
461 return 0;
462 }
463 return quint8(data().at(i: 8));
464}
465
466BMHDChunk::Masking BMHDChunk::masking() const
467{
468 if (!isValid()) {
469 return BMHDChunk::Masking::None;
470 }
471 return BMHDChunk::Masking(quint8(data().at(i: 9)));
472}
473
474BMHDChunk::Compression BMHDChunk::compression() const
475{
476 if (!isValid()) {
477 return BMHDChunk::Compression::Uncompressed;
478 }
479 return BMHDChunk::Compression(data().at(i: 10));
480
481}
482
483qint16 BMHDChunk::transparency() const
484{
485 if (!isValid()) {
486 return 0;
487 }
488 return i16(c1: data().at(i: 13), c2: data().at(i: 12));
489}
490
491quint8 BMHDChunk::xAspectRatio() const
492{
493 if (!isValid()) {
494 return 0;
495 }
496 return quint8(data().at(i: 14));
497}
498
499quint8 BMHDChunk::yAspectRatio() const
500{
501 if (!isValid()) {
502 return 0;
503 }
504 return quint8(data().at(i: 15));
505}
506
507quint16 BMHDChunk::pageWidth() const
508{
509 if (!isValid()) {
510 return 0;
511 }
512 return ui16(c1: data().at(i: 17), c2: data().at(i: 16));
513}
514
515quint16 BMHDChunk::pageHeight() const
516{
517 if (!isValid()) {
518 return 0;
519 }
520 return ui16(c1: data().at(i: 19), c2: data().at(i: 18));
521}
522
523quint32 BMHDChunk::rowLen() const
524{
525 return ((quint32(width()) + 15) / 16) * 2;
526}
527
528/* ******************
529 * *** CMAP Chunk ***
530 * ****************** */
531
532CMAPChunk::~CMAPChunk()
533{
534
535}
536
537CMAPChunk::CMAPChunk() : IFFChunk()
538{
539}
540
541bool CMAPChunk::isValid() const
542{
543 return chunkId() == CMAPChunk::defaultChunkId();
544}
545
546qint32 CMAPChunk::count() const
547{
548 if (!isValid()) {
549 return 0;
550 }
551 return dataBytes() / 3;
552}
553
554QList<QRgb> CMAPChunk::palette(bool halfbride) const
555{
556 auto p = innerPalette();
557 if (!halfbride) {
558 return p;
559 }
560 auto tmp = p;
561 for (auto &&v : tmp) {
562 p << qRgb(r: qRed(rgb: v) / 2, g: qGreen(rgb: v) / 2, b: qBlue(rgb: v) / 2);
563 }
564 return p;
565}
566
567bool CMAPChunk::innerReadStructure(QIODevice *d)
568{
569 return cacheData(d);
570}
571
572QList<QRgb> CMAPChunk::innerPalette() const
573{
574 QList<QRgb> l;
575 auto &&d = data();
576 for (qint32 i = 0, n = count(); i < n; ++i) {
577 auto i3 = i * 3;
578 l << qRgb(r: d.at(i: i3), g: d.at(i: i3 + 1), b: d.at(i: i3 + 2));
579 }
580 return l;
581}
582
583
584/* ******************
585 * *** CMYK Chunk ***
586 * ****************** */
587
588CMYKChunk::~CMYKChunk()
589{
590
591}
592
593CMYKChunk::CMYKChunk() : CMAPChunk()
594{
595
596}
597
598bool CMYKChunk::isValid() const
599{
600 return chunkId() == CMYKChunk::defaultChunkId();
601}
602
603qint32 CMYKChunk::count() const
604{
605 if (!isValid()) {
606 return 0;
607 }
608 return dataBytes() / 4;
609}
610
611QList<QRgb> CMYKChunk::innerPalette() const
612{
613 QList<QRgb> l;
614 auto &&d = data();
615 for (qint32 i = 0, n = count(); i < n; ++i) {
616 auto i4 = i * 4;
617 auto C = quint8(d.at(i: i4)) / 255.;
618 auto M = quint8(d.at(i: i4 + 1)) / 255.;
619 auto Y = quint8(d.at(i: i4 + 2)) / 255.;
620 auto K = quint8(d.at(i: i4 + 3)) / 255.;
621 l << QColor::fromCmykF(c: C, m: M, y: Y, k: K).toRgb().rgb();
622 }
623 return l;
624}
625
626
627/* ******************
628 * *** CAMG Chunk ***
629 * ****************** */
630
631CAMGChunk::~CAMGChunk()
632{
633
634}
635
636CAMGChunk::CAMGChunk() : IFFChunk()
637{
638}
639
640bool CAMGChunk::isValid() const
641{
642 if (dataBytes() != 4) {
643 return false;
644 }
645 return chunkId() == CAMGChunk::defaultChunkId();
646}
647
648CAMGChunk::ModeIds CAMGChunk::modeId() const
649{
650 if (!isValid()) {
651 return CAMGChunk::ModeIds();
652 }
653 return CAMGChunk::ModeIds(ui32(c1: data().at(i: 3), c2: data().at(i: 2), c3: data().at(i: 1), c4: data().at(i: 0)));
654}
655
656bool CAMGChunk::innerReadStructure(QIODevice *d)
657{
658 return cacheData(d);
659}
660
661/* ******************
662 * *** DPI Chunk ***
663 * ****************** */
664
665DPIChunk::~DPIChunk()
666{
667
668}
669
670DPIChunk::DPIChunk() : IFFChunk()
671{
672}
673
674bool DPIChunk::isValid() const
675{
676 if (dpiX() == 0 || dpiY() == 0) {
677 return false;
678 }
679 return chunkId() == DPIChunk::defaultChunkId();
680}
681
682quint16 DPIChunk::dpiX() const
683{
684 if (dataBytes() < 4) {
685 return 0;
686 }
687 return ui16(c1: data().at(i: 1), c2: data().at(i: 0));
688}
689
690quint16 DPIChunk::dpiY() const
691{
692 if (dataBytes() < 4) {
693 return 0;
694 }
695 return ui16(c1: data().at(i: 3), c2: data().at(i: 2));
696}
697
698qint32 DPIChunk::dotsPerMeterX() const
699{
700 return dpi2ppm(dpi: dpiX());
701}
702
703qint32 DPIChunk::dotsPerMeterY() const
704{
705 return dpi2ppm(dpi: dpiY());
706}
707
708bool DPIChunk::innerReadStructure(QIODevice *d)
709{
710 return cacheData(d);
711}
712
713/* ******************
714 * *** XBMI Chunk ***
715 * ****************** */
716
717XBMIChunk::~XBMIChunk()
718{
719
720}
721
722XBMIChunk::XBMIChunk() : DPIChunk()
723{
724}
725
726bool XBMIChunk::isValid() const
727{
728 if (dpiX() == 0 || dpiY() == 0) {
729 return false;
730 }
731 return chunkId() == XBMIChunk::defaultChunkId();
732}
733
734quint16 XBMIChunk::dpiX() const
735{
736 if (dataBytes() < 6) {
737 return 0;
738 }
739 return ui16(c1: data().at(i: 3), c2: data().at(i: 2));
740}
741
742quint16 XBMIChunk::dpiY() const
743{
744 if (dataBytes() < 6) {
745 return 0;
746 }
747 return ui16(c1: data().at(i: 5), c2: data().at(i: 4));
748}
749
750XBMIChunk::PictureType XBMIChunk::pictureType() const
751{
752 if (dataBytes() < 6) {
753 return PictureType(-1);
754 }
755 return PictureType(i16(c1: data().at(i: 1), c2: data().at(i: 0)));
756}
757
758/* ******************
759 * *** BODY Chunk ***
760 * ****************** */
761
762BODYChunk::~BODYChunk()
763{
764
765}
766
767BODYChunk::BODYChunk() : IFFChunk()
768{
769}
770
771bool BODYChunk::isValid() const
772{
773 return chunkId() == BODYChunk::defaultChunkId();
774}
775
776// For each RGB value, a LONG-word (32 bits) is written:
777// with the 24 RGB bits in the MSB positions; the "genlock"
778// bit next, and then a 7 bit repeat count.
779//
780// See also: https://wiki.amigaos.net/wiki/RGBN_and_RGB8_IFF_Image_Data
781inline qint64 rgb8Decompress(QIODevice *input, char *output, qint64 olen)
782{
783 qint64 j = 0;
784 for (qint64 available = olen; j < olen; available = olen - j) {
785 auto pos = input->pos();
786 auto ba4 = input->read(maxlen: 4);
787 if (ba4.size() != 4) {
788 break;
789 }
790 auto cnt = qint32(ba4.at(i: 3) & 0x7F);
791 if (cnt * 3 > available) {
792 if (!input->seek(pos))
793 return -1;
794 break;
795 }
796 for (qint32 i = 0; i < cnt; ++i) {
797 output[j++] = ba4.at(i: 0);
798 output[j++] = ba4.at(i: 1);
799 output[j++] = ba4.at(i: 2);
800 }
801 }
802 return j;
803}
804
805// For each RGB value, a WORD (16-bits) is written: with the
806// 12 RGB bits in the MSB (most significant bit) positions;
807// the "genlock" bit next; and then a 3 bit repeat count.
808// If the repeat count is greater than 7, the 3-bit count is
809// zero, and a BYTE repeat count follows. If the repeat count
810// is greater than 255, the BYTE count is zero, and a WORD
811// repeat count follows. Repeat counts greater than 65536 are
812// not supported.
813//
814// See also: https://wiki.amigaos.net/wiki/RGBN_and_RGB8_IFF_Image_Data
815inline qint32 rgbnCount(QIODevice *input, quint8 &R, quint8& G, quint8 &B)
816{
817 auto ba2 = input->read(maxlen: 2);
818 if (ba2.size() != 2)
819 return 0;
820
821 R = ba2.at(i: 0) & 0xF0;
822 R = R | (R >> 4);
823
824 G = ba2.at(i: 0) & 0x0F;
825 G = G | (G << 4);
826
827 B = ba2.at(i: 1) & 0xF0;
828 B = B | (B >> 4);
829
830 auto cnt = ba2.at(i: 1) & 7;
831 if (cnt == 0) {
832 auto ba1 = input->read(maxlen: 1);
833 if (ba1.size() != 1)
834 return 0;
835 cnt = quint8(ba1.at(i: 0));
836 }
837 if (cnt == 0) {
838 auto baw = input->read(maxlen: 2);
839 if (baw.size() != 2)
840 return 0;
841 cnt = qint32(quint8(baw.at(i: 0))) << 8 | quint8(baw.at(i: 1));
842 }
843
844 return cnt;
845}
846
847inline qint64 rgbNDecompress(QIODevice *input, char *output, qint64 olen)
848{
849 qint64 j = 0;
850 for (qint64 available = olen; j < olen; available = olen - j) {
851 quint8 R = 0, G = 0, B = 0;
852 auto pos = input->pos();
853 auto cnt = rgbnCount(input, R, G, B);
854 if (cnt * 3 > available || cnt == 0) {
855 if (!input->seek(pos))
856 return -1;
857 break;
858 }
859 for (qint32 i = 0; i < cnt; ++i) {
860 output[j++] = R;
861 output[j++] = G;
862 output[j++] = B;
863 }
864 }
865 return j;
866}
867
868
869inline qint64 vdatDecompress(const IFFChunk *chunk, const BMHDChunk *header, qint32 y, char *output, qint64 olen)
870{
871 auto vdats = IFFChunk::searchT<VDATChunk>(chunk);
872 auto rowLen = header->rowLen();
873 if (olen < rowLen * vdats.size()) {
874 return -1;
875 }
876 for (qint32 i = 0, n = vdats.size(); i < n; ++i) {
877 auto&& uc = vdats.at(i)->uncompressedData(header);
878 if (y * rowLen > uc.size() - rowLen)
879 return -1;
880 std::memcpy(dest: output + (i * rowLen), src: uc.data() + (y * rowLen), n: rowLen);
881 }
882 return vdats.size() * rowLen;
883}
884
885QByteArray BODYChunk::strideRead(QIODevice *d, qint32 y, const BMHDChunk *header, const CAMGChunk *camg, const CMAPChunk *cmap, const IPALChunk *ipal, const QByteArray& formType) const
886{
887 if (!isValid() || header == nullptr || d == nullptr) {
888 return {};
889 }
890
891 auto isRgbN = formType == RGBN_FORM_TYPE;
892 auto isRgb8 = formType == RGB8_FORM_TYPE;
893 auto isPbm = formType == PBM__FORM_TYPE;
894 auto lineCompressed = isRgbN || isRgb8 ? false : true;
895 auto readSize = strideSize(header, formType);
896 auto bufSize = readSize;
897 if (isRgbN) {
898 bufSize = std::max(a: quint32(65536 * 3), b: readSize);
899 }
900 if (isRgb8) {
901 bufSize = std::max(a: quint32(127 * 3), b: readSize);
902 }
903 for (auto nextPos = nextChunkPos(); !d->atEnd() && d->pos() < nextPos && _readBuffer.size() < readSize;) {
904 QByteArray buf(bufSize, char());
905 qint64 rr = -1;
906 if (header->compression() == BMHDChunk::Compression::Rle) {
907 // WARNING: The online spec says it's the same as TIFF but that's
908 // not accurate: the RLE -128 code is not a noop.
909 rr = packbitsDecompress(input: d, output: buf.data(), olen: buf.size(), allowN128: true);
910 } else if (header->compression() == BMHDChunk::Compression::Vdat) {
911 rr = vdatDecompress(chunk: this, header, y, output: buf.data(), olen: buf.size());
912 } else if (header->compression() == BMHDChunk::Compression::RgbN8) {
913 if (isRgb8)
914 rr = rgb8Decompress(input: d, output: buf.data(), olen: buf.size());
915 else if (isRgbN)
916 rr = rgbNDecompress(input: d, output: buf.data(), olen: buf.size());
917 } else if (header->compression() == BMHDChunk::Compression::Uncompressed) {
918 rr = d->read(data: buf.data(), maxlen: buf.size()); // never seen
919 } else {
920 qCDebug(LOG_IFFPLUGIN) << "BODYChunk::strideRead(): unknown compression" << header->compression();
921 }
922 if ((rr != readSize && lineCompressed) || (rr < 1))
923 return {};
924 _readBuffer.append(s: buf.data(), len: rr);
925 }
926
927 auto planes = _readBuffer.left(n: readSize);
928 _readBuffer.remove(index: 0, len: readSize);
929 if (isPbm) {
930 return pbm(planes, y, header, camg, cmap, ipal);
931 }
932 if (isRgb8) {
933 return rgb8(planes, y, header, camg, cmap, ipal);
934 }
935 if (isRgbN) {
936 return rgbN(planes, y, header, camg, cmap, ipal);
937 }
938 return deinterleave(planes, y, header, camg, cmap, ipal);
939}
940
941bool BODYChunk::resetStrideRead(QIODevice *d) const
942{
943 _readBuffer.clear();
944 return seek(d);
945}
946
947CAMGChunk::ModeIds BODYChunk::safeModeId(const BMHDChunk *header, const CAMGChunk *camg, const CMAPChunk *cmap)
948{
949 if (camg) {
950 return camg->modeId();
951 }
952 if (header == nullptr) {
953 return CAMGChunk::ModeIds();
954 }
955 auto cmapCount = cmap ? cmap->count() : 0;
956 auto bitplanes = header->bitplanes();
957 if (bitplanes >= BITPLANES_HALFBRIDE_MIN && bitplanes <= BITPLANES_HALFBRIDE_MAX) {
958 if (cmapCount == (1 << (bitplanes - 1)))
959 return CAMGChunk::ModeIds(CAMGChunk::ModeId::HalfBrite);
960 }
961 if (bitplanes >= BITPLANES_HAM_MIN && bitplanes <= BITPLANES_HAM_MAX) {
962 if (cmapCount == (1 << (bitplanes - 2)))
963 return CAMGChunk::ModeIds(CAMGChunk::ModeId::Ham);
964 }
965 return CAMGChunk::ModeIds();
966}
967
968quint32 BODYChunk::strideSize(const BMHDChunk *header, const QByteArray& formType) const
969{
970 // RGB8 / RGBN
971 if (formType == RGB8_FORM_TYPE || formType == RGBN_FORM_TYPE) {
972 return header->width() * 3;
973 }
974
975 // PBM
976 if (formType == PBM__FORM_TYPE) {
977 auto rs = header->width() * header->bitplanes() / 8;
978 if (rs & 1)
979 ++rs;
980 return rs;
981 }
982
983 // ILBM
984 auto sz = header->rowLen() * header->bitplanes();
985 if (header->masking() == BMHDChunk::Masking::HasMask)
986 sz += header->rowLen();
987 return sz;
988}
989
990QByteArray BODYChunk::pbm(const QByteArray &planes, qint32, const BMHDChunk *header, const CAMGChunk *, const CMAPChunk *, const IPALChunk *) const
991{
992 if (planes.size() != strideSize(header, PBM__FORM_TYPE)) {
993 return {};
994 }
995 if (header->bitplanes() == 8) {
996 // The data are contiguous.
997 return planes;
998 }
999 return {};
1000}
1001
1002QByteArray BODYChunk::rgb8(const QByteArray &planes, qint32, const BMHDChunk *header, const CAMGChunk *, const CMAPChunk *, const IPALChunk *) const
1003{
1004 if (planes.size() != strideSize(header, RGB8_FORM_TYPE)) {
1005 return {};
1006 }
1007 return planes;
1008}
1009
1010QByteArray BODYChunk::rgbN(const QByteArray &planes, qint32, const BMHDChunk *header, const CAMGChunk *, const CMAPChunk *, const IPALChunk *) const
1011{
1012 if (planes.size() != strideSize(header, RGBN_FORM_TYPE)) {
1013 return {};
1014 }
1015 return planes;
1016}
1017
1018bool BODYChunk::innerReadStructure(QIODevice *d)
1019{
1020 auto ok = true;
1021 if (d->peek(maxlen: 4) == VDAT_CHUNK) {
1022 setChunks(IFFChunk::innerFromDevice(d, ok: &ok, parent: this));
1023 }
1024 return ok;
1025}
1026
1027QByteArray BODYChunk::deinterleave(const QByteArray &planes, qint32 y, const BMHDChunk *header, const CAMGChunk *camg, const CMAPChunk *cmap, const IPALChunk *ipal) const
1028{
1029 if (planes.size() != strideSize(header, ILBM_FORM_TYPE)) {
1030 return {};
1031 }
1032
1033 auto rowLen = qint32(header->rowLen());
1034 auto bitplanes = header->bitplanes();
1035 auto modeId = BODYChunk::safeModeId(header, camg, cmap);
1036
1037 QByteArray ba;
1038 switch (bitplanes) {
1039 case 1: // gray, indexed and rgb Ham mode
1040 case 2:
1041 case 3:
1042 case 4:
1043 case 5:
1044 case 6:
1045 case 7:
1046 case 8:
1047 if ((modeId & CAMGChunk::ModeId::Ham) && (cmap) &&
1048 (bitplanes >= BITPLANES_HAM_MIN && bitplanes <= BITPLANES_HAM_MAX)) {
1049 // From A Quick Introduction to IFF.txt:
1050 //
1051 // Amiga HAM (Hold and Modify) mode lets the Amiga display all 4096 RGB values.
1052 // In HAM mode, the bits in the two last planes describe an R G or B
1053 // modification to the color of the previous pixel on the line to create the
1054 // color of the current pixel. So a 6-plane HAM picture has 4 planes for
1055 // specifying absolute color pixels giving up to 16 absolute colors which would
1056 // be specified in the ILBM CMAP chunk. The bits in the last two planes are
1057 // color modification bits which cause the Amiga, in HAM mode, to take the RGB
1058 // value of the previous pixel (Hold and), substitute the 4 bits in planes 0-3
1059 // for the previous color's R G or B component (Modify) and display the result
1060 // for the current pixel. If the first pixel of a scan line is a modification
1061 // pixel, it modifies the RGB value of the border color (register 0). The color
1062 // modification bits in the last two planes (planes 4 and 5) are interpreted as
1063 // follows:
1064 // 00 - no modification. Use planes 0-3 as normal color register index
1065 // 10 - hold previous, replacing Blue component with bits from planes 0-3
1066 // 01 - hold previous, replacing Red component with bits from planes 0-3
1067 // 11 - hold previous. replacing Green component with bits from planes 0-3
1068 ba = QByteArray(rowLen * 8 * 3, char());
1069 auto pal = cmap->palette();
1070 if (ipal) {
1071 auto tmp = ipal->palette(y);
1072 if (!tmp.isEmpty())
1073 pal = tmp;
1074 }
1075 // HAM 6: 2 control bits+4 bits of data, 16-color palette
1076 //
1077 // HAM 8: 2 control bits+6 bits of data, 64-color palette
1078 //
1079 // HAM 5: 1 control bit (and 1 hardwired to zero)+4 bits of data
1080 // (red and green modify operations are unavailable)
1081 auto ctlbits = bitplanes > 5 ? 2 : 1;
1082 auto max = (1 << (bitplanes - ctlbits)) - 1;
1083 quint8 prev[3] = {};
1084 for (qint32 i = 0, cnt = 0; i < rowLen; ++i) {
1085 for (qint32 j = 0; j < 8; ++j, ++cnt) {
1086 quint8 idx = 0, ctl = 0;
1087 for (qint32 k = 0, msk = (1 << (7 - j)); k < bitplanes; ++k) {
1088 if ((planes.at(i: k * rowLen + i) & msk) == 0)
1089 continue;
1090 if (k < bitplanes - ctlbits)
1091 idx |= 1 << k;
1092 else
1093 ctl |= 1 << (bitplanes - k - 1);
1094 }
1095 if (ctl && ctlbits == 1) {
1096 ctl <<= 1; // HAM 5 has only 1 control bit and the LSB is always 0
1097 }
1098 switch (ctl) {
1099 case 1: // red
1100 prev[0] = idx * 255 / max;
1101 break;
1102 case 2: // blue
1103 prev[2] = idx * 255 / max;
1104 break;
1105 case 3: // green
1106 prev[1] = idx * 255 / max;
1107 break;
1108 default:
1109 if (idx < pal.size()) {
1110 prev[0] = qRed(rgb: pal.at(i: idx));
1111 prev[1] = qGreen(rgb: pal.at(i: idx));
1112 prev[2] = qBlue(rgb: pal.at(i: idx));
1113 } else {
1114 qCWarning(LOG_IFFPLUGIN) << "BODYChunk::deinterleave(): palette index" << idx << "is out of range";
1115 }
1116 break;
1117 }
1118 auto cnt3 = cnt * 3;
1119 ba[cnt3] = char(prev[0]);
1120 ba[cnt3 + 1] = char(prev[1]);
1121 ba[cnt3 + 2] = char(prev[2]);
1122 }
1123 }
1124 } else if ((modeId & CAMGChunk::ModeId::HalfBrite) && (cmap) &&
1125 (bitplanes >= BITPLANES_HALFBRIDE_MIN && bitplanes <= BITPLANES_HALFBRIDE_MAX)) {
1126 // From A Quick Introduction to IFF.txt:
1127 //
1128 // In HALFBRITE mode, the Amiga interprets the bit in the
1129 // last plane as HALFBRITE modification. The bits in the other planes are
1130 // treated as normal color register numbers (RGB values for each color register
1131 // is specified in the CMAP chunk). If the bit in the last plane is set (1),
1132 // then that pixel is displayed at half brightness. This can provide up to 64
1133 // absolute colors.
1134 ba = QByteArray(rowLen * 8, char());
1135 auto palSize = cmap->count();
1136 for (qint32 i = 0, cnt = 0; i < rowLen; ++i) {
1137 for (qint32 j = 0; j < 8; ++j, ++cnt) {
1138 quint8 idx = 0, ctl = 0;
1139 for (qint32 k = 0, msk = (1 << (7 - j)); k < bitplanes; ++k) {
1140 if ((planes.at(i: k * rowLen + i) & msk) == 0)
1141 continue;
1142 if (k < bitplanes - 1)
1143 idx |= 1 << k;
1144 else
1145 ctl = 1;
1146 }
1147 if (idx < palSize) {
1148 ba[cnt] = ctl ? idx + palSize : idx;
1149 } else {
1150 qCWarning(LOG_IFFPLUGIN) << "BODYChunk::deinterleave(): palette index" << idx << "is out of range";
1151 }
1152 }
1153 }
1154 } else {
1155 // From A Quick Introduction to IFF.txt:
1156 //
1157 // If the ILBM is not HAM or HALFBRITE, then after parsing and uncompacting if
1158 // necessary, you will have N planes of pixel data. Color register used for
1159 // each pixel is specified by looking at each pixel thru the planes. I.e.,
1160 // if you have 5 planes, and the bit for a particular pixel is set in planes
1161 // 0 and 3:
1162 //
1163 // PLANE 4 3 2 1 0
1164 // PIXEL 0 1 0 0 1
1165 //
1166 // then that pixel uses color register binary 01001 = 9
1167 ba = QByteArray(rowLen * 8, char());
1168 for (qint32 i = 0; i < rowLen; ++i) {
1169 for (qint32 k = 0, i8 = i * 8; k < bitplanes; ++k) {
1170 auto v = planes.at(i: k * rowLen + i);
1171 auto msk = 1 << k;
1172 if (v & (1 << 7))
1173 ba[i8] |= msk;
1174 if (v & (1 << 6))
1175 ba[i8 + 1] |= msk;
1176 if (v & (1 << 5))
1177 ba[i8 + 2] |= msk;
1178 if (v & (1 << 4))
1179 ba[i8 + 3] |= msk;
1180 if (v & (1 << 3))
1181 ba[i8 + 4] |= msk;
1182 if (v & (1 << 2))
1183 ba[i8 + 5] |= msk;
1184 if (v & (1 << 1))
1185 ba[i8 + 6] |= msk;
1186 if (v & 1)
1187 ba[i8 + 7] |= msk;
1188 }
1189 }
1190 }
1191 break;
1192
1193 case 24: // rgb
1194 case 32: // rgba (SView5 extension)
1195 // From A Quick Introduction to IFF.txt:
1196 //
1197 // If a deep ILBM (like 12 or 24 planes), there should be no CMAP
1198 // and instead the BODY planes are interpreted as the bits of RGB
1199 // in the order R0...Rn G0...Gn B0...Bn
1200 //
1201 // NOTE: This code does not support 12-planes images
1202 ba = QByteArray(rowLen * bitplanes, char());
1203 for (qint32 i = 0, cnt = 0, p = bitplanes / 8; i < rowLen; ++i) {
1204 for (qint32 j = 0; j < 8; ++j)
1205 for (qint32 k = 0; k < p; ++k, ++cnt) {
1206 auto k8 = k * 8;
1207 auto msk = (1 << (7 - j));
1208 if (planes.at(i: k8 * rowLen + i) & msk)
1209 ba[cnt] |= 0x01;
1210 if (planes.at(i: (1 + k8) * rowLen + i) & msk)
1211 ba[cnt] |= 0x02;
1212 if (planes.at(i: (2 + k8) * rowLen + i) & msk)
1213 ba[cnt] |= 0x04;
1214 if (planes.at(i: (3 + k8) * rowLen + i) & msk)
1215 ba[cnt] |= 0x08;
1216 if (planes.at(i: (4 + k8) * rowLen + i) & msk)
1217 ba[cnt] |= 0x10;
1218 if (planes.at(i: (5 + k8) * rowLen + i) & msk)
1219 ba[cnt] |= 0x20;
1220 if (planes.at(i: (6 + k8) * rowLen + i) & msk)
1221 ba[cnt] |= 0x40;
1222 if (planes.at(i: (7 + k8) * rowLen + i) & msk)
1223 ba[cnt] |= 0x80;
1224 }
1225 }
1226 break;
1227
1228 case 48: // rgb (SView5 extension)
1229 case 64: // rgba (SView5 extension)
1230 // From https://aminet.net/package/docs/misc/ILBM64:
1231 //
1232 // Previously, the IFF-ILBM fileformat has been
1233 // extended two times already, for 24 bit and 32 bit
1234 // image data:
1235 //
1236 // 24 bit -> 24 planes composing RGB 8:8:8 true color
1237 // 32 bit -> 32 planes composing RGBA 8:8:8:8 true color
1238 // plus alpha
1239 //
1240 // The former extension quickly became a common one,
1241 // while the latter until recently mainly had been
1242 // used by some NewTek software.
1243 //
1244 // Now the following - as a consequent logical extension
1245 // of the previously mentioned definitions - is introduced
1246 // by SView5-Library:
1247 //
1248 // 48 bit -> 48 planes composing RGB 16:16:16 true color
1249 // 64 bit -> 64 planes composing RGBA 16:16:16:16 true color
1250 // plus alpha
1251 //
1252 // The resulting data is intended to allow direct transformation
1253 // from the PNG format into the Amiga (ILBM) bitmap format.
1254
1255 ba = QByteArray(rowLen * 64, char()); // the RGBX QT format is 64-bits
1256 const qint32 order[] = { 1, 0, 3, 2, 5, 4, 7, 6 };
1257 for (qint32 i = 0, cnt = 0, p = bitplanes / 8; i < rowLen; ++i) {
1258 for (qint32 j = 0; j < 8; ++j, cnt += 8) {
1259 for (qint32 k = 0; k < p; ++k) {
1260 auto k8 = k * 8;
1261 auto msk = (1 << (7 - j));
1262 auto idx = cnt + order[k];
1263 if (planes.at(i: k8 * rowLen + i) & msk)
1264 ba[idx] |= 0x01;
1265 if (planes.at(i: (1 + k8) * rowLen + i) & msk)
1266 ba[idx] |= 0x02;
1267 if (planes.at(i: (2 + k8) * rowLen + i) & msk)
1268 ba[idx] |= 0x04;
1269 if (planes.at(i: (3 + k8) * rowLen + i) & msk)
1270 ba[idx] |= 0x08;
1271 if (planes.at(i: (4 + k8) * rowLen + i) & msk)
1272 ba[idx] |= 0x10;
1273 if (planes.at(i: (5 + k8) * rowLen + i) & msk)
1274 ba[idx] |= 0x20;
1275 if (planes.at(i: (6 + k8) * rowLen + i) & msk)
1276 ba[idx] |= 0x40;
1277 if (planes.at(i: (7 + k8) * rowLen + i) & msk)
1278 ba[idx] |= 0x80;
1279 }
1280 if (p == 6) { // RGBX wants unused X data set to 0xFF
1281 ba[cnt + 6] = char(0xFF);
1282 ba[cnt + 7] = char(0xFF);
1283 }
1284 }
1285 }
1286 break;
1287 }
1288 return ba;
1289}
1290
1291/* ******************
1292 * *** ABIT Chunk ***
1293 * ****************** */
1294
1295ABITChunk::~ABITChunk()
1296{
1297
1298}
1299
1300ABITChunk::ABITChunk() : BODYChunk()
1301{
1302
1303}
1304
1305bool ABITChunk::isValid() const
1306{
1307 return chunkId() == ABITChunk::defaultChunkId();
1308}
1309
1310QByteArray ABITChunk::strideRead(QIODevice *d, qint32 y, const BMHDChunk *header, const CAMGChunk *camg, const CMAPChunk *cmap, const IPALChunk *ipal, const QByteArray& formType) const
1311{
1312 if (!isValid() || header == nullptr || d == nullptr) {
1313 return {};
1314 }
1315 if (header->compression() != BMHDChunk::Compression::Uncompressed || formType != ACBM_FORM_TYPE) {
1316 return {};
1317 }
1318
1319 // convert ABIT data to an ILBM line on the fly
1320 auto ilbmLine = QByteArray(strideSize(header, formType), char());
1321 auto rowSize = header->rowLen();
1322 auto height = header->height();
1323 if (y >= height) {
1324 return {};
1325 }
1326 for (qint32 plane = 0, planes = qint32(header->bitplanes()); plane < planes; ++plane) {
1327 if (!seek(d, relPos: qint64(plane) * rowSize * height + y * rowSize))
1328 return {};
1329 auto offset = qint64(plane) * rowSize;
1330 if (offset + rowSize > ilbmLine.size())
1331 return {};
1332 if (d->read(data: ilbmLine.data() + offset, maxlen: rowSize) != rowSize)
1333 return {};
1334 }
1335
1336 // decode the ILBM line
1337 QBuffer buf;
1338 buf.setData(ilbmLine);
1339 if (!buf.open(openMode: QBuffer::ReadOnly)) {
1340 return {};
1341 }
1342 return BODYChunk::strideRead(d: &buf, y, header, camg, cmap, ipal, ILBM_FORM_TYPE);
1343}
1344
1345bool ABITChunk::resetStrideRead(QIODevice *d) const
1346{
1347 return BODYChunk::resetStrideRead(d);
1348}
1349
1350
1351/* **********************
1352 * *** FORM Interface ***
1353 * ********************** */
1354
1355IFOR_Chunk::~IFOR_Chunk()
1356{
1357
1358}
1359
1360IFOR_Chunk::IFOR_Chunk() : IFFChunk()
1361{
1362
1363}
1364
1365QImageIOHandler::Transformation IFOR_Chunk::transformation() const
1366{
1367 auto exifs = IFFChunk::searchT<EXIFChunk>(chunks: chunks());
1368 if (!exifs.isEmpty()) {
1369 auto exif = exifs.first()->value();
1370 if (!exif.isEmpty())
1371 return exif.transformation();
1372 }
1373 return QImageIOHandler::Transformation::TransformationNone;
1374}
1375
1376QImage::Format IFOR_Chunk::optionformat() const
1377{
1378 auto fmt = this->format();
1379 if (fmt == QImage::Format_Indexed8) {
1380 if (auto ipal = searchIPal()) {
1381 fmt = ipal->hasAlpha() ? FORMAT_RGBA_8BIT : FORMAT_RGB_8BIT;
1382 }
1383 }
1384 return fmt;
1385}
1386
1387const IPALChunk *IFOR_Chunk::searchIPal() const
1388{
1389 const IPALChunk *ipal = nullptr;
1390 auto beam = IFFChunk::searchT<BEAMChunk>(chunk: this);
1391 if (!beam.isEmpty()) {
1392 ipal = beam.first();
1393 }
1394 auto ctbl = IFFChunk::searchT<CTBLChunk>(chunk: this);
1395 if (!ctbl.isEmpty()) {
1396 ipal = ctbl.first();
1397 }
1398 auto sham = IFFChunk::searchT<SHAMChunk>(chunk: this);
1399 if (!sham.isEmpty()) {
1400 ipal = sham.first();
1401 }
1402 auto rast = IFFChunk::searchT<RASTChunk>(chunk: this);
1403 if (!rast.isEmpty()) {
1404 ipal = rast.first();
1405 }
1406 auto pchg = IFFChunk::searchT<PCHGChunk>(chunk: this);
1407 if (!pchg.isEmpty()) {
1408 ipal = pchg.first();
1409 }
1410 if (ipal && ipal->isValid()) {
1411 return ipal;
1412 }
1413 return nullptr;
1414}
1415
1416
1417/* ******************
1418 * *** FORM Chunk ***
1419 * ****************** */
1420
1421FORMChunk::~FORMChunk()
1422{
1423
1424}
1425
1426FORMChunk::FORMChunk() : IFOR_Chunk()
1427{
1428}
1429
1430bool FORMChunk::isValid() const
1431{
1432 return chunkId() == FORMChunk::defaultChunkId();
1433}
1434
1435bool FORMChunk::isSupported() const
1436{
1437 return format() != QImage::Format_Invalid;
1438}
1439
1440bool FORMChunk::innerReadStructure(QIODevice *d)
1441{
1442 if (bytes() < 4) {
1443 return false;
1444 }
1445 _type = d->read(maxlen: 4);
1446 auto ok = true;
1447
1448 // NOTE: add new supported type to CATChunk as well.
1449 if (_type == ILBM_FORM_TYPE) {
1450 setChunks(IFFChunk::innerFromDevice(d, ok: &ok, parent: this));
1451 } else if (_type == PBM__FORM_TYPE) {
1452 setChunks(IFFChunk::innerFromDevice(d, ok: &ok, parent: this));
1453 } else if (_type == ACBM_FORM_TYPE) {
1454 setChunks(IFFChunk::innerFromDevice(d, ok: &ok, parent: this));
1455 } else if (_type == RGB8_FORM_TYPE) {
1456 setChunks(IFFChunk::innerFromDevice(d, ok: &ok, parent: this));
1457 } else if (_type == RGBN_FORM_TYPE) {
1458 setChunks(IFFChunk::innerFromDevice(d, ok: &ok, parent: this));
1459 } else if (_type == IMAG_FORM_TYPE) {
1460 setChunks(IFFChunk::innerFromDevice(d, ok: &ok, parent: this));
1461 } else if (_type == RGFX_FORM_TYPE) {
1462 setChunks(IFFChunk::innerFromDevice(d, ok: &ok, parent: this));
1463 }
1464 return ok;
1465}
1466
1467QImage::Format FORMChunk::iffFormat() const
1468{
1469 auto headers = IFFChunk::searchT<BMHDChunk>(chunks: chunks());
1470 if (headers.isEmpty()) {
1471 return QImage::Format_Invalid;
1472 }
1473
1474 if (auto &&h = headers.first()) {
1475 auto cmaps = IFFChunk::searchT<CMAPChunk>(chunks: chunks());
1476 if (cmaps.isEmpty()) {
1477 auto cmyks = IFFChunk::searchT<CMYKChunk>(chunks: chunks());
1478 for (auto &&cmyk : cmyks)
1479 cmaps.append(t: cmyk);
1480 }
1481 auto camgs = IFFChunk::searchT<CAMGChunk>(chunks: chunks());
1482 auto modeId = BODYChunk::safeModeId(header: h, camg: camgs.isEmpty() ? nullptr : camgs.first(), cmap: cmaps.isEmpty() ? nullptr : cmaps.first());
1483 if (h->bitplanes() == 13) {
1484 return FORMAT_RGB_8BIT; // NOTE: with a little work you could use Format_RGB444
1485 }
1486 if (h->bitplanes() == 24 || h->bitplanes() == 25) {
1487 return FORMAT_RGB_8BIT;
1488 }
1489 if (h->bitplanes() == 48) {
1490 return QImage::Format_RGBX64;
1491 }
1492 if (h->bitplanes() == 32) {
1493 return QImage::Format_RGBA8888;
1494 }
1495 if (h->bitplanes() == 64) {
1496 return QImage::Format_RGBA64;
1497 }
1498 if (h->bitplanes() >= 1 && h->bitplanes() <= 8) {
1499 if (h->bitplanes() >= BITPLANES_HAM_MIN && h->bitplanes() <= BITPLANES_HAM_MAX) {
1500 if (modeId & CAMGChunk::ModeId::Ham)
1501 return FORMAT_RGB_8BIT;
1502 }
1503
1504 if (!cmaps.isEmpty()) {
1505 return QImage::Format_Indexed8;
1506 }
1507
1508 return QImage::Format_Grayscale8;
1509 }
1510 qCDebug(LOG_IFFPLUGIN) << "FORMChunk::format(): Unsupported" << h->bitplanes() << "bitplanes";
1511 }
1512
1513 return QImage::Format_Invalid;
1514}
1515
1516QImage::Format FORMChunk::cdiFormat() const
1517{
1518 auto headers = IFFChunk::searchT<IHDRChunk>(chunks: chunks());
1519 if (headers.isEmpty()) {
1520 return QImage::Format_Invalid;
1521 }
1522
1523 if (auto &&h = headers.first()) {
1524 if (h->model() == IHDRChunk::Rgb555 && h->depth() == 16) { // no test case
1525 return QImage::Format_RGB555;
1526 }
1527
1528 if (h->depth() == 4) {
1529 if (h->model() == IHDRChunk::CLut4 || h->model() == IHDRChunk::CLut3) { // CLut3: no test case
1530 return QImage::Format_Indexed8;
1531 }
1532 }
1533
1534 if (h->depth() == 8) {
1535 if (h->model() == IHDRChunk::CLut8 || h->model() == IHDRChunk::CLut7 || h->model() == IHDRChunk::Rle7) {
1536 return QImage::Format_Indexed8;
1537 }
1538 if (h->model() == IHDRChunk::Rgb888) { // no test case
1539 return FORMAT_RGB_8BIT;
1540 }
1541 if (h->model() == IHDRChunk::DYuv) {
1542 return FORMAT_RGB_8BIT;
1543 }
1544 }
1545 }
1546
1547 return QImage::Format_Invalid;
1548}
1549
1550QImage::Format FORMChunk::rgfxFormat() const
1551{
1552 auto headers = IFFChunk::searchT<RGHDChunk>(chunks: chunks());
1553 if (headers.isEmpty()) {
1554 return QImage::Format_Invalid;
1555 }
1556
1557 if (auto &&h = headers.first()) {
1558 auto rgfx_format = RGHDChunk::BitmapTypes(h->bitmapType() & 0x3FFFFFFF);
1559 if (rgfx_format == RGHDChunk::BitmapType::Chunky8 || rgfx_format == RGHDChunk::BitmapType::Planar8)
1560 return QImage::Format_Indexed8;
1561 if (rgfx_format == RGHDChunk::BitmapType::Rgb15 || rgfx_format == RGHDChunk::BitmapType::Rgb16)
1562 return QImage::Format_RGB555; // NOTE: RGB16 ignoring alpha due to missing compatible Qt format
1563 if (rgfx_format == RGHDChunk::BitmapType::Rgb24)
1564 return FORMAT_RGB_8BIT;
1565 if (rgfx_format == RGHDChunk::BitmapType::Rgb32)
1566 return FORMAT_RGBA_8BIT;
1567 if (rgfx_format == RGHDChunk::BitmapType::Rgb48)
1568 return QImage::Format_RGBX64;
1569 if (rgfx_format == RGHDChunk::BitmapType::Rgb64)
1570 return QImage::Format_RGBA64;
1571 if (rgfx_format == RGHDChunk::BitmapType::Rgb96)
1572 return QImage::Format_RGBX32FPx4;
1573 if (rgfx_format == RGHDChunk::BitmapType::Rgb128)
1574 return QImage::Format_RGBA32FPx4;
1575 }
1576
1577 return QImage::Format_Invalid;
1578}
1579
1580QByteArray FORMChunk::formType() const
1581{
1582 return _type;
1583}
1584
1585QImage::Format FORMChunk::format() const
1586{
1587 if (formType() == IMAG_FORM_TYPE) {
1588 return cdiFormat();
1589 } else if (formType() == RGFX_FORM_TYPE) {
1590 return rgfxFormat();
1591 }
1592 return iffFormat();
1593}
1594
1595QSize FORMChunk::size() const
1596{
1597 if (formType() == IMAG_FORM_TYPE) {
1598 auto ihdrs = IFFChunk::searchT<IHDRChunk>(chunks: chunks());
1599 if (!ihdrs.isEmpty()) {
1600 return ihdrs.first()->size();
1601 }
1602 } else if (formType() == RGFX_FORM_TYPE) {
1603 auto rghds = IFFChunk::searchT<RGHDChunk>(chunks: chunks());
1604 if (!rghds.isEmpty()) {
1605 return rghds.first()->size();
1606 }
1607 } else {
1608 auto bmhds = IFFChunk::searchT<BMHDChunk>(chunks: chunks());
1609 if (!bmhds.isEmpty()) {
1610 return bmhds.first()->size();
1611 }
1612 }
1613 return {};
1614}
1615
1616/* ******************
1617 * *** FOR4 Chunk ***
1618 * ****************** */
1619
1620FOR4Chunk::~FOR4Chunk()
1621{
1622
1623}
1624
1625FOR4Chunk::FOR4Chunk() : IFOR_Chunk()
1626{
1627
1628}
1629
1630bool FOR4Chunk::isValid() const
1631{
1632 return chunkId() == FOR4Chunk::defaultChunkId();
1633}
1634
1635qint32 FOR4Chunk::alignBytes() const
1636{
1637 return 4;
1638}
1639
1640bool FOR4Chunk::isSupported() const
1641{
1642 return format() != QImage::Format_Invalid;
1643}
1644
1645bool FOR4Chunk::innerReadStructure(QIODevice *d)
1646{
1647 if (bytes() < 4) {
1648 return false;
1649 }
1650 _type = d->read(maxlen: 4);
1651 auto ok = true;
1652 if (_type == CIMG_FOR4_TYPE) {
1653 setChunks(IFFChunk::innerFromDevice(d, ok: &ok, parent: this));
1654 } else if (_type == TBMP_FOR4_TYPE) {
1655 setChunks(IFFChunk::innerFromDevice(d, ok: &ok, parent: this));
1656 }
1657 return ok;
1658}
1659
1660QByteArray FOR4Chunk::formType() const
1661{
1662 return _type;
1663}
1664
1665QImage::Format FOR4Chunk::format() const
1666{
1667 auto headers = IFFChunk::searchT<TBHDChunk>(chunks: chunks());
1668 if (headers.isEmpty()) {
1669 return QImage::Format_Invalid;
1670 }
1671 return headers.first()->format();
1672}
1673
1674QSize FOR4Chunk::size() const
1675{
1676 auto headers = IFFChunk::searchT<TBHDChunk>(chunks: chunks());
1677 if (headers.isEmpty()) {
1678 return {};
1679 }
1680 return headers.first()->size();
1681}
1682
1683/* ******************
1684 * *** CAT Chunk ***
1685 * ****************** */
1686
1687CATChunk::~CATChunk()
1688{
1689
1690}
1691
1692CATChunk::CATChunk() : IFFChunk()
1693{
1694
1695}
1696
1697bool CATChunk::isValid() const
1698{
1699 return chunkId() == CATChunk::defaultChunkId();
1700}
1701
1702QByteArray CATChunk::catType() const
1703{
1704 return _type;
1705}
1706
1707bool CATChunk::innerReadStructure(QIODevice *d)
1708{
1709 if (bytes() < 4) {
1710 return false;
1711 }
1712 _type = d->read(maxlen: 4);
1713 auto ok = true;
1714
1715 // supports the image formats of FORMChunk.
1716 if (_type == ILBM_FORM_TYPE) {
1717 setChunks(IFFChunk::innerFromDevice(d, ok: &ok, parent: this));
1718 } else if (_type == PBM__FORM_TYPE) {
1719 setChunks(IFFChunk::innerFromDevice(d, ok: &ok, parent: this));
1720 } else if (_type == ACBM_FORM_TYPE) {
1721 setChunks(IFFChunk::innerFromDevice(d, ok: &ok, parent: this));
1722 } else if (_type == RGB8_FORM_TYPE) {
1723 setChunks(IFFChunk::innerFromDevice(d, ok: &ok, parent: this));
1724 } else if (_type == RGBN_FORM_TYPE) {
1725 setChunks(IFFChunk::innerFromDevice(d, ok: &ok, parent: this));
1726 } else if (_type == IMAG_FORM_TYPE) {
1727 setChunks(IFFChunk::innerFromDevice(d, ok: &ok, parent: this));
1728 } else if (_type == RGFX_FORM_TYPE) {
1729 setChunks(IFFChunk::innerFromDevice(d, ok: &ok, parent: this));
1730 }
1731 return ok;
1732}
1733
1734/* ******************
1735 * *** TBHD Chunk ***
1736 * ****************** */
1737
1738TBHDChunk::~TBHDChunk()
1739{
1740
1741}
1742
1743TBHDChunk::TBHDChunk()
1744{
1745
1746}
1747
1748bool TBHDChunk::isValid() const
1749{
1750 if (dataBytes() != 24 && dataBytes() != 32) {
1751 return false;
1752 }
1753 return chunkId() == TBHDChunk::defaultChunkId();
1754}
1755
1756qint32 TBHDChunk::alignBytes() const
1757{
1758 return 4;
1759}
1760
1761qint32 TBHDChunk::width() const
1762{
1763 if (!isValid()) {
1764 return 0;
1765 }
1766 return i32(c1: data().at(i: 3), c2: data().at(i: 2), c3: data().at(i: 1), c4: data().at(i: 0));
1767}
1768
1769qint32 TBHDChunk::height() const
1770{
1771 if (!isValid()) {
1772 return 0;
1773 }
1774 return i32(c1: data().at(i: 7), c2: data().at(i: 6), c3: data().at(i: 5), c4: data().at(i: 4));
1775}
1776
1777QSize TBHDChunk::size() const
1778{
1779 return QSize(width(), height());
1780}
1781
1782qint32 TBHDChunk::left() const
1783{
1784 if (dataBytes() != 32) {
1785 return 0;
1786 }
1787 return i32(c1: data().at(i: 27), c2: data().at(i: 26), c3: data().at(i: 25), c4: data().at(i: 24));
1788}
1789
1790qint32 TBHDChunk::top() const
1791{
1792 if (dataBytes() != 32) {
1793 return 0;
1794 }
1795 return i32(c1: data().at(i: 31), c2: data().at(i: 30), c3: data().at(i: 29), c4: data().at(i: 28));
1796}
1797
1798TBHDChunk::Flags TBHDChunk::flags() const
1799{
1800 if (!isValid()) {
1801 return TBHDChunk::Flags();
1802 }
1803 return TBHDChunk::Flags(ui32(c1: data().at(i: 15), c2: data().at(i: 14), c3: data().at(i: 13), c4: data().at(i: 12)));
1804}
1805
1806qint32 TBHDChunk::bpc() const
1807{
1808 if (!isValid()) {
1809 return 0;
1810 }
1811 return ui16(c1: data().at(i: 17), c2: data().at(i: 16)) ? 2 : 1;
1812}
1813
1814qint32 TBHDChunk::channels() const
1815{
1816 if ((flags() & TBHDChunk::Flag::RgbA) == TBHDChunk::Flag::RgbA) {
1817 return 4;
1818 }
1819 if ((flags() & TBHDChunk::Flag::Rgb) == TBHDChunk::Flag::Rgb) {
1820 return 3;
1821 }
1822 return 0;
1823}
1824
1825quint16 TBHDChunk::tiles() const
1826{
1827 if (!isValid()) {
1828 return 0;
1829 }
1830 return ui16(c1: data().at(i: 19), c2: data().at(i: 18));
1831}
1832
1833QImage::Format TBHDChunk::format() const
1834{
1835 // Support for RGBA and RGB only for now.
1836 if ((flags() & TBHDChunk::Flag::RgbA) == TBHDChunk::Flag::RgbA) {
1837 if (bpc() == 2)
1838 return QImage::Format_RGBA64;
1839 else if (bpc() == 1)
1840 return QImage::Format_RGBA8888;
1841 } else if ((flags() & TBHDChunk::Flag::Rgb) == TBHDChunk::Flag::Rgb) {
1842 if (bpc() == 2)
1843 return QImage::Format_RGBX64;
1844 else if (bpc() == 1)
1845 return FORMAT_RGB_8BIT;
1846 }
1847
1848 return QImage::Format_Invalid;
1849}
1850
1851TBHDChunk::Compression TBHDChunk::compression() const
1852{
1853 if (!isValid()) {
1854 return TBHDChunk::Compression::Uncompressed;
1855 }
1856 return TBHDChunk::Compression(ui32(c1: data().at(i: 23), c2: data().at(i: 22), c3: data().at(i: 21), c4: data().at(i: 20)));
1857}
1858
1859bool TBHDChunk::innerReadStructure(QIODevice *d)
1860{
1861 return cacheData(d);
1862}
1863
1864/* ******************
1865 * *** RGBA Chunk ***
1866 * ****************** */
1867
1868RGBAChunk::~RGBAChunk()
1869{
1870}
1871
1872RGBAChunk::RGBAChunk()
1873{
1874
1875}
1876
1877bool RGBAChunk::isValid() const
1878{
1879 if (bytes() < 8) {
1880 return false;
1881 }
1882 return chunkId() == RGBAChunk::defaultChunkId();
1883}
1884
1885qint32 RGBAChunk::alignBytes() const
1886{
1887 return 4;
1888}
1889
1890bool RGBAChunk::isTileCompressed(const TBHDChunk *header) const
1891{
1892 if (!isValid() || header == nullptr) {
1893 return false;
1894 }
1895 return qint64(header->channels()) * size().width() * size().height() * header->bpc() > qint64(bytes() - 8);
1896}
1897
1898QPoint RGBAChunk::pos() const
1899{
1900 return _posPx;
1901}
1902
1903QSize RGBAChunk::size() const
1904{
1905 return _sizePx;
1906}
1907
1908// Maya version of IFF uses a slightly different algorithm for RLE compression.
1909// To understand how it works I saved images with regular patterns from Photoshop
1910// and then checked the data. It is basically the same as packbits except for how
1911// the length is extracted: I don't know if it's a standard variant or not, so
1912// I'm keeping it private.
1913inline qint64 rleMayaDecompress(QIODevice *input, char *output, qint64 olen)
1914{
1915 qint64 j = 0;
1916 for (qint64 rr = 0, available = olen; j < olen; available = olen - j) {
1917 char n;
1918
1919 // check the output buffer space for the next run
1920 if (available < 128) {
1921 if (input->peek(data: &n, maxlen: 1) != 1) { // end of data (or error)
1922 break;
1923 }
1924 rr = qint64(n & 0x7F) + 1;
1925 if (rr > available)
1926 break;
1927 }
1928
1929 // decompress
1930 if (input->read(data: &n, maxlen: 1) != 1) { // end of data (or error)
1931 break;
1932 }
1933
1934 rr = qint64(n & 0x7F) + 1;
1935 if ((n & 0x80) == 0) {
1936 auto read = input->read(data: output + j, maxlen: rr);
1937 if (rr != read) {
1938 return -1;
1939 }
1940 } else {
1941 char b;
1942 if (input->read(data: &b, maxlen: 1) != 1) {
1943 break;
1944 }
1945 std::memset(s: output + j, c: b, n: size_t(rr));
1946 }
1947
1948 j += rr;
1949 }
1950 return j;
1951}
1952
1953QByteArray RGBAChunk::readStride(QIODevice *d, const TBHDChunk *header) const
1954{
1955 auto readSize = size().width();
1956 if (readSize == 0) {
1957 return {};
1958 }
1959
1960 // It seems that tiles are compressed independently only if there is space savings.
1961 // The compression method specified in the header is only to indicate the type of
1962 // compression if used.
1963 if (!isTileCompressed(header)) {
1964 // when not compressed, the line contains all channels
1965 readSize *= header->bpc() * header->channels();
1966 QByteArray buf(readSize, char());
1967 auto rr = d->read(data: buf.data(), maxlen: buf.size());
1968 if (rr != buf.size()) {
1969 return {};
1970 }
1971 return buf;
1972 }
1973
1974 // compressed
1975 for (auto nextPos = nextChunkPos(); !d->atEnd() && d->pos() < nextPos && _readBuffer.size() < readSize;) {
1976 QByteArray buf(readSize * size().height(), char());
1977 qint64 rr = -1;
1978 if (header->compression() == TBHDChunk::Compression::Rle) {
1979 rr = rleMayaDecompress(input: d, output: buf.data(), olen: buf.size());
1980 }
1981 if (rr != buf.size()) {
1982 return {};
1983 }
1984 _readBuffer.append(s: buf.data(), len: rr);
1985 }
1986
1987 auto buff = _readBuffer.left(n: readSize);
1988 _readBuffer.remove(index: 0, len: readSize);
1989
1990 return buff;
1991}
1992
1993/*!
1994 * \brief compressedTile
1995 *
1996 * The compressed tile contains compressed data per channel.
1997 *
1998 * If 16 bit, high and low bytes are treated separately (so I have
1999 * channels * 2 compressed data blocks). First the high ones, then the low
2000 * ones (or vice versa): for the reconstruction I went by trial and error :)
2001 * \param d The device
2002 * \param header The header.
2003 * \return The tile as Qt image.
2004 */
2005QImage RGBAChunk::compressedTile(QIODevice *d, const TBHDChunk *header) const
2006{
2007 QImage img(size(), header->format());
2008 auto bpc = header->bpc();
2009
2010 if (bpc == 1) {
2011 for (auto c = 0, cs = header->channels(); c < cs; ++c) {
2012 for (auto y = 0, h = img.height(); y < h; ++y) {
2013 auto ba = readStride(d, header);
2014 if (ba.isEmpty()) {
2015 return {};
2016 }
2017 auto scl = reinterpret_cast<quint8*>(img.scanLine(y));
2018 for (auto x = 0, w = std::min(a: int(ba.size()), b: img.width()); x < w; ++x) {
2019 scl[x * cs + cs - c - 1] = ba.at(i: x);
2020 }
2021 }
2022 }
2023 } else if (bpc == 2) {
2024 auto cs = std::max(a: 1, b: header->channels());
2025 if (cs < 4) { // alpha on 64-bit images must be 0xFF
2026 std::memset(s: img.bits(), c: 0xFF, n: img.sizeInBytes());
2027 }
2028 for (auto c = 0, cc = header->channels() * header->bpc(); c < cc; ++c) {
2029#if Q_BYTE_ORDER == Q_BIG_ENDIAN
2030 auto c_bcp = c / cs; // Not tried
2031#else
2032 auto c_bcp = 1 - c / cs;
2033#endif
2034 auto c_cs = (cs - 1 - c % cs) * bpc + c_bcp;
2035 for (auto y = 0, h = img.height(); y < h; ++y) {
2036 auto ba = readStride(d, header);
2037 if (ba.isEmpty()) {
2038 return {};
2039 }
2040 auto scl = reinterpret_cast<quint8*>(img.scanLine(y));
2041 for (auto x = 0, w = std::min(a: int(ba.size()), b: img.width()); x < w; ++x) {
2042 scl[x * 4 * bpc + c_cs] = ba.at(i: x); // * 4 -> Qt RGB 64-bit formats are always 4 channels
2043 }
2044 }
2045 }
2046 }
2047
2048 return img;
2049}
2050
2051/*!
2052 * \brief RGBAChunk::uncompressedTile
2053 *
2054 * The uncompressed tile scanline contains the data in
2055 * B0 G0 R0 A0 B1 G1 R1 A1... Bn Gn Rn An format.
2056 * \param d The device
2057 * \param header The header.
2058 * \return The tile as Qt image.
2059 */
2060QImage RGBAChunk::uncompressedTile(QIODevice *d, const TBHDChunk *header) const
2061{
2062 QImage img(size(), header->format());
2063 auto bpc = header->bpc();
2064
2065 if (bpc == 1) {
2066 auto cs = header->channels();
2067 for (auto y = 0, h = img.height(); y < h; ++y) {
2068 auto ba = readStride(d, header);
2069 if (ba.isEmpty()) {
2070 return {};
2071 }
2072 auto scl = reinterpret_cast<quint8*>(img.scanLine(y));
2073 for (auto c = 0; c < cs; ++c) {
2074 for (auto x = 0, w = std::min(a: int(ba.size() / cs), b: img.width()); x < w; ++x) {
2075 auto xcs = x * cs;
2076 scl[xcs + cs - c - 1] = ba.at(i: xcs + c);
2077 }
2078 }
2079 }
2080 } else if (bpc == 2) {
2081 auto cs = header->channels();
2082 if (cs < 4) { // alpha on 64-bit images must be 0xFF
2083 std::memset(s: img.bits(), c: 0xFF, n: img.sizeInBytes());
2084 }
2085
2086 for (auto y = 0, h = img.height(); y < h; ++y) {
2087 auto ba = readStride(d, header);
2088 if (ba.isEmpty()) {
2089 return {};
2090 }
2091 auto scl = reinterpret_cast<quint16*>(img.scanLine(y));
2092 auto src = reinterpret_cast<const quint16*>(ba.data());
2093 for (auto c = 0; c < cs; ++c) {
2094 for (auto x = 0, w = std::min(a: int(ba.size() / cs / bpc), b: img.width()); x < w; ++x) {
2095 auto xcs = x * cs;
2096 auto xcs4 = x * 4;
2097#if Q_BYTE_ORDER == Q_BIG_ENDIAN
2098 scl[xcs4 + cs - c - 1] = src[xcs + c]; // Not tried
2099#else
2100 scl[xcs4 + cs - c - 1] = (src[xcs + c] >> 8) | (src[xcs + c] << 8);
2101#endif
2102 }
2103 }
2104 }
2105 }
2106
2107 return img;
2108}
2109
2110QImage RGBAChunk::tile(QIODevice *d, const TBHDChunk *header) const
2111{
2112 if (!isValid() || header == nullptr) {
2113 return {};
2114 }
2115 if (!seek(d, relPos: 8)) {
2116 return {};
2117 }
2118
2119 if (isTileCompressed(header)) {
2120 return compressedTile(d, header);
2121 }
2122
2123 return uncompressedTile(d, header);
2124}
2125
2126bool RGBAChunk::innerReadStructure(QIODevice *d)
2127{
2128 auto ba = d->read(maxlen: 8);
2129 if (ba.size() != 8) {
2130 return false;
2131 }
2132
2133 auto x0 = ui16(c1: ba.at(i: 1), c2: ba.at(i: 0));
2134 auto y0 = ui16(c1: ba.at(i: 3), c2: ba.at(i: 2));
2135 auto x1 = ui16(c1: ba.at(i: 5), c2: ba.at(i: 4));
2136 auto y1 = ui16(c1: ba.at(i: 7), c2: ba.at(i: 6));
2137 if (x0 > x1 || y0 > y1) {
2138 return false;
2139 }
2140
2141 _posPx = QPoint(x0, y0);
2142 _sizePx = QSize(qint32(x1) - x0 + 1, qint32(y1) - y0 + 1);
2143
2144 return true;
2145}
2146
2147
2148/* ******************
2149 * *** ANNO Chunk ***
2150 * ****************** */
2151
2152ANNOChunk::~ANNOChunk()
2153{
2154
2155}
2156
2157ANNOChunk::ANNOChunk()
2158{
2159
2160}
2161
2162bool ANNOChunk::isValid() const
2163{
2164 return chunkId() == ANNOChunk::defaultChunkId();
2165}
2166
2167QString ANNOChunk::value() const
2168{
2169 return dataToString(chunk: this);
2170}
2171
2172bool ANNOChunk::innerReadStructure(QIODevice *d)
2173{
2174 return cacheData(d);
2175}
2176
2177/* ******************
2178 * *** AUTH Chunk ***
2179 * ****************** */
2180
2181AUTHChunk::~AUTHChunk()
2182{
2183
2184}
2185
2186AUTHChunk::AUTHChunk()
2187{
2188
2189}
2190
2191bool AUTHChunk::isValid() const
2192{
2193 return chunkId() == AUTHChunk::defaultChunkId();
2194}
2195
2196QString AUTHChunk::value() const
2197{
2198 return dataToString(chunk: this);
2199}
2200
2201bool AUTHChunk::innerReadStructure(QIODevice *d)
2202{
2203 return cacheData(d);
2204}
2205
2206
2207/* ******************
2208 * *** COPY Chunk ***
2209 * ****************** */
2210
2211COPYChunk::~COPYChunk()
2212{
2213
2214}
2215
2216COPYChunk::COPYChunk()
2217{
2218
2219}
2220
2221bool COPYChunk::isValid() const
2222{
2223 return chunkId() == COPYChunk::defaultChunkId();
2224}
2225
2226QString COPYChunk::value() const
2227{
2228 return dataToString(chunk: this);
2229}
2230
2231bool COPYChunk::innerReadStructure(QIODevice *d)
2232{
2233 return cacheData(d);
2234}
2235
2236
2237/* ******************
2238 * *** DATE Chunk ***
2239 * ****************** */
2240
2241DATEChunk::~DATEChunk()
2242{
2243
2244}
2245
2246DATEChunk::DATEChunk()
2247{
2248
2249}
2250
2251bool DATEChunk::isValid() const
2252{
2253 return chunkId() == DATEChunk::defaultChunkId();
2254}
2255
2256QDateTime DATEChunk::value() const
2257{
2258 if (!isValid()) {
2259 return {};
2260 }
2261 return QDateTime::fromString(string: QString::fromLatin1(ba: data()), format: Qt::TextDate);
2262}
2263
2264bool DATEChunk::innerReadStructure(QIODevice *d)
2265{
2266 return cacheData(d);
2267}
2268
2269
2270/* ******************
2271 * *** EXIF Chunk ***
2272 * ****************** */
2273
2274EXIFChunk::~EXIFChunk()
2275{
2276
2277}
2278
2279EXIFChunk::EXIFChunk()
2280{
2281
2282}
2283
2284bool EXIFChunk::isValid() const
2285{
2286 if (!data().startsWith(bv: QByteArray("Exif\0\0"))) {
2287 return false;
2288 }
2289 return chunkId() == EXIFChunk::defaultChunkId();
2290}
2291
2292MicroExif EXIFChunk::value() const
2293{
2294 if (!isValid()) {
2295 return {};
2296 }
2297 return MicroExif::fromByteArray(ba: data().mid(index: 6));
2298}
2299
2300bool EXIFChunk::innerReadStructure(QIODevice *d)
2301{
2302 return cacheData(d);
2303}
2304
2305
2306/* ******************
2307 * *** ICCN Chunk ***
2308 * ****************** */
2309
2310ICCNChunk::~ICCNChunk()
2311{
2312
2313}
2314
2315ICCNChunk::ICCNChunk()
2316{
2317
2318}
2319
2320bool ICCNChunk::isValid() const
2321{
2322 return chunkId() == ICCNChunk::defaultChunkId();
2323}
2324
2325QString ICCNChunk::value() const
2326{
2327 return dataToString(chunk: this);
2328}
2329
2330bool ICCNChunk::innerReadStructure(QIODevice *d)
2331{
2332 return cacheData(d);
2333}
2334
2335
2336/* ******************
2337 * *** ICCP Chunk ***
2338 * ****************** */
2339
2340ICCPChunk::~ICCPChunk()
2341{
2342
2343}
2344
2345ICCPChunk::ICCPChunk()
2346{
2347
2348}
2349
2350bool ICCPChunk::isValid() const
2351{
2352 return chunkId() == ICCPChunk::defaultChunkId();
2353}
2354
2355QColorSpace ICCPChunk::value() const
2356{
2357 if (!isValid()) {
2358 return {};
2359 }
2360 return QColorSpace::fromIccProfile(iccProfile: data());
2361}
2362
2363bool ICCPChunk::innerReadStructure(QIODevice *d)
2364{
2365 return cacheData(d);
2366}
2367
2368/* ******************
2369 * *** FVER Chunk ***
2370 * ****************** */
2371
2372FVERChunk::~FVERChunk()
2373{
2374
2375}
2376
2377FVERChunk::FVERChunk()
2378{
2379
2380}
2381
2382bool FVERChunk::isValid() const
2383{
2384 return chunkId() == FVERChunk::defaultChunkId();
2385}
2386
2387bool FVERChunk::innerReadStructure(QIODevice *d)
2388{
2389 return cacheData(d);
2390}
2391
2392/* ******************
2393 * *** HIST Chunk ***
2394 * ****************** */
2395
2396HISTChunk::~HISTChunk()
2397{
2398
2399}
2400
2401HISTChunk::HISTChunk()
2402{
2403
2404}
2405
2406bool HISTChunk::isValid() const
2407{
2408 return chunkId() == HISTChunk::defaultChunkId();
2409}
2410
2411QString HISTChunk::value() const
2412{
2413 if (!isValid()) {
2414 return {};
2415 }
2416 return QString::fromLatin1(ba: data());
2417}
2418
2419bool HISTChunk::innerReadStructure(QIODevice *d)
2420{
2421 return cacheData(d);
2422}
2423
2424
2425/* ******************
2426 * *** NAME Chunk ***
2427 * ****************** */
2428
2429NAMEChunk::~NAMEChunk()
2430{
2431
2432}
2433
2434NAMEChunk::NAMEChunk()
2435{
2436
2437}
2438
2439bool NAMEChunk::isValid() const
2440{
2441 return chunkId() == NAMEChunk::defaultChunkId();
2442}
2443
2444QString NAMEChunk::value() const
2445{
2446 return dataToString(chunk: this);
2447}
2448
2449bool NAMEChunk::innerReadStructure(QIODevice *d)
2450{
2451 return cacheData(d);
2452}
2453
2454
2455/* ******************
2456 * *** VDAT Chunk ***
2457 * ****************** */
2458
2459VDATChunk::~VDATChunk()
2460{
2461
2462}
2463
2464VDATChunk::VDATChunk()
2465{
2466
2467}
2468
2469bool VDATChunk::isValid() const
2470{
2471 return chunkId() == VDATChunk::defaultChunkId();
2472}
2473
2474static QByteArray decompressVdat(const QByteArray &comp)
2475{
2476 QByteArray out;
2477 auto ok = true;
2478 auto cpos = 0;
2479
2480 auto readU16BE = [&](const QByteArray &src, int &pos, bool *ok) -> quint16 {
2481 if (pos + 2 > src.size()) {
2482 *ok = false;
2483 return 0;
2484 }
2485 auto v = quint16((quint8(src[pos]) << 8) | quint8(src[pos + 1]));
2486 pos += 2;
2487 return v;
2488 };
2489
2490 auto readI8 = [&](const QByteArray &src, int &pos, bool *ok) -> qint8 {
2491 if (pos >= src.size()) {
2492 *ok = false;
2493 return 0;
2494 }
2495 return qint8(src[pos++]);
2496 };
2497
2498 auto emitWord = [&](quint16 w) {
2499 out.append(c: char(w & 0xFF));
2500 out.append(c: char(w >> 8));
2501 };
2502
2503 auto cmdCnt = readU16BE(comp, cpos, &ok);
2504 if (!ok) {
2505 return{};
2506 }
2507
2508 // decode command stream
2509 auto dpos = cmdCnt + (cmdCnt & 1);
2510 for (auto n = cmdCnt; cpos < n && dpos < comp.size() && ok;) {
2511 auto cmd = readI8(comp, cpos, &ok);
2512 if (cmd == 0) {
2513 auto count = readU16BE(comp, dpos, &ok);
2514 for (auto i = 0; i < count; ++i)
2515 emitWord(readU16BE(comp, dpos, &ok));
2516 } else if (cmd == 1) {
2517 auto count = readU16BE(comp, dpos, &ok);
2518 auto value = readU16BE(comp, dpos, &ok);
2519 for (auto i = 0; i < count; ++i)
2520 emitWord(value);
2521 } else if (cmd < 0) {
2522 auto count = -qint32(cmd);
2523 for (auto i = 0; i < count; ++i)
2524 emitWord(readU16BE(comp, dpos, &ok));
2525 } else {
2526 auto count = quint16(cmd);
2527 auto value = readU16BE(comp, dpos, &ok);
2528 for (auto i = 0; i < count; ++i)
2529 emitWord(value);
2530 }
2531 if (!ok) {
2532 return{};
2533 }
2534 }
2535 return out;
2536}
2537
2538static QByteArray vdatToIlbmPlane(const QByteArray &vdatData, const BMHDChunk *header)
2539{
2540 QByteArray ba(vdatData.size(), char());
2541 auto rowLen = header->rowLen();
2542 for (auto x = 0, n = 0; x < rowLen; x += 2) {
2543 for (auto y = 0, off = x, h = header->height(); y < h; y++, off += rowLen) {
2544 if ((off + 1 >= ba.size()) || n + 1 >= vdatData.size()) {
2545 return{};
2546 }
2547 ba[off + 1] = vdatData.at(i: n++);
2548 ba[off] = vdatData.at(i: n++);
2549 }
2550 }
2551 return ba;
2552}
2553
2554const QByteArray &VDATChunk::uncompressedData(const BMHDChunk *header) const
2555{
2556 if (uncompressed.isEmpty()) {
2557 auto tmp = decompressVdat(comp: data());
2558 if (tmp.size() == header->rowLen() * header->height()) {
2559 uncompressed = vdatToIlbmPlane(vdatData: tmp, header);
2560 }
2561 }
2562 return uncompressed;
2563}
2564
2565
2566bool VDATChunk::innerReadStructure(QIODevice *d)
2567{
2568 return cacheData(d);
2569}
2570
2571
2572/* ******************
2573 * *** VERS Chunk ***
2574 * ****************** */
2575
2576VERSChunk::~VERSChunk()
2577{
2578
2579}
2580
2581VERSChunk::VERSChunk()
2582{
2583
2584}
2585
2586bool VERSChunk::isValid() const
2587{
2588 return chunkId() == VERSChunk::defaultChunkId();
2589}
2590
2591QString VERSChunk::value() const
2592{
2593 if (!isValid()) {
2594 return {};
2595 }
2596 return QString::fromLatin1(ba: data());
2597}
2598
2599bool VERSChunk::innerReadStructure(QIODevice *d)
2600{
2601 return cacheData(d);
2602}
2603
2604
2605/* ******************
2606 * *** XMP0 Chunk ***
2607 * ****************** */
2608
2609XMP0Chunk::~XMP0Chunk()
2610{
2611
2612}
2613
2614XMP0Chunk::XMP0Chunk()
2615{
2616
2617}
2618
2619bool XMP0Chunk::isValid() const
2620{
2621 return chunkId() == XMP0Chunk::defaultChunkId();
2622}
2623
2624QString XMP0Chunk::value() const
2625{
2626 return dataToString(chunk: this);
2627}
2628
2629bool XMP0Chunk::innerReadStructure(QIODevice *d)
2630{
2631 return cacheData(d);
2632}
2633
2634
2635/* ******************
2636 * *** IHDR Chunk ***
2637 * ****************** */
2638
2639IHDRChunk::~IHDRChunk()
2640{
2641
2642}
2643
2644IHDRChunk::IHDRChunk()
2645{
2646
2647}
2648
2649bool IHDRChunk::isValid() const
2650{
2651 if (dataBytes() < 14) {
2652 return false;
2653 }
2654 return chunkId() == IHDRChunk::defaultChunkId();
2655}
2656
2657qint32 IHDRChunk::width() const
2658{
2659 if (!isValid()) {
2660 return 0;
2661 }
2662 return qint32(ui16(c1: data().at(i: 1), c2: data().at(i: 0)));
2663}
2664
2665qint32 IHDRChunk::height() const
2666{
2667 if (!isValid()) {
2668 return 0;
2669 }
2670 return qint32(ui16(c1: data().at(i: 5), c2: data().at(i: 4)));
2671}
2672
2673QSize IHDRChunk::size() const
2674{
2675 return QSize(width(), height());
2676}
2677
2678qint32 IHDRChunk::lineSize() const
2679{
2680 if (!isValid()) {
2681 return 0;
2682 }
2683 return qint32(ui16(c1: data().at(i: 3), c2: data().at(i: 2)));
2684}
2685
2686quint16 IHDRChunk::depth() const
2687{
2688 if (!isValid()) {
2689 return 0;
2690 }
2691 return qint32(ui16(c1: data().at(i: 9), c2: data().at(i: 8)));
2692}
2693
2694IHDRChunk::Model IHDRChunk::model() const
2695{
2696 if (!isValid()) {
2697 return IHDRChunk::Model::Invalid;
2698 }
2699 return IHDRChunk::Model(ui16(c1: data().at(i: 7), c2: data().at(i: 6)));
2700}
2701
2702IHDRChunk::DYuvKind IHDRChunk::yuvKind() const
2703{
2704 if (!isValid()) {
2705 return IHDRChunk::DYuvKind::One;
2706 }
2707 return IHDRChunk::DYuvKind(data().at(i: 10));
2708}
2709
2710IHDRChunk::Yuv IHDRChunk::yuvStart() const
2711{
2712 if (!isValid()) {
2713 return{};
2714 }
2715 return Yuv(data().at(i: 11), data().at(i: 12), data().at(i: 13));
2716}
2717
2718bool IHDRChunk::innerReadStructure(QIODevice *d)
2719{
2720 return cacheData(d);
2721}
2722
2723
2724/* ******************
2725 * *** IPAR Chunk ***
2726 * ****************** */
2727
2728IPARChunk::~IPARChunk()
2729{
2730
2731}
2732
2733IPARChunk::IPARChunk()
2734{
2735
2736}
2737
2738bool IPARChunk::isValid() const
2739{
2740 if (dataBytes() < 22) {
2741 return false;
2742 }
2743 return chunkId() == IPARChunk::defaultChunkId();
2744}
2745
2746qint32 IPARChunk::xOffset() const
2747{
2748 if (!isValid()) {
2749 return 0;
2750 }
2751 return qint32(ui16(c1: data().at(i: 1), c2: data().at(i: 0)));
2752}
2753
2754qint32 IPARChunk::yOffset() const
2755{
2756 if (!isValid()) {
2757 return 0;
2758 }
2759 return qint32(ui16(c1: data().at(i: 3), c2: data().at(i: 2)));
2760}
2761
2762double IPARChunk::aspectRatio() const
2763{
2764 if (!isValid()) {
2765 return 1;
2766 }
2767 if (auto xr = ui16(c1: data().at(i: 5), c2: data().at(i: 4))) {
2768 auto yr = double(ui16(c1: data().at(i: 7), c2: data().at(i: 6)));
2769 return yr / xr;
2770 }
2771 return 1;
2772}
2773
2774qint32 IPARChunk::xPage() const
2775{
2776 if (!isValid()) {
2777 return 0;
2778 }
2779 return qint32(ui16(c1: data().at(i: 9), c2: data().at(i: 8)));
2780}
2781
2782qint32 IPARChunk::yPage() const
2783{
2784 if (!isValid()) {
2785 return 0;
2786 }
2787 return qint32(ui16(c1: data().at(i: 11), c2: data().at(i: 10)));
2788}
2789
2790qint32 IPARChunk::xGrub() const
2791{
2792 if (!isValid()) {
2793 return 0;
2794 }
2795 return qint32(ui16(c1: data().at(i: 13), c2: data().at(i: 12)));
2796}
2797
2798qint32 IPARChunk::yGrub() const
2799{
2800 if (!isValid()) {
2801 return 0;
2802 }
2803 return qint32(ui16(c1: data().at(i: 15), c2: data().at(i: 14)));
2804}
2805
2806IPARChunk::Rgb IPARChunk::mask() const
2807{
2808 if (!isValid()) {
2809 return {};
2810 }
2811 return Rgb(data().at(i: 16), data().at(i: 17), data().at(i: 18));
2812}
2813
2814IPARChunk::Rgb IPARChunk::transparency() const
2815{
2816 if (!isValid()) {
2817 return {};
2818 }
2819 return Rgb(data().at(i: 19), data().at(i: 20), data().at(i: 21));
2820}
2821
2822bool IPARChunk::innerReadStructure(QIODevice *d)
2823{
2824 return cacheData(d);
2825}
2826
2827
2828/* ******************
2829 * *** PLTE Chunk ***
2830 * ****************** */
2831
2832PLTEChunk::~PLTEChunk()
2833{
2834
2835}
2836
2837PLTEChunk::PLTEChunk()
2838{
2839
2840}
2841
2842bool PLTEChunk::isValid() const
2843{
2844 if (dataBytes() < 4) {
2845 return false;
2846 }
2847 if (dataBytes() - 4 < total() * 3) {
2848 return false;
2849 }
2850 return chunkId() == PLTEChunk::defaultChunkId();
2851}
2852
2853qint32 PLTEChunk::count() const
2854{
2855 if (!isValid()) {
2856 return 0;
2857 }
2858 return total() - count();
2859}
2860
2861qint32 PLTEChunk::offset() const
2862{
2863 return qint32(ui16(c1: data().at(i: 1), c2: data().at(i: 0)));
2864}
2865
2866qint32 PLTEChunk::total() const
2867{
2868 return qint32(ui16(c1: data().at(i: 3), c2: data().at(i: 2)));
2869}
2870
2871QList<QRgb> PLTEChunk::innerPalette() const
2872{
2873 if (!isValid()) {
2874 return{};
2875 }
2876 QList<QRgb> l;
2877 auto &&d = data();
2878 for (qint32 i = offset(), n = total(); i < n; ++i) {
2879 auto i3 = 4 + i * 3;
2880 l << qRgb(r: d.at(i: i3), g: d.at(i: i3 + 1), b: d.at(i: i3 + 2));
2881 }
2882 return l;
2883}
2884
2885
2886/* ******************
2887 * *** YUVS Chunk ***
2888 * ****************** */
2889
2890YUVSChunk::~YUVSChunk()
2891{
2892
2893}
2894
2895YUVSChunk::YUVSChunk()
2896{
2897
2898}
2899
2900bool YUVSChunk::isValid() const
2901{
2902 return chunkId() == YUVSChunk::defaultChunkId();
2903}
2904
2905qint32 YUVSChunk::count() const
2906{
2907 return dataBytes() / 3;
2908}
2909
2910IHDRChunk::Yuv YUVSChunk::yuvStart(qint32 y) const
2911{
2912 if (!isValid() || y >= count()) {
2913 return{};
2914 }
2915 return IHDRChunk::Yuv(data().at(i: y * 3), data().at(i: y * 3 + 1), data().at(i: y * 3 + 2));
2916}
2917
2918bool YUVSChunk::innerReadStructure(QIODevice *d)
2919{
2920 return cacheData(d);
2921}
2922
2923
2924/* ******************
2925 * *** IDAT Chunk ***
2926 * ****************** */
2927
2928IDATChunk::~IDATChunk()
2929{
2930
2931}
2932
2933IDATChunk::IDATChunk()
2934{
2935
2936}
2937
2938bool IDATChunk::isValid() const
2939{
2940 return chunkId() == IDATChunk::defaultChunkId();
2941}
2942
2943/*!
2944 * Converts a YUV pixel to RGB.
2945 */
2946inline IPARChunk::Rgb yuvToRgb(IHDRChunk::Yuv yuv) {
2947 IPARChunk::Rgb rgb;
2948
2949 // Green Book Cap. V Par. 4.4.2.2
2950 const auto b = yuv.y + (yuv.u - 128.) * 1.733;
2951 const auto r = yuv.y + (yuv.v - 128.) * 1.371;
2952 const auto g = (yuv.y - 0.299 * r - 0.114 * b) / 0.587;
2953
2954 rgb.r = quint8(std::clamp(val: r + 0.5, lo: 0., hi: 255.));
2955 rgb.g = quint8(std::clamp(val: g + 0.5, lo: 0., hi: 255.));
2956 rgb.b = quint8(std::clamp(val: b + 0.5, lo: 0., hi: 255.));
2957
2958 return rgb;
2959}
2960
2961static QByteArray decompressRL7Row(QIODevice *device, int width) {
2962 QByteArray row;
2963 for (auto x = 0; x < width;) {
2964 char b;
2965 if (!device->getChar(c: &b)) {
2966 return{};
2967 }
2968 if (b & 0x80) {
2969 auto color = b & 0x7F;
2970 if (!device->getChar(c: &b)) {
2971 return{};
2972 }
2973 auto length = quint8(b);
2974 if (length == 0) {
2975 row.append(n: width - x, ch: char(color));
2976 x = width;
2977 } else {
2978 auto count = std::min(a: int(length), b: width - x);
2979 row.append(n: count, ch: char(color));
2980 x += count;
2981 }
2982 } else {
2983 row.append(c: b);
2984 x++;
2985 }
2986 }
2987 return row;
2988}
2989
2990QByteArray IDATChunk::strideRead(QIODevice *d, qint32 y, const IHDRChunk *header, const IPARChunk *params, const YUVSChunk *yuvs) const
2991{
2992 Q_UNUSED(params)
2993 if (!isValid() || header == nullptr || d == nullptr) {
2994 return {};
2995 }
2996
2997 auto read = strideSize(header);
2998 for (auto nextPos = nextChunkPos(); !d->atEnd() && d->pos() < nextPos;) {
2999 QByteArray rr;
3000 if (header->model() == IHDRChunk::Rle7) {
3001 rr = decompressRL7Row(device: d, width: header->width());
3002 } else {
3003 rr = d->read(maxlen: read);
3004 }
3005
3006 if (header->model() == IHDRChunk::CLut4) {
3007 if (rr.size() < header->width() / 2) {
3008 return {};
3009 }
3010 QByteArray tmp(header->width(), char());
3011 for (auto x = 0, w = header->width(); x < w; ++x) {
3012 auto i8 = quint8(rr.at(i: x / 2));
3013 tmp[x] = x & 1 ? i8 & 0xF : (i8 >> 4) & 0xF;
3014 }
3015 rr = tmp;
3016 }
3017
3018 if (header->model() == IHDRChunk::Rgb555) {
3019 for (qint32 x = 0, w = rr.size() - 1; x < w; x += 2) {
3020 std::swap(a&: rr[x], b&: rr[x + 1]);
3021 }
3022 }
3023
3024 if (header->model() == IHDRChunk::DYuv) {
3025 if (rr.size() < header->width()) {
3026 return {};
3027 }
3028
3029 // delta table: Green Book Cap. V Par. 3.4.1.3
3030 // NOTE 1: using the wrong delta table creates visible artifacts on the image.
3031 // NOTE 2: using { 0, 1, 4, 9, 16, 27, 44, 79, -128, -79, -44, -27, -16, -9, -4, -1 }
3032 // table gives the same result (when assigned to an uint8).
3033 static const qint32 deltaTable[16] = {
3034 0, 1, 4, 9, 16, 27, 44, 79, 128, 177, 212, 229, 240, 247, 252, 255
3035 };
3036
3037 auto yuv = header->yuvStart();
3038 if (header->yuvKind() == IHDRChunk::Each && yuvs) {
3039 yuv = yuvs->yuvStart(y);
3040 }
3041
3042 QByteArray tmp(header->width() * 3, char());
3043 for (auto x = 0, w = header->width() - 1; x < w; x += 2) {
3044 // nibble order from Green Book Cap. V Par. 6.5.1.1
3045 // NOTE: using the wrong nibble order creates visible artifacts on the image.
3046 const auto du = deltaTable[(rr.at(i: x) >> 4) & 0x0F];
3047 const auto d1 = deltaTable[rr.at(i: x) & 0x0F];
3048 const auto dv = deltaTable[(rr.at(i: x + 1) >> 4) & 0x0F];
3049 const auto d2 = deltaTable[rr.at(i: x + 1) & 0x0F];
3050
3051 // pixel 1
3052 yuv.y = d1 + yuv.y;
3053 yuv.u = du + yuv.u;
3054 yuv.v = dv + yuv.v;
3055 auto rgb = yuvToRgb(yuv);
3056 tmp[x * 3] = rgb.r;
3057 tmp[x * 3 + 1] = rgb.g;
3058 tmp[x * 3 + 2] = rgb.b;
3059
3060 // pixel 2
3061 yuv.y = d2 + yuv.y;
3062 rgb = yuvToRgb(yuv);
3063 tmp[(x + 1) * 3] = rgb.r;
3064 tmp[(x + 1) * 3 + 1] = rgb.g;
3065 tmp[(x + 1) * 3 + 2] = rgb.b;
3066 }
3067 rr = tmp;
3068 }
3069
3070 return rr;
3071 }
3072
3073 return {};
3074}
3075
3076bool IDATChunk::resetStrideRead(QIODevice *d) const
3077{
3078 return seek(d);
3079}
3080
3081quint32 IDATChunk::strideSize(const IHDRChunk *header) const
3082{
3083 if (header == nullptr) {
3084 return 0;
3085 }
3086
3087 auto rs = (header->width() * header->depth() + 7) / 8;
3088
3089 // No padding bytes are inserted in the data.
3090 if (header->model() == IHDRChunk::Rgb888) {
3091 return rs;
3092 }
3093
3094 // The first pixel of each scan line must begin in a longword boundary.
3095 if (auto mod = rs % 4)
3096 rs += (4 - mod);
3097 return rs;
3098}
3099
3100
3101/* ******************
3102 * *** RGHD Chunk ***
3103 * ****************** */
3104
3105RGHDChunk::~RGHDChunk()
3106{
3107
3108}
3109
3110RGHDChunk::RGHDChunk()
3111{
3112
3113}
3114
3115bool RGHDChunk::isValid() const
3116{
3117 return dataBytes() >= 13 * sizeof(quint32) && chunkId() == RGHDChunk::defaultChunkId();
3118}
3119
3120QSize RGHDChunk::size() const
3121{
3122 return QSize(width(), height());
3123}
3124
3125qint32 RGHDChunk::leftEdge() const
3126{
3127 if (!isValid()) {
3128 return 0;
3129 }
3130 return i32(data: data(), pos: 0);
3131}
3132
3133qint32 RGHDChunk::topEdge() const
3134{
3135 if (!isValid()) {
3136 return 0;
3137 }
3138 return i32(data: data(), pos: 4);
3139}
3140
3141qint32 RGHDChunk::width() const
3142{
3143 if (!isValid()) {
3144 return 0;
3145 }
3146 return i32(data: data(), pos: 8);
3147}
3148
3149qint32 RGHDChunk::height() const
3150{
3151 if (!isValid()) {
3152 return 0;
3153 }
3154 return i32(data: data(), pos: 12);
3155}
3156
3157qint32 RGHDChunk::pageWidth() const
3158{
3159 if (!isValid()) {
3160 return 0;
3161 }
3162 return i32(data: data(), pos: 16);
3163}
3164
3165qint32 RGHDChunk::pageHeight() const
3166{
3167 if (!isValid()) {
3168 return 0;
3169 }
3170 return i32(data: data(), pos: 20);
3171}
3172
3173quint32 RGHDChunk::depth() const
3174{
3175 if (!isValid()) {
3176 return 0;
3177 }
3178 return ui32(data: data(), pos: 24);
3179}
3180
3181quint32 RGHDChunk::pixelBits() const
3182{
3183 if (!isValid()) {
3184 return 0;
3185 }
3186 return ui32(data: data(), pos: 28);
3187}
3188
3189quint32 RGHDChunk::bytesPerLine() const
3190{
3191 if (!isValid()) {
3192 return 0;
3193 }
3194 return ui32(data: data(), pos: 32);
3195}
3196
3197RGHDChunk::Compression RGHDChunk::compression() const
3198{
3199 if (!isValid()) {
3200 return Compression::Uncompressed;
3201 }
3202 return Compression(ui32(data: data(), pos: 36));
3203}
3204
3205quint32 RGHDChunk::xAspect() const
3206{
3207 if (!isValid()) {
3208 return 0;
3209 }
3210 return ui32(data: data(), pos: 40);
3211}
3212
3213quint32 RGHDChunk::yAspect() const
3214{
3215 if (!isValid()) {
3216 return 0;
3217 }
3218 return ui32(data: data(), pos: 44);
3219}
3220
3221double RGHDChunk::aspectRatio() const
3222{
3223 if (auto xr = xAspect()) {
3224 auto yr = yAspect();
3225 return double(yr) / double(xr);
3226 }
3227 return 1;
3228}
3229
3230RGHDChunk::BitmapTypes RGHDChunk::bitmapType() const
3231{
3232 if (!isValid()) {
3233 return BitmapType::Planar8;
3234 }
3235 return BitmapTypes(ui32(data: data(), pos: 48));
3236}
3237
3238bool RGHDChunk::innerReadStructure(QIODevice *d)
3239{
3240 return cacheData(d);
3241}
3242
3243
3244/* ******************
3245 * *** RCOL Chunk ***
3246 * ****************** */
3247
3248RCOLChunk::~RCOLChunk()
3249{
3250
3251}
3252
3253RCOLChunk::RCOLChunk()
3254{
3255
3256}
3257
3258bool RCOLChunk::isValid() const
3259{
3260 return dataBytes() >= 776 && chunkId() == RCOLChunk::defaultChunkId();
3261}
3262
3263qint32 RCOLChunk::count() const
3264{
3265 return isValid() ? 256 : 0;
3266}
3267
3268QList<QRgb> RCOLChunk::innerPalette() const
3269{
3270 if (!isValid()) {
3271 return {};
3272 }
3273
3274 QList<QRgb> l;
3275 auto &&d = data();
3276 for (qint32 i = 0, n = count(); i < n; ++i) {
3277 auto i3 = i * 3 + 8;
3278 l << qRgb(r: d.at(i: i3), g: d.at(i: i3 + 1), b: d.at(i: i3 + 2));
3279 }
3280
3281 if (ui32(data: data(), pos: 0)) {
3282 auto tr = ui32(data: data(), pos: 4);
3283 if (tr < l.size()) {
3284 l[tr] &= 0x00FFFFFF;
3285 }
3286 }
3287
3288 return l;
3289}
3290
3291
3292/* ******************
3293 * *** RFLG Chunk ***
3294 * ****************** */
3295
3296RFLGChunk::~RFLGChunk()
3297{
3298
3299}
3300
3301RFLGChunk::RFLGChunk()
3302{
3303
3304}
3305
3306bool RFLGChunk::isValid() const
3307{
3308 return dataBytes() >= 4 && chunkId() == RFLGChunk::defaultChunkId();
3309}
3310
3311RFLGChunk::Flags RFLGChunk::flags() const
3312{
3313 if (!isValid()) {
3314 return {};
3315 }
3316 return Flags(ui32(data: data(), pos: 0));
3317}
3318
3319bool RFLGChunk::innerReadStructure(QIODevice *d)
3320{
3321 return cacheData(d);
3322}
3323
3324
3325/* ******************
3326 * *** RSCM Chunk ***
3327 * ****************** */
3328
3329RSCMChunk::~RSCMChunk()
3330{
3331
3332}
3333
3334RSCMChunk::RSCMChunk()
3335{
3336
3337}
3338
3339bool RSCMChunk::isValid() const
3340{
3341 return dataBytes() >= 12 && chunkId() == RSCMChunk::defaultChunkId();
3342}
3343
3344quint32 RSCMChunk::viewMode() const
3345{
3346 if (!isValid()) {
3347 return 0;
3348 }
3349 return ui32(data: data(), pos: 0);
3350}
3351
3352quint32 RSCMChunk::localVM0() const
3353{
3354 if (!isValid()) {
3355 return 0;
3356 }
3357 return ui32(data: data(), pos: 4);
3358}
3359
3360quint32 RSCMChunk::localVM1() const
3361{
3362 if (!isValid()) {
3363 return 0;
3364 }
3365 return ui32(data: data(), pos: 8);
3366}
3367
3368bool RSCMChunk::innerReadStructure(QIODevice *d)
3369{
3370 return cacheData(d);
3371}
3372
3373
3374/* ******************
3375 * *** RBOD Chunk ***
3376 * ****************** */
3377
3378RBODChunk::~RBODChunk()
3379{
3380
3381}
3382
3383RBODChunk::RBODChunk()
3384{
3385
3386}
3387
3388bool RBODChunk::isValid() const
3389{
3390 return chunkId() == RBODChunk::defaultChunkId();
3391}
3392
3393QByteArray RBODChunk::strideRead(QIODevice *d, qint32 y, const RGHDChunk *header, const RSCMChunk *rcsm, const RCOLChunk *rcol) const
3394{
3395 if (!isValid() || header == nullptr || d == nullptr) {
3396 return {};
3397 }
3398
3399 QByteArray planes;
3400 auto readSize = strideSize(header);
3401 for (auto nextPos = nextChunkPos(); !d->atEnd() && d->pos() < nextPos && planes.size() < readSize;) {
3402 if (header->compression() == RGHDChunk::Compression::Uncompressed) {
3403 planes = d->read(maxlen: readSize);
3404 } else {
3405 qCDebug(LOG_IFFPLUGIN) << "RBODChunk::strideRead(): unknown compression" << header->compression();
3406 }
3407 if (planes.size() != readSize) {
3408 return {};
3409 }
3410 }
3411
3412 return deinterleave(planes, y, header, rcsm, rcol);
3413}
3414
3415bool RBODChunk::resetStrideRead(QIODevice *d) const
3416{
3417 return seek(d);
3418}
3419
3420QByteArray RBODChunk::deinterleave(const QByteArray &planes, qint32 y, const RGHDChunk *header, const RSCMChunk *rcsm, const RCOLChunk *rcol) const
3421{
3422 Q_UNUSED(y)
3423 Q_UNUSED(rcsm)
3424 Q_UNUSED(rcol)
3425 if (planes.size() != strideSize(header)) {
3426 return {};
3427 }
3428
3429 QByteArray ba;
3430
3431 auto width = header->width();
3432 auto rgfx_format = RGHDChunk::BitmapTypes(header->bitmapType() & 0x3FFFFFFF);
3433
3434 if (rgfx_format == RGHDChunk::BitmapType::Chunky8) {
3435 ba = planes;
3436 } else if (rgfx_format == RGHDChunk::BitmapType::Planar8) {
3437 // No test case: ignoring...
3438 } else if (rgfx_format == RGHDChunk::BitmapType::Rgb15 || rgfx_format == RGHDChunk::BitmapType::Rgb16) {
3439 ba = planes;
3440 if (QSysInfo::ByteOrder == QSysInfo::LittleEndian) {
3441 for (qint32 x = 0; x < width; ++x) {
3442 auto x2 = x * 2;
3443 ba[x2] = planes[x2 + 1];
3444 ba[x2 + 1] = planes[x2];
3445 }
3446 }
3447 } else if (rgfx_format == RGHDChunk::BitmapType::Rgb24) {
3448 ba = planes;
3449 } else if (rgfx_format == RGHDChunk::BitmapType::Rgb32) {
3450 ba.resize(size: planes.size());
3451 auto invAlpha = header->bitmapType() & RGHDChunk::BitmapType::HasInvAlpha;
3452 for (qint32 x = 0; x < width; ++x) {
3453 auto x4 = x * 4;
3454 ba[x4] = planes[x4 + 1];
3455 ba[x4 + 1] = planes[x4 + 2];
3456 ba[x4 + 2] = planes[x4 + 3];
3457 ba[x4 + 3] = invAlpha ? 255 - planes[x4] : planes[x4];
3458 }
3459 } else if (rgfx_format == RGHDChunk::BitmapType::Rgb48) {
3460 QDataStream in(planes);
3461 in.setByteOrder(QDataStream::BigEndian);
3462 QDataStream ou(&ba, QDataStream::WriteOnly);
3463 ou.setByteOrder(QDataStream::ByteOrder(QSysInfo::ByteOrder));
3464
3465 quint16 r, g, b;
3466 for (qint32 x = 0; x < width; ++x) {
3467 in >> r >> g >> b;
3468 ou << r << g << b << quint16(0xFFFF);
3469 }
3470
3471 if (in.status() != QDataStream::Ok || ou.status() != QDataStream::Ok) {
3472 return {};
3473 }
3474 } else if (rgfx_format == RGHDChunk::BitmapType::Rgb64) {
3475 QDataStream in(planes);
3476 in.setByteOrder(QDataStream::BigEndian);
3477 QDataStream ou(&ba, QDataStream::WriteOnly);
3478 ou.setByteOrder(QDataStream::ByteOrder(QSysInfo::ByteOrder));
3479
3480 auto invAlpha = header->bitmapType() & RGHDChunk::BitmapType::HasInvAlpha;
3481 quint16 r, g, b, a;
3482 for (qint32 x = 0; x < width; ++x) {
3483 in >> a >> r >> g >> b;
3484 if (invAlpha)
3485 a = 0xFFFF - a;
3486 ou << r << g << b << a;
3487 }
3488
3489 if (in.status() != QDataStream::Ok || ou.status() != QDataStream::Ok) {
3490 return {};
3491 }
3492 } else if (rgfx_format == RGHDChunk::BitmapType::Rgb96) {
3493 QDataStream in(planes);
3494 in.setByteOrder(QDataStream::BigEndian);
3495 in.setFloatingPointPrecision(QDataStream::SinglePrecision);
3496 QDataStream ou(&ba, QDataStream::WriteOnly);
3497 ou.setByteOrder(QDataStream::ByteOrder(QSysInfo::ByteOrder));
3498 ou.setFloatingPointPrecision(QDataStream::SinglePrecision);
3499
3500 float r, g, b;
3501 for (qint32 x = 0; x < width; ++x) {
3502 in >> r >> g >> b;
3503 ou << r << g << b << float(1);
3504 }
3505
3506 if (in.status() != QDataStream::Ok || ou.status() != QDataStream::Ok) {
3507 return {};
3508 }
3509 } else if (rgfx_format == RGHDChunk::BitmapType::Rgb128) {
3510 QDataStream in(planes);
3511 in.setByteOrder(QDataStream::BigEndian);
3512 in.setFloatingPointPrecision(QDataStream::SinglePrecision);
3513 QDataStream ou(&ba, QDataStream::WriteOnly);
3514 ou.setByteOrder(QDataStream::ByteOrder(QSysInfo::ByteOrder));
3515 ou.setFloatingPointPrecision(QDataStream::SinglePrecision);
3516
3517 auto invAlpha = header->bitmapType() & RGHDChunk::BitmapType::HasInvAlpha;
3518 float r, g, b, a;
3519 for (qint32 x = 0; x < width; ++x) {
3520 in >> a >> r >> g >> b;
3521 if (invAlpha)
3522 a = 1 - a;
3523 ou << r << g << b << a;
3524 }
3525
3526 if (in.status() != QDataStream::Ok || ou.status() != QDataStream::Ok) {
3527 return {};
3528 }
3529 }
3530
3531 return ba;
3532}
3533
3534quint32 RBODChunk::strideSize(const RGHDChunk *header) const
3535{
3536 auto rgfx_format = RGHDChunk::BitmapTypes(header->bitmapType() & 0x3FFFFFFF);
3537 if (rgfx_format == RGHDChunk::BitmapType::Planar8) {
3538 return (header->width() + 7) / 8;
3539 }
3540 if (rgfx_format == RGHDChunk::BitmapType::Chunky8) {
3541 return header->width();
3542 }
3543 if (rgfx_format == RGHDChunk::BitmapType::Rgb15 || rgfx_format == RGHDChunk::BitmapType::Rgb16) {
3544 return header->width() * 2;
3545 }
3546 if (rgfx_format == RGHDChunk::BitmapType::Rgb24) {
3547 return header->width() * 3;
3548 }
3549 if (rgfx_format == RGHDChunk::BitmapType::Rgb32) {
3550 return header->width() * 4;
3551 }
3552 if (rgfx_format == RGHDChunk::BitmapType::Rgb48) {
3553 return header->width() * 6;
3554 }
3555 if (rgfx_format == RGHDChunk::BitmapType::Rgb64) {
3556 return header->width() * 8;
3557 }
3558 if (rgfx_format == RGHDChunk::BitmapType::Rgb96) {
3559 return header->width() * 12;
3560 }
3561 if (rgfx_format == RGHDChunk::BitmapType::Rgb128) {
3562 return header->width() * 16;
3563 }
3564 return 0;
3565}
3566
3567
3568/* ******************
3569 * *** BEAM Chunk ***
3570 * ****************** */
3571
3572BEAMChunk::~BEAMChunk()
3573{
3574
3575}
3576
3577BEAMChunk::BEAMChunk()
3578 : IPALChunk()
3579 , _height()
3580{
3581
3582}
3583
3584bool BEAMChunk::isValid() const
3585{
3586 return chunkId() == BEAMChunk::defaultChunkId();
3587}
3588
3589IPALChunk *BEAMChunk::clone() const
3590{
3591 return new BEAMChunk(*this);
3592}
3593
3594bool BEAMChunk::initialize(const QList<QRgb> &, qint32 height)
3595{
3596 _height = height;
3597 return true;
3598}
3599
3600QList<QRgb> BEAMChunk::palette(qint32 y) const
3601{
3602 auto &&height = _height;
3603 if (height < 1) {
3604 return {};
3605 }
3606 auto bpp = bytes() / height;
3607 if (bytes() != height * bpp) {
3608 return {};
3609 }
3610 auto col = qint32(bpp / 2);
3611 auto &&dt = data();
3612 QList<QRgb> pal;
3613 for (auto c = 0; c < col; ++c) {
3614 // 2 bytes per color (0x0R 0xGB)
3615 auto idx = bpp * y + c * 2;
3616 if (idx + 1 < dt.size()) {
3617 auto r = quint8(dt[idx] & 0x0F);
3618 auto g = quint8(dt[idx + 1] & 0xF0);
3619 auto b = quint8(dt[idx + 1] & 0x0F);
3620 pal << qRgb(r: r | (r << 4), g: (g >> 4) | g, b: b | (b << 4));
3621 }
3622 }
3623 return pal;
3624}
3625
3626bool BEAMChunk::innerReadStructure(QIODevice *d)
3627{
3628 return cacheData(d);
3629}
3630
3631
3632/* ******************
3633 * *** CTBL Chunk ***
3634 * ****************** */
3635
3636CTBLChunk::~CTBLChunk()
3637{
3638
3639}
3640
3641CTBLChunk::CTBLChunk() : BEAMChunk()
3642{
3643
3644}
3645
3646bool CTBLChunk::isValid() const
3647{
3648 return chunkId() == CTBLChunk::defaultChunkId();
3649}
3650
3651
3652/* ******************
3653 * *** SHAM Chunk ***
3654 * ****************** */
3655
3656SHAMChunk::~SHAMChunk()
3657{
3658
3659}
3660
3661SHAMChunk::SHAMChunk()
3662 : IPALChunk()
3663 , _height()
3664{
3665
3666}
3667
3668bool SHAMChunk::isValid() const
3669{
3670 if (dataBytes() < 2) {
3671 return false;
3672 }
3673 auto &&dt = data();
3674 if (dt[0] != 0 && dt[1] != 0) {
3675 // In all the sham test cases I have them at zero...
3676 // if they are different from zero I suppose they should
3677 // be interpreted differently from what was done.
3678 return false;
3679 }
3680 return chunkId() == SHAMChunk::defaultChunkId();
3681}
3682
3683IPALChunk *SHAMChunk::clone() const
3684{
3685 return new SHAMChunk(*this);
3686}
3687
3688QList<QRgb> SHAMChunk::palette(qint32 y) const
3689{
3690 auto && height = _height;
3691 if (height < 1) {
3692 return {};
3693 }
3694 auto bpp = 32; // always 32 bytes per palette (16 colors)
3695 auto div = 0;
3696 if (bytes() == quint32(height * bpp + 2)) {
3697 div = 1;
3698 } else if (bytes() == quint32(height / 2 * bpp + 2)) {
3699 div = 2;
3700 }
3701 if (div == 0) {
3702 return {};
3703 }
3704 auto &&dt = data();
3705 QList<QRgb> pal;
3706 for (auto c = 0, col = bpp / 2, idx0 = y / div * bpp + 2; c < col; ++c) {
3707 // 2 bytes per color (0x0R 0xGB)
3708 auto idx = idx0 + c * 2;
3709 if (idx + 1 < dt.size()) {
3710 auto r = quint8(dt[idx] & 0x0F);
3711 auto g = quint8(dt[idx + 1] & 0xF0);
3712 auto b = quint8(dt[idx + 1] & 0x0F);
3713 pal << qRgb(r: r | (r << 4), g: (g >> 4) | g, b: b | (b << 4));
3714 }
3715 }
3716 return pal;
3717}
3718
3719bool SHAMChunk::initialize(const QList<QRgb> &, qint32 height)
3720{
3721 _height = height;
3722 return true;
3723}
3724
3725bool SHAMChunk::innerReadStructure(QIODevice *d)
3726{
3727 return cacheData(d);
3728}
3729
3730/* ******************
3731 * *** RAST Chunk ***
3732 * ****************** */
3733
3734RASTChunk::~RASTChunk()
3735{
3736
3737}
3738
3739RASTChunk::RASTChunk()
3740 : IPALChunk()
3741 , _height()
3742{
3743
3744}
3745
3746bool RASTChunk::isValid() const
3747{
3748 return chunkId() == RASTChunk::defaultChunkId();
3749}
3750
3751IPALChunk *RASTChunk::clone() const
3752{
3753 return new RASTChunk(*this);
3754}
3755
3756QList<QRgb> RASTChunk::palette(qint32 y) const
3757{
3758 auto &&height = _height;
3759 if (height < 1) {
3760 return {};
3761 }
3762 auto bpp = bytes() / height;
3763 if (bytes() != height * bpp) {
3764 return {};
3765 }
3766 auto col = qint32(bpp / 2 - 1);
3767 auto &&dt = data();
3768 QList<QRgb> pal;
3769 for (auto c = 0; c < col; ++c) {
3770 auto idx = bpp * y + 2 + c * 2;
3771 if (idx + 1 < dt.size()) {
3772 // The Atari ST uses 3 bits per color (512 colors) while the Atari STE
3773 // uses 4 bits per color (4096 colors). This strange encoding with the
3774 // least significant bit set as MSB is, I believe, to ensure hardware
3775 // compatibility between the two machines.
3776 #define H1L(a) ((quint8(a) & 0x7) << 1) | ((quint8(a) >> 3) & 1)
3777 auto r = H1L(dt[idx]);
3778 auto g = H1L(dt[idx + 1] >> 4);
3779 auto b = H1L(dt[idx + 1]);
3780 #undef H1L
3781 pal << qRgb(r: r | (r << 4), g: (g << 4) | g, b: b | (b << 4));
3782 }
3783 }
3784 return pal;
3785}
3786
3787bool RASTChunk::initialize(const QList<QRgb> &, qint32 height)
3788{
3789 _height = height;
3790 return true;
3791}
3792
3793bool RASTChunk::innerReadStructure(QIODevice *d)
3794{
3795 return cacheData(d);
3796}
3797
3798/* ******************
3799 * *** PCHG Chunk ***
3800 * ****************** */
3801
3802PCHGChunk::~PCHGChunk()
3803{
3804}
3805
3806PCHGChunk::PCHGChunk() : IPALChunk()
3807{
3808
3809}
3810
3811PCHGChunk::Compression PCHGChunk::compression() const
3812{
3813 if (!isValid()) {
3814 return Compression::Uncompressed;
3815 }
3816 return Compression(ui16(data: data(), pos: 0));
3817}
3818
3819PCHGChunk::Flags PCHGChunk::flags() const
3820{
3821 if (!isValid()) {
3822 return Flags(Flag::None);
3823 }
3824 return Flags(ui16(data: data(), pos: 2));
3825}
3826
3827qint16 PCHGChunk::startLine() const
3828{
3829 if (!isValid()) {
3830 return 0;
3831 }
3832 return i16(data: data(), pos: 4);
3833}
3834
3835quint16 PCHGChunk::lineCount() const
3836{
3837 if (!isValid()) {
3838 return 0;
3839 }
3840 return ui16(data: data(), pos: 6);
3841}
3842
3843quint16 PCHGChunk::changedLines() const
3844{
3845 if (!isValid()) {
3846 return 0;
3847 }
3848 return ui16(data: data(), pos: 8);
3849}
3850
3851quint16 PCHGChunk::minReg() const
3852{
3853 if (!isValid()) {
3854 return 0;
3855 }
3856 return ui16(data: data(), pos: 10);
3857}
3858
3859quint16 PCHGChunk::maxReg() const
3860{
3861 if (!isValid()) {
3862 return 0;
3863 }
3864 return ui16(data: data(), pos: 12);
3865}
3866
3867quint16 PCHGChunk::maxChanges() const
3868{
3869 if (!isValid()) {
3870 return 0;
3871 }
3872 return ui16(data: data(), pos: 14);
3873}
3874
3875quint32 PCHGChunk::totalChanges() const
3876{
3877 if (!isValid()) {
3878 return 0;
3879 }
3880 return ui32(data: data(), pos: 16);
3881}
3882
3883bool PCHGChunk::hasAlpha() const
3884{
3885 return (flags() & PCHGChunk::Flag::UseAlpha) ? true : false;
3886}
3887
3888bool PCHGChunk::isValid() const
3889{
3890 if (dataBytes() < 20) {
3891 return false;
3892 }
3893 return chunkId() == PCHGChunk::defaultChunkId();
3894}
3895
3896IPALChunk *PCHGChunk::clone() const
3897{
3898 return new PCHGChunk(*this);
3899}
3900
3901QList<QRgb> PCHGChunk::palette(qint32 y) const
3902{
3903 return _palettes.value(key: y);
3904}
3905
3906// ----------------------------------------------------------------------------
3907// PCHG_FastDecomp reimplementation (Amiga 68k -> portable C++/Qt)
3908// ----------------------------------------------------------------------------
3909// This mirrors the original 68k routine semantics:
3910// - The Huffman tree is stored as a sequence of signed 16-bit words (big-endian)
3911// and TreeCode points to the *last word* of that sequence.
3912// - Bits are consumed MSB-first from 32-bit big-endian longwords of the source.
3913// - Navigation rules (matching the assembly):
3914// bit=1: read w = *(a3). If w < 0 then a3 += w (byte-wise) and continue;
3915// else emit (w & 0xFF) and reset a3 to TreeCode (last word).
3916// bit=0: predecrement a3 by 2; read w = *a3. If w < 0: continue;
3917// else if (w & 0x0100) emit (w & 0xFF) and reset a3; else continue.
3918// - Stop after writing exactly OriginalSize bytes.
3919//
3920// This function expects a single QByteArray laid out as:
3921// [ tree (treeSize bytes, even) | compressed bitstream (... bytes) ]
3922//
3923// On any error, logs with qCCritical(LOG_IFFPLUGIN) and returns {}.
3924// ----------------------------------------------------------------------------
3925//
3926// NOTE: Sebastiano Vigna, the author of the PCHG specification and the ASM
3927// decompression code for the Motorola 68K, gave us permission to use his
3928// code and recommended that we convert it with AI.
3929
3930// Core decompressor (tree + compressed stream in one QByteArray)
3931static QByteArray pchgFastDecomp(const QByteArray& input, int treeSize, int originalSize)
3932{
3933 // Read a big-endian 16-bit signed word from a byte buffer
3934 auto read_be16 = [&](const char* base, int byteIndex, int size) -> qint16 {
3935 if (byteIndex + 1 >= size)
3936 return 0; // caller must bounds-check; we keep silent here
3937 const quint8 b0 = static_cast<quint8>(base[byteIndex]);
3938 const quint8 b1 = static_cast<quint8>(base[byteIndex + 1]);
3939 return static_cast<qint16>((b0 << 8) | b1);
3940 };
3941
3942 // Read a big-endian 32-bit unsigned long from a byte buffer
3943 auto read_be32 = [&](const char* base, int byteIndex, int size) -> quint32 {
3944 if (byteIndex + 3 >= size)
3945 return 0; // caller must bounds-check
3946 const quint8 b0 = static_cast<quint8>(base[byteIndex]);
3947 const quint8 b1 = static_cast<quint8>(base[byteIndex + 1]);
3948 const quint8 b2 = static_cast<quint8>(base[byteIndex + 2]);
3949 const quint8 b3 = static_cast<quint8>(base[byteIndex + 3]);
3950 return (static_cast<quint32>(b0) << 24) |
3951 (static_cast<quint32>(b1) << 16) |
3952 (static_cast<quint32>(b2) << 8) |
3953 static_cast<quint32>(b3);
3954 };
3955
3956 // Basic validation
3957 if (treeSize <= 0 || (treeSize & 1)) {
3958 qCCritical(LOG_IFFPLUGIN) << "Invalid treeSize (must be positive and even)" << treeSize;
3959 return {};
3960 }
3961 if (input.size() < treeSize) {
3962 qCCritical(LOG_IFFPLUGIN) << "Input too small for treeSize" << input.size() << treeSize;
3963 return {};
3964 }
3965 if (originalSize < 0) {
3966 qCCritical(LOG_IFFPLUGIN) << "Invalid originalSize" << originalSize;
3967 return {};
3968 }
3969
3970 const char* data = input.constData();
3971 const int totalSize = input.size();
3972
3973 // Tree view (big-endian words)
3974 const int treeBytes = treeSize;
3975 const int treeWords = treeBytes / 2;
3976 if (treeWords <= 0) {
3977 qCCritical(LOG_IFFPLUGIN) << "Tree has zero words";
3978 return {};
3979 }
3980
3981 // Compressed stream
3982 const int srcBase = treeBytes; // offset where bitstream starts
3983 const int srcSize = totalSize - srcBase;
3984 if (srcSize <= 0 && originalSize > 0) {
3985 qCCritical(LOG_IFFPLUGIN) << "No compressed payload present";
3986 return {};
3987 }
3988
3989 // Emulate a3 pointer to words:
3990 // a2 points to the *last word* => word index (0..treeWords-1)
3991 auto resetA3 = [&]() {
3992 return treeWords - 1; // last word index
3993 };
3994 int a3_word = resetA3();
3995
3996 // Bit reader: loads 32b big-endian and shifts MSB-first
3997 quint32 bitbuf = 0;
3998 int bits = 0; // remaining bits in bitbuf
3999 int srcPos = 0; // byte offset relative to srcBase
4000
4001 auto refill = [&]() -> bool {
4002 if (srcPos + 4 > srcSize) {
4003 qCCritical(LOG_IFFPLUGIN) << "Compressed stream underflow while refilling bit buffer"
4004 << "srcPos=" << srcPos << "srcSize=" << srcSize;
4005 return false;
4006 }
4007 bitbuf = read_be32(data + srcBase, srcPos, srcSize);
4008 bits = 32;
4009 srcPos += 4;
4010 return true;
4011 };
4012
4013 // Main decode loop: produce exactly originalSize bytes
4014 QByteArray out;
4015 while (out.size() < qsizetype(originalSize)) {
4016 if (bits == 0) {
4017 if (!refill()) {
4018 // Not enough bits to complete output
4019 return {};
4020 }
4021 }
4022
4023 const bool bit1 = (bitbuf & 0x80000000u) != 0u; // MSB before shift
4024 bitbuf <<= 1;
4025 --bits;
4026
4027 if (bit1) {
4028 // Case bit == 1 --> w = *(a3)
4029 if (a3_word < 0 || a3_word >= treeWords) {
4030 qCCritical(LOG_IFFPLUGIN) << "a3 out of bounds (bit=1)" << a3_word;
4031 return {};
4032 }
4033 const int byteIndex = a3_word * 2;
4034 const qint16 w = read_be16(data, byteIndex, treeBytes);
4035
4036 if (w < 0) {
4037 // a3 += w (w is a signed byte offset, must be even)
4038 if (w & 1) {
4039 qCCritical(LOG_IFFPLUGIN) << "Misaligned tree offset (odd)" << w;
4040 return {};
4041 }
4042 const int deltaWords = w / 2; // arithmetic division, w is even in valid streams
4043 const int next = a3_word + deltaWords;
4044 if (next < 0 || next >= treeWords) {
4045 qCCritical(LOG_IFFPLUGIN) << "a3 out of bounds after offset" << next;
4046 return {};
4047 }
4048 a3_word = next;
4049 } else {
4050 // Leaf: emit low 8 bits, reset a3
4051 out.append(c: static_cast<char>(w & 0xFF));
4052 a3_word = resetA3();
4053 }
4054 } else {
4055 // Case bit == 0 --> w = *--a3 (predecrement)
4056 --a3_word;
4057 if (a3_word < 0) {
4058 qCCritical(LOG_IFFPLUGIN) << "a3 underflow on predecrement";
4059 return {};
4060 }
4061 const int byteIndex = a3_word * 2;
4062 const qint16 w = read_be16(data, byteIndex, treeBytes);
4063
4064 if (w < 0) {
4065 // Internal node: continue with current a3
4066 continue;
4067 }
4068
4069 // Non-negative: check bit #8; if set -> leaf
4070 if ((w & 0x0100) != 0) {
4071 out.append(c: static_cast<char>(w & 0xFF));
4072 a3_word = resetA3();
4073 } else {
4074 // Not a leaf: continue scanning
4075 continue;
4076 }
4077 }
4078 }
4079
4080 return out;
4081}
4082// !Huffman decompression
4083
4084bool PCHGChunk::initialize(const QList<QRgb> &cmapPalette, qint32 height)
4085{
4086 Q_UNUSED(height)
4087 auto dt = data().mid(index: 20);
4088 if (compression() == PCHGChunk::Compression::Huffman) {
4089 QDataStream ds(dt);
4090 ds.setByteOrder(QDataStream::BigEndian);
4091
4092 quint32 infoSize;
4093 ds >> infoSize;
4094 quint32 origSize;
4095 ds >> origSize;
4096
4097 dt = pchgFastDecomp(input: dt.mid(index: 8), treeSize: infoSize, originalSize: origSize);
4098 }
4099 if (dt.isEmpty()) {
4100 return false;
4101 }
4102
4103 QDataStream ds(dt);
4104 ds.setByteOrder(QDataStream::BigEndian);
4105
4106 // read the masks
4107 auto lcnt = lineCount();
4108 auto nlw = (lcnt + 31) / 32; // number of LWORD containing the bit mask
4109 QList<quint32> masks;
4110 for (auto i = 0; i < nlw; ++i) {
4111 quint32 mask;
4112 ds >> mask;
4113 masks << mask;
4114 }
4115 if (ds.status() != QDataStream::Ok) {
4116 return false;
4117 }
4118
4119 // read the palettes
4120 auto changesLoaded = qint64();
4121 auto startY = startLine();
4122 auto last = cmapPalette;
4123 auto flgs = flags();
4124 for (auto i = 0; i < lcnt; ++i) {
4125 auto mask = masks.at(i: i / 32);
4126 if (((mask >> (31 - i % 32)) & 1) == 0) {
4127 _palettes.insert(key: i + startY, value: last);
4128 continue; // no palette change for this line
4129 }
4130
4131 QHash<quint16, QRgb> hash;
4132 if (flgs & PCHGChunk::Flag::F12Bit) {
4133 quint8 c16;
4134 ds >> c16;
4135 quint8 c32;
4136 ds >> c32;
4137 for (auto j = 0; j < int(c16); ++j) {
4138 quint16 tmp;
4139 ds >> tmp;
4140 hash.insert(key: ((tmp >> 12) & 0xF), value: qRgb(r: ((tmp >> 8) & 0xF) * 17, g: ((tmp >> 4) & 0xF) * 17, b: ((tmp & 0xF) * 17)));
4141 }
4142 for (auto j = 0; j < int(c32); ++j) {
4143 quint16 tmp;
4144 ds >> tmp;
4145 hash.insert(key: (((tmp >> 12) & 0xF) + 16), value: qRgb(r: ((tmp >> 8) & 0xF) * 17, g: ((tmp >> 4) & 0xF) * 17, b: ((tmp & 0xF) * 17)));
4146 }
4147 } else if (flgs & PCHGChunk::Flag::F32Bit) { // NOTE: missing test case (not tested)
4148 quint16 cnt;
4149 ds >> cnt;
4150 for (auto j = 0; j < int(cnt); ++j) {
4151 quint16 reg;
4152 ds >> reg;
4153 quint8 alpha;
4154 ds >> alpha;
4155 quint8 red;
4156 ds >> red;
4157 quint8 blue;
4158 ds >> blue;
4159 quint8 green;
4160 ds >> green;
4161 hash.insert(key: reg, value: qRgba(r: red, g: green, b: blue, a: flgs & PCHGChunk::Flag::UseAlpha ? alpha : 0xFF));
4162 }
4163 }
4164
4165 if (ds.status() != QDataStream::Ok) {
4166 return false;
4167 }
4168
4169 for (auto i = qsizetype(), n = last.size(); i < n; ++i) {
4170 if (hash.contains(key: i))
4171 last[i] = hash.value(key: i);
4172 }
4173
4174 _palettes.insert(key: i + startY, value: last);
4175 changesLoaded += hash.size();
4176 }
4177
4178 if (changesLoaded != qint64(totalChanges())) {
4179 qCDebug(LOG_IFFPLUGIN) << "PCHGChunk::innerReadStructure(): palette changes count mismatch!";
4180 }
4181
4182 return true;
4183}
4184
4185bool PCHGChunk::innerReadStructure(QIODevice *d)
4186{
4187 return cacheData(d);
4188}
4189

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