/* kimgio module for SGI images SPDX-FileCopyrightText: 2004 Melchior FRANZ SPDX-License-Identifier: LGPL-2.0-or-later */ /* this code supports: * reading: * everything, except images with 1 dimension or images with * mapmode != NORMAL (e.g. dithered); Images with 16 bit * precision or more than 4 layers are stripped down. * writing: * Run Length Encoded (RLE) or Verbatim (uncompressed) * (whichever is smaller) * * Please report if you come across rgb/rgba/sgi/bw files that aren't * recognized. Also report applications that can't deal with images * saved by this filter. */ #include "rgb_p.h" #include "util_p.h" #include #include #include #include #include class RLEData : public QList { public: RLEData() { } RLEData(const uchar *d, uint l, uint o) : _offset(o) { for (uint i = 0; i < l; i++) { append(d[i]); } } bool operator<(const RLEData &) const; void write(QDataStream &s); uint offset() const { return _offset; } private: uint _offset; }; class RLEMap : public QMap { public: RLEMap() : _counter(0) , _offset(0) { } uint insert(const uchar *d, uint l); QList vector(); void setBaseOffset(uint o) { _offset = o; } private: uint _counter; uint _offset; }; class SGIImagePrivate { public: SGIImagePrivate(); ~SGIImagePrivate(); bool readImage(QImage &); bool writeImage(const QImage &); bool isValid() const; bool isSupported() const; bool peekHeader(QIODevice *device); QSize size() const; QImage::Format format() const; void setDevice(QIODevice *device); private: enum { NORMAL, DITHERED, SCREEN, COLORMAP, }; // colormap QIODevice *_dev; QDataStream _stream; quint16 _magic = 0; quint8 _rle = 0; quint8 _bpc = 0; quint16 _dim = 0; quint16 _xsize = 0; quint16 _ysize = 0; quint16 _zsize = 0; quint32 _pixmin = 0; quint32 _pixmax = 0; char _imagename[80]; quint32 _colormap = 0; quint8 _unused[404]; quint32 _unused32 = 0; quint32 *_starttab; quint32 *_lengthtab; QByteArray _data; QByteArray::Iterator _pos; RLEMap _rlemap; QList _rlevector; uint _numrows; bool readData(QImage &); bool getRow(uchar *dest); bool readHeader(); static bool readHeader(QDataStream &ds, SGIImagePrivate *sgi); bool writeHeader(); bool writeRle(); bool writeVerbatim(const QImage &); bool scanData(const QImage &); uint compact(uchar *, uchar *); uchar intensity(uchar); }; SGIImagePrivate::SGIImagePrivate() : _dev(nullptr) , _starttab(nullptr) , _lengthtab(nullptr) { std::memset(_imagename, 0, sizeof(_imagename)); std::memset(_unused, 0, sizeof(_unused)); } SGIImagePrivate::~SGIImagePrivate() { delete[] _starttab; delete[] _lengthtab; } /////////////////////////////////////////////////////////////////////////////// void SGIImagePrivate::setDevice(QIODevice *device) { _dev = device; _stream.setDevice(_dev); } bool SGIImagePrivate::getRow(uchar *dest) { int n; int i; if (!_rle) { for (i = 0; i < _xsize; i++) { if (_pos >= _data.end()) { return false; } dest[i] = uchar(*_pos); _pos += _bpc; } return true; } for (i = 0; i < _xsize;) { if (_bpc == 2) { _pos++; } if (_pos >= _data.end()) { return false; } n = *_pos & 0x7f; if (!n) { break; } if (*_pos++ & 0x80) { for (; i < _xsize && _pos < _data.end() && n--; i++) { *dest++ = *_pos; _pos += _bpc; } } else { for (; i < _xsize && n--; i++) { *dest++ = *_pos; } _pos += _bpc; } } return i == _xsize; } bool SGIImagePrivate::readData(QImage &img) { QRgb *c; quint32 *start = _starttab; QByteArray lguard(_xsize, 0); uchar *line = (uchar *)lguard.data(); unsigned x; unsigned y; if (!_rle) { _pos = _data.begin(); } for (y = 0; y < _ysize; y++) { if (_rle) { _pos = _data.begin() + *start++; } if (!getRow(line)) { return false; } c = (QRgb *)img.scanLine(_ysize - y - 1); for (x = 0; x < _xsize; x++, c++) { *c = qRgb(line[x], line[x], line[x]); } } if (_zsize == 1) { return true; } if (_zsize != 2) { for (y = 0; y < _ysize; y++) { if (_rle) { _pos = _data.begin() + *start++; } if (!getRow(line)) { return false; } c = (QRgb *)img.scanLine(_ysize - y - 1); for (x = 0; x < _xsize; x++, c++) { *c = qRgb(qRed(*c), line[x], line[x]); } } for (y = 0; y < _ysize; y++) { if (_rle) { _pos = _data.begin() + *start++; } if (!getRow(line)) { return false; } c = (QRgb *)img.scanLine(_ysize - y - 1); for (x = 0; x < _xsize; x++, c++) { *c = qRgb(qRed(*c), qGreen(*c), line[x]); } } if (_zsize == 3) { return true; } } for (y = 0; y < _ysize; y++) { if (_rle) { _pos = _data.begin() + *start++; } if (!getRow(line)) { return false; } c = (QRgb *)img.scanLine(_ysize - y - 1); for (x = 0; x < _xsize; x++, c++) { *c = qRgba(qRed(*c), qGreen(*c), qBlue(*c), line[x]); } } return true; } bool SGIImagePrivate::readImage(QImage &img) { if (!readHeader() || !isSupported()) { return false; } if (_stream.atEnd()) { return false; } img = imageAlloc(size(), format()); if (img.isNull()) { qWarning() << "Failed to allocate image, invalid dimensions?" << QSize(_xsize, _ysize); return false; } if (_zsize > 4) { // qDebug() << "using first 4 of " << _zsize << " channels"; // Only let this continue if it won't cause a int overflow later // this is most likely a broken file anyway if (_ysize > std::numeric_limits::max() / _zsize) { return false; } } _numrows = _ysize * _zsize; if (_rle) { uint l; _starttab = new quint32[_numrows]; for (l = 0; !_stream.atEnd() && l < _numrows; l++) { _stream >> _starttab[l]; _starttab[l] -= 512 + _numrows * 2 * sizeof(quint32); } for (; l < _numrows; l++) { _starttab[l] = 0; } _lengthtab = new quint32[_numrows]; for (l = 0; !_stream.atEnd() && l < _numrows; l++) { _stream >> _lengthtab[l]; } } if (_stream.status() != QDataStream::Ok) { return false; } _data = _dev->readAll(); // sanity check if (_rle) { for (uint o = 0; o < _numrows; o++) { // don't change to greater-or-equal! if (_starttab[o] + _lengthtab[o] > (uint)_data.size()) { // qDebug() << "image corrupt (sanity check failed)"; return false; } } } if (!readData(img)) { // qDebug() << "image corrupt (incomplete scanline)"; return false; } return true; } /////////////////////////////////////////////////////////////////////////////// void RLEData::write(QDataStream &s) { for (int i = 0; i < size(); i++) { s << at(i); } } bool RLEData::operator<(const RLEData &b) const { uchar ac; uchar bc; for (int i = 0; i < qMin(size(), b.size()); i++) { ac = at(i); bc = b[i]; if (ac != bc) { return ac < bc; } } return size() < b.size(); } uint RLEMap::insert(const uchar *d, uint l) { RLEData data = RLEData(d, l, _offset); Iterator it = find(data); if (it != end()) { return it.value(); } _offset += l; return QMap::insert(data, _counter++).value(); } QList RLEMap::vector() { QList v(size()); for (Iterator it = begin(); it != end(); ++it) { v.replace(it.value(), &it.key()); } return v; } uchar SGIImagePrivate::intensity(uchar c) { if (c < _pixmin) { _pixmin = c; } if (c > _pixmax) { _pixmax = c; } return c; } uint SGIImagePrivate::compact(uchar *d, uchar *s) { uchar *dest = d; uchar *src = s; uchar patt; uchar *t; uchar *end = s + _xsize; int i; int n; while (src < end) { for (n = 0, t = src; t + 2 < end && !(*t == t[1] && *t == t[2]); t++) { n++; } while (n) { i = n > 126 ? 126 : n; n -= i; *dest++ = 0x80 | i; while (i--) { *dest++ = *src++; } } if (src == end) { break; } patt = *src++; for (n = 1; src < end && *src == patt; src++) { n++; } while (n) { i = n > 126 ? 126 : n; n -= i; *dest++ = i; *dest++ = patt; } } *dest++ = 0; return dest - d; } bool SGIImagePrivate::scanData(const QImage &img) { quint32 *start = _starttab; QByteArray lineguard(_xsize * 2, 0); QByteArray bufguard(_xsize, 0); uchar *line = (uchar *)lineguard.data(); uchar *buf = (uchar *)bufguard.data(); const QRgb *c; unsigned x; unsigned y; uint len; for (y = 0; y < _ysize; y++) { const int yPos = _ysize - y - 1; // scanline doesn't do any sanity checking if (yPos >= img.height()) { qWarning() << "Failed to get scanline for" << yPos; return false; } c = reinterpret_cast(img.scanLine(yPos)); for (x = 0; x < _xsize; x++) { buf[x] = intensity(qRed(*c++)); } len = compact(line, buf); *start++ = _rlemap.insert(line, len); } if (_zsize == 1) { return true; } if (_zsize != 2) { for (y = 0; y < _ysize; y++) { const int yPos = _ysize - y - 1; if (yPos >= img.height()) { qWarning() << "Failed to get scanline for" << yPos; return false; } c = reinterpret_cast(img.scanLine(yPos)); for (x = 0; x < _xsize; x++) { buf[x] = intensity(qGreen(*c++)); } len = compact(line, buf); *start++ = _rlemap.insert(line, len); } for (y = 0; y < _ysize; y++) { const int yPos = _ysize - y - 1; if (yPos >= img.height()) { qWarning() << "Failed to get scanline for" << yPos; return false; } c = reinterpret_cast(img.scanLine(yPos)); for (x = 0; x < _xsize; x++) { buf[x] = intensity(qBlue(*c++)); } len = compact(line, buf); *start++ = _rlemap.insert(line, len); } if (_zsize == 3) { return true; } } for (y = 0; y < _ysize; y++) { const int yPos = _ysize - y - 1; if (yPos >= img.height()) { qWarning() << "Failed to get scanline for" << yPos; return false; } c = reinterpret_cast(img.scanLine(yPos)); for (x = 0; x < _xsize; x++) { buf[x] = intensity(qAlpha(*c++)); } len = compact(line, buf); *start++ = _rlemap.insert(line, len); } return true; } bool SGIImagePrivate::isValid() const { // File signature/magic number if (_magic != 0x01da) { return false; } // Compression, 0 = Uncompressed, 1 = RLE compressed if (_rle > 1) { return false; } // Bytes per pixel, 1 = 8 bit, 2 = 16 bit if (_bpc != 1 && _bpc != 2) { return false; } // Image dimension, 3 for RGBA image if (_dim < 1 || _dim > 3) { return false; } // Number channels in the image file, 4 for RGBA image if (_zsize < 1) { return false; } return true; } bool SGIImagePrivate::isSupported() const { if (!isValid()) { return false; } if (_colormap != NORMAL) { return false; // only NORMAL supported } if (_dim == 1) { return false; } return true; } bool SGIImagePrivate::peekHeader(QIODevice *device) { qint64 pos = 0; if (!device->isSequential()) { pos = device->pos(); } auto ok = false; QByteArray header; { // datastream is destroyed before working on device header = device->read(512); QDataStream ds(header); ok = SGIImagePrivate::readHeader(ds, this) && isValid(); } if (!device->isSequential()) { return device->seek(pos) && ok; } // sequential device undo auto head = header.data(); auto readBytes = header.size(); while (readBytes > 0) { device->ungetChar(head[readBytes-- - 1]); } return ok; } QSize SGIImagePrivate::size() const { return QSize(_xsize, _ysize); } QImage::Format SGIImagePrivate::format() const { if (_zsize == 2 || _zsize == 4) { return QImage::Format_ARGB32; } return QImage::Format_RGB32; } bool SGIImagePrivate::readHeader() { return readHeader(_stream, this); } bool SGIImagePrivate::readHeader(QDataStream &ds, SGIImagePrivate *sgi) { // magic ds >> sgi->_magic; // verbatim/rle ds >> sgi->_rle; // bytes per channel ds >> sgi->_bpc; // number of dimensions ds >> sgi->_dim; ds >> sgi->_xsize >> sgi->_ysize >> sgi->_zsize >> sgi->_pixmin >> sgi->_pixmax >> sgi->_unused32; // name ds.readRawData(sgi->_imagename, 80); sgi->_imagename[79] = '\0'; ds >> sgi->_colormap; for (size_t i = 0; i < sizeof(_unused); i++) { ds >> sgi->_unused[i]; } return ds.status() == QDataStream::Ok; } bool SGIImagePrivate::writeHeader() { _stream << _magic; _stream << _rle << _bpc << _dim; _stream << _xsize << _ysize << _zsize; _stream << _pixmin << _pixmax; _stream << _unused32; for (int i = 0; i < 80; i++) { _imagename[i] = '\0'; } _stream.writeRawData(_imagename, 80); _stream << _colormap; for (size_t i = 0; i < sizeof(_unused); i++) { _stream << _unused[i]; } return _stream.status() == QDataStream::Ok; } bool SGIImagePrivate::writeRle() { _rle = 1; // qDebug() << "writing RLE data"; if (!writeHeader()) { return false; } uint i; // write start table for (i = 0; i < _numrows; i++) { _stream << quint32(_rlevector[_starttab[i]]->offset()); } // write length table for (i = 0; i < _numrows; i++) { _stream << quint32(_rlevector[_starttab[i]]->size()); } // write data for (i = 0; (int)i < _rlevector.size(); i++) { const_cast(_rlevector[i])->write(_stream); } return _stream.status() == QDataStream::Ok; } bool SGIImagePrivate::writeVerbatim(const QImage &img) { _rle = 0; if (!writeHeader()) { return false; } const QRgb *c; unsigned x; unsigned y; for (y = 0; y < _ysize; y++) { c = reinterpret_cast(img.scanLine(_ysize - y - 1)); for (x = 0; x < _xsize; x++) { _stream << quint8(qRed(*c++)); } } if (_zsize == 1) { return _stream.status() == QDataStream::Ok; } if (_zsize != 2) { for (y = 0; y < _ysize; y++) { c = reinterpret_cast(img.scanLine(_ysize - y - 1)); for (x = 0; x < _xsize; x++) { _stream << quint8(qGreen(*c++)); } } for (y = 0; y < _ysize; y++) { c = reinterpret_cast(img.scanLine(_ysize - y - 1)); for (x = 0; x < _xsize; x++) { _stream << quint8(qBlue(*c++)); } } if (_zsize == 3) { return _stream.status() == QDataStream::Ok; } } for (y = 0; y < _ysize; y++) { c = reinterpret_cast(img.scanLine(_ysize - y - 1)); for (x = 0; x < _xsize; x++) { _stream << quint8(qAlpha(*c++)); } } return _stream.status() == QDataStream::Ok; } bool SGIImagePrivate::writeImage(const QImage &image) { // qDebug() << "writing "; // TODO add filename QImage img = image; if (img.allGray()) { _dim = 2, _zsize = 1; } else { _dim = 3, _zsize = 3; } auto hasAlpha = img.hasAlphaChannel(); if (hasAlpha) { _dim = 3, _zsize++; } if (hasAlpha && img.format() != QImage::Format_ARGB32) { img = img.convertToFormat(QImage::Format_ARGB32); } else if (!hasAlpha && img.format() != QImage::Format_RGB32) { img = img.convertToFormat(QImage::Format_RGB32); } if (img.isNull()) { // qDebug() << "can't convert image to depth 32"; return false; } const int w = img.width(); const int h = img.height(); if (w > 65535 || h > 65535) { return false; } _magic = 0x01da; _bpc = 1; _xsize = w; _ysize = h; _pixmin = ~0u; _pixmax = 0; _colormap = NORMAL; _numrows = _ysize * _zsize; _starttab = new quint32[_numrows]; _rlemap.setBaseOffset(512 + _numrows * 2 * sizeof(quint32)); if (!scanData(img)) { // qDebug() << "this can't happen"; return false; } _rlevector = _rlemap.vector(); long verbatim_size = _numrows * _xsize; long rle_size = _numrows * 2 * sizeof(quint32); for (int i = 0; i < _rlevector.size(); i++) { rle_size += _rlevector[i]->size(); } if (verbatim_size <= rle_size) { return writeVerbatim(img); } return writeRle(); } /////////////////////////////////////////////////////////////////////////////// RGBHandler::RGBHandler() : QImageIOHandler() , d(new SGIImagePrivate) { } bool RGBHandler::canRead() const { if (canRead(device())) { setFormat("rgb"); return true; } return false; } bool RGBHandler::read(QImage *outImage) { d->setDevice(device()); return d->readImage(*outImage); } bool RGBHandler::write(const QImage &image) { d->setDevice(device()); return d->writeImage(image); } bool RGBHandler::supportsOption(ImageOption option) const { if (option == QImageIOHandler::Size) { return true; } if (option == QImageIOHandler::ImageFormat) { return true; } return false; } QVariant RGBHandler::option(ImageOption option) const { QVariant v; if (option == QImageIOHandler::Size) { auto &&sgi = d; if (sgi->isSupported()) { v = QVariant::fromValue(sgi->size()); } else if (auto dev = device()) { if (d->peekHeader(dev) && sgi->isSupported()) { v = QVariant::fromValue(sgi->size()); } } } if (option == QImageIOHandler::ImageFormat) { auto &&sgi = d; if (sgi->isSupported()) { v = QVariant::fromValue(sgi->format()); } else if (auto dev = device()) { if (d->peekHeader(dev) && sgi->isSupported()) { v = QVariant::fromValue(sgi->format()); } } } return v; } bool RGBHandler::canRead(QIODevice *device) { if (!device) { qWarning("RGBHandler::canRead() called with no device"); return false; } SGIImagePrivate sgi; return sgi.peekHeader(device) && sgi.isSupported(); } /////////////////////////////////////////////////////////////////////////////// QImageIOPlugin::Capabilities RGBPlugin::capabilities(QIODevice *device, const QByteArray &format) const { if (format == "rgb" || format == "rgba" || format == "bw" || format == "sgi") { return Capabilities(CanRead | CanWrite); } if (!format.isEmpty()) { return {}; } if (!device->isOpen()) { return {}; } Capabilities cap; if (device->isReadable() && RGBHandler::canRead(device)) { cap |= CanRead; } if (device->isWritable()) { cap |= CanWrite; } return cap; } QImageIOHandler *RGBPlugin::create(QIODevice *device, const QByteArray &format) const { QImageIOHandler *handler = new RGBHandler; handler->setDevice(device); handler->setFormat(format); return handler; } #include "moc_rgb_p.cpp"