From ebc366b3c59c655e8eff49ec8ab2d54de23da298 Mon Sep 17 00:00:00 2001 From: Mirco Miranda Date: Wed, 20 Aug 2025 23:53:00 +0200 Subject: [PATCH] TGA: memory optimizations and native grayscale support - Removed temporary buffer of uncompressed image size when reading (loads images using half the memory) - Added native support for Grayscale images (they are no longer converted to RGB(A) 32 when reading/writing) - Fixes wrong X channel value on RGBX32 images (CCBUG: 499584) It should also reduce loading times for corrupted images. Closes #33 --- .../write/format/tga/Format_Grayscale16.tga | Bin 27666 -> 9234 bytes .../write/format/tga/Format_Grayscale8.tga | Bin 27666 -> 9234 bytes src/imageformats/tga.cpp | 289 +++++++++++------- src/imageformats/tga_p.h | 2 + 4 files changed, 173 insertions(+), 118 deletions(-) diff --git a/autotests/write/format/tga/Format_Grayscale16.tga b/autotests/write/format/tga/Format_Grayscale16.tga index 3d2196d9dfc1c1c13ecf37faea98c0b1f67039eb..b35b686159c3a6c7cab5d50bf9a8999bf2a1b8f9 100644 GIT binary patch literal 9234 zcmeI1`)eFk6vqP=tQKEIMe)(nVyULKRIG@C#s?JC(m*X1sghd!K~xYFy3KC(PB(2f zYqIUqm^O9mqogTKs!a^0jgcg3BC*>%DfsxsUqt*d`WIZ!YD0Su9Z@<13K6HAVq-=j)(ohwn}+{CQGzKbMj1DACt;{ zBga#^$JnokKbGq-M{gaQ%1Ypx@1nmve`H?bK<}sfwbb1*{KE@hmLRLRUAZpL=q33{ z*;6VvsrW*)E#~{~e1Pv1Op+`{<~V(Ic&gVqoDD#h+;dZ8(Z=KFnMax;_Rz;%nfG zrIP$%iTGOh;y((&oWlJ3vh1&3A0ezs3}ED^hLQpBJ9~9LrwT2s??1_;a|s>a>r0a@ zs}K3Q3AAK#8bSn+zh(Vt>!X4&|I_ZE_|_%+gy8_p{|WHH{H>J3hb_P&5k8Rmq53Ciet#uCSA2oS5q;Mu%|$xYPcL5N`EyX8Xp+VRz@KyT z`-0|MpXm8tw1Vb~d7b|U?%zQ55uY)irG{4V>6+Jt+vhbyO>N`V=hlHhN3E~!5|?3; zo`02m3a$7;{B75v6u8)d^V?d5dI)hm48AyH^9S(30?%LkVDlHk;m;x;mJ(X9FzScv zM4k;Nf00)FV5-kT0eDuNl z@4UV5jXke6zOwy=XPTvmg||+r&S3;6vi?b?5Jcf7#}Xeh8rJZ|9HtXne~5e%5DYfjDp}F-l#pIO`}Z}yx%c%w zyLau}@$&W;pL^!%Cm-LsrT*a!>(@g0+*vpJz$aVwumqXM&1Ie&i7dbpb3ebo`23lRvTk04 zGJIOc$fY>)sb9#C{a8Cca}+Do%#S8&=BJM#-isfV_){sW*QaBU@ZS8cuWI3U3~Rr? zbbj(eLyNmB8jZ)>hvIQ|7C(j8`ttZ#ES-(TRQ?VZIw*-cpX2d(VkXWWAABOj272K8 zi>{9fLU--_A%gne=a)axj^B?e;KwC?UNfw+r#?9RFQiwgWlRUwf7c@@GOu&W`qHGcqAWYn$xp a`*tSh@kFkOz=u*%QK)FH_j&I)I{Xirl={U0 literal 27666 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 vI!+VB5hQvZquKD~2fB5dC diff --git a/autotests/write/format/tga/Format_Grayscale8.tga b/autotests/write/format/tga/Format_Grayscale8.tga index 3d2196d9dfc1c1c13ecf37faea98c0b1f67039eb..b35b686159c3a6c7cab5d50bf9a8999bf2a1b8f9 100644 GIT binary patch literal 9234 zcmeI1`)eFk6vqP=tQKEIMe)(nVyULKRIG@C#s?JC(m*X1sghd!K~xYFy3KC(PB(2f zYqIUqm^O9mqogTKs!a^0jgcg3BC*>%DfsxsUqt*d`WIZ!YD0Su9Z@<13K6HAVq-=j)(ohwn}+{CQGzKbMj1DACt;{ zBga#^$JnokKbGq-M{gaQ%1Ypx@1nmve`H?bK<}sfwbb1*{KE@hmLRLRUAZpL=q33{ z*;6VvsrW*)E#~{~e1Pv1Op+`{<~V(Ic&gVqoDD#h+;dZ8(Z=KFnMax;_Rz;%nfG zrIP$%iTGOh;y((&oWlJ3vh1&3A0ezs3}ED^hLQpBJ9~9LrwT2s??1_;a|s>a>r0a@ zs}K3Q3AAK#8bSn+zh(Vt>!X4&|I_ZE_|_%+gy8_p{|WHH{H>J3hb_P&5k8Rmq53Ciet#uCSA2oS5q;Mu%|$xYPcL5N`EyX8Xp+VRz@KyT z`-0|MpXm8tw1Vb~d7b|U?%zQ55uY)irG{4V>6+Jt+vhbyO>N`V=hlHhN3E~!5|?3; zo`02m3a$7;{B75v6u8)d^V?d5dI)hm48AyH^9S(30?%LkVDlHk;m;x;mJ(X9FzScv zM4k;Nf00)FV5-kT0eDuNl z@4UV5jXke6zOwy=XPTvmg||+r&S3;6vi?b?5Jcf7#}Xeh8rJZ|9HtXne~5e%5DYfjDp}F-l#pIO`}Z}yx%c%w zyLau}@$&W;pL^!%Cm-LsrT*a!>(@g0+*vpJz$aVwumqXM&1Ie&i7dbpb3ebo`23lRvTk04 zGJIOc$fY>)sb9#C{a8Cca}+Do%#S8&=BJM#-isfV_){sW*QaBU@ZS8cuWI3U3~Rr? zbbj(eLyNmB8jZ)>hvIQ|7C(j8`ttZ#ES-(TRQ?VZIw*-cpX2d(VkXWWAABOj272K8 zi>{9fLU--_A%gne=a)axj^B?e;KwC?UNfw+r#?9RFQiwgWlRUwf7c@@GOu&W`qHGcqAWYn$xp a`*tSh@kFkOz=u*%QK)FH_j&I)I{Xirl={U0 literal 27666 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 vI!+VB5hQvZquKD~2fB5dC diff --git a/src/imageformats/tga.cpp b/src/imageformats/tga.cpp index 823d2a9..22de94a 100644 --- a/src/imageformats/tga.cpp +++ b/src/imageformats/tga.cpp @@ -2,6 +2,7 @@ This file is part of the KDE project SPDX-FileCopyrightText: 2003 Dominik Seichter SPDX-FileCopyrightText: 2004 Ignacio CastaƱo + SPDX-FileCopyrightText: 2025 Mirco Miranda SPDX-License-Identifier: LGPL-2.0-or-later */ @@ -14,6 +15,8 @@ * pixel formats 8, 16, 24 and 32. * writing: * uncompressed true color tga files + * uncompressed grayscale tga files + * uncompressed indexed tga files */ #include "tga_p.h" @@ -191,11 +194,13 @@ static QImage::Format imageFormat(const TgaHeader &head) format = QImage::Format_ARGB32; } // Anyway, GIMP also saves gray images with alpha in TGA format - } else if((info.grey) && (head.pixel_size == 16) && (numAlphaBits)) { + } else if ((info.grey) && (head.pixel_size == 16) && (numAlphaBits)) { if (numAlphaBits == 8) { format = QImage::Format_ARGB32; } - } else if (head.image_type == TGA_TYPE_INDEXED || head.image_type == TGA_TYPE_RLE_INDEXED) { + } else if (info.grey) { + format = QImage::Format_Grayscale8; + } else if (info.pal) { format = QImage::Format_Indexed8; } else { format = QImage::Format_RGB32; @@ -220,20 +225,101 @@ static bool peekHeader(QIODevice *device, TgaHeader &header) return true; } -static bool LoadTGA(QDataStream &s, const TgaHeader &tga, QImage &img) +/*! + * \brief readTgaLine + * Read a scan line from the raw data. + * \param dev The current device. + * \param pixel_size The number of bytes per pixel. + * \param size The size of the uncompressed TGA raw line + * \param rle True if the stream is RLE compressed, otherwise false. + * \param cache The cache buffer used to store data (only used when the stream is RLE). + * \return The uncompressed raw data of a line or an empty array on error. + */ +static QByteArray readTgaLine(QIODevice *dev, qint32 pixel_size, qint32 size, bool rle, QByteArray &cache) +{ + // uncompressed stream + if (!rle) { + auto ba = dev->read(size); + if (ba.size() != size) + ba.clear(); + return ba; + } + + // RLE compressed stream + if (cache.size() < qsizetype(size)) { + // Decode image. + qint64 num = size; + + while (num > 0) { + if (dev->atEnd()) { + break; + } + + // Get packet header. + char cc; + if (dev->read(&cc, 1) != 1) { + cache.clear(); + break; + } + auto c = uchar(cc); + + uint count = (c & 0x7f) + 1; + QByteArray tmp(count * pixel_size, char()); + auto dst = tmp.data(); + num -= count * pixel_size; + + if (c & 0x80) { // RLE pixels. + assert(pixel_size <= 8); + char pixel[8]; + const int dataRead = dev->read(pixel, pixel_size); + if (dataRead < (int)pixel_size) { + memset(&pixel[dataRead], 0, pixel_size - dataRead); + } + do { + memcpy(dst, pixel, pixel_size); + dst += pixel_size; + } while (--count); + } else { // Raw pixels. + count *= pixel_size; + const int dataRead = dev->read(dst, count); + if (dataRead < 0) { + cache.clear(); + break; + } + + if ((uint)dataRead < count) { + const size_t toCopy = count - dataRead; + memset(&dst[dataRead], 0, toCopy); + } + dst += count; + } + + cache.append(tmp); + } + } + + auto data = cache.left(size); + cache.remove(0, size); + if (data.size() != size) + data.clear(); + return data; +} + +static bool LoadTGA(QIODevice *dev, const TgaHeader &tga, QImage &img) { img = imageAlloc(tga.width, tga.height, imageFormat(tga)); if (img.isNull()) { - qWarning() << "Failed to allocate image, invalid dimensions?" << QSize(tga.width, tga.height); + qWarning() << "LoadTGA: Failed to allocate image, invalid dimensions?" << QSize(tga.width, tga.height); return false; } TgaHeaderInfo info(tga); const int numAlphaBits = tga.flags & 0xf; - uint pixel_size = (tga.pixel_size / 8); - qint64 size = qint64(tga.width) * qint64(tga.height) * pixel_size; - + bool hasAlpha = img.hasAlphaChannel(); + qint32 pixel_size = (tga.pixel_size / 8); + qint32 line_size = qint32(tga.width) * pixel_size; + qint64 size = qint64(tga.height) * line_size; if (size < 1) { // qDebug() << "This TGA file is broken with size " << size; return false; @@ -251,7 +337,7 @@ static bool LoadTGA(QDataStream &s, const TgaHeader &tga, QImage &img) if (tga.colormap_size == 32) { // BGRA. char data[4]; for (QRgb &rgb : colorTable) { - const auto dataRead = s.readRawData(data, 4); + const auto dataRead = dev->read(data, 4); if (dataRead < 4) { return false; } @@ -261,7 +347,7 @@ static bool LoadTGA(QDataStream &s, const TgaHeader &tga, QImage &img) } else if (tga.colormap_size == 24) { // BGR. char data[3]; for (QRgb &rgb : colorTable) { - const auto dataRead = s.readRawData(data, 3); + const auto dataRead = dev->read(data, 3); if (dataRead < 3) { return false; } @@ -276,96 +362,8 @@ static bool LoadTGA(QDataStream &s, const TgaHeader &tga, QImage &img) img.setColorTable(colorTable); } - // Allocate image. - uchar *const image = reinterpret_cast(malloc(size)); - if (!image) { - return false; - } - - bool valid = true; - - if (info.rle) { - // Decode image. - char *dst = (char *)image; - char *imgEnd = dst + size; - qint64 num = size; - - while (num > 0 && valid) { - if (s.atEnd()) { - valid = false; - break; - } - - // Get packet header. - uchar c; - s >> c; - - uint count = (c & 0x7f) + 1; - num -= count * pixel_size; - if (num < 0) { - valid = false; - break; - } - - if (c & 0x80) { - // RLE pixels. - assert(pixel_size <= 8); - char pixel[8]; - const int dataRead = s.readRawData(pixel, pixel_size); - if (dataRead < (int)pixel_size) { - memset(&pixel[dataRead], 0, pixel_size - dataRead); - } - do { - if (dst + pixel_size > imgEnd) { - qWarning() << "Trying to write out of bounds!" << ptrdiff_t(dst) << (ptrdiff_t(imgEnd) - ptrdiff_t(pixel_size)); - valid = false; - break; - } - - memcpy(dst, pixel, pixel_size); - dst += pixel_size; - } while (--count); - } else { - // Raw pixels. - count *= pixel_size; - const int dataRead = s.readRawData(dst, count); - if (dataRead < 0) { - free(image); - return false; - } - - if ((uint)dataRead < count) { - const size_t toCopy = count - dataRead; - if (&dst[dataRead] + toCopy > imgEnd) { - qWarning() << "Trying to write out of bounds!" << ptrdiff_t(image) << ptrdiff_t(&dst[dataRead]); - ; - valid = false; - break; - } - - memset(&dst[dataRead], 0, toCopy); - } - dst += count; - } - } - } else { - // Read raw image. - const int dataRead = s.readRawData((char *)image, size); - if (dataRead < 0) { - free(image); - return false; - } - if (dataRead < size) { - memset(&image[dataRead], 0, size - dataRead); - } - } - - if (!valid) { - free(image); - return false; - } - // Convert image to internal format. + bool valid = true; int y_start; int y_step; int y_end; @@ -379,9 +377,15 @@ static bool LoadTGA(QDataStream &s, const TgaHeader &tga, QImage &img) y_end = -1; } - uchar *src = image; - + QByteArray cache; for (int y = y_start; y != y_end; y += y_step) { + auto tgaLine = readTgaLine(dev, pixel_size, line_size, info.rle, cache); + if (tgaLine.size() != qsizetype(line_size)) { + qWarning() << "LoadTGA: Error while decoding a TGA raw line"; + valid = false; + break; + } + auto src = tgaLine.data(); if (info.pal) { // Paletted. auto scanline = img.scanLine(y); @@ -394,15 +398,16 @@ static bool LoadTGA(QDataStream &s, const TgaHeader &tga, QImage &img) scanline[x] = idx; } } else if (info.grey) { - auto scanline = reinterpret_cast(img.scanLine(y)); - // Greyscale. - for (int x = 0; x < tga.width; x++) { - if (tga.pixel_size == 16) { + if (tga.pixel_size == 16) { // Greyscale with alpha. + auto scanline = reinterpret_cast(img.scanLine(y)); + for (int x = 0; x < tga.width; x++) { scanline[x] = qRgba(*src, *src, *src, *(src + 1)); src += 2; } - else { - scanline[x] = qRgb(*src, *src, *src); + } else { // Greyscale. + auto scanline = img.scanLine(y); + for (int x = 0; x < tga.width; x++) { + scanline[x] = *src; src++; } } @@ -421,9 +426,12 @@ static bool LoadTGA(QDataStream &s, const TgaHeader &tga, QImage &img) src += 3; } } else if (tga.pixel_size == 32) { + auto div = (1 << numAlphaBits) - 1; + if (div == 0) + hasAlpha = false; for (int x = 0; x < tga.width; x++) { // ### TODO: verify with images having really some alpha data - const uchar alpha = (src[3] << (8 - numAlphaBits)); + const int alpha = hasAlpha ? int((src[3]) << (8 - numAlphaBits)) * 255 / div : 255; scanline[x] = qRgba(src[2], src[1], src[0], alpha); src += 4; } @@ -431,8 +439,11 @@ static bool LoadTGA(QDataStream &s, const TgaHeader &tga, QImage &img) } } - // Free image. - free(image); +#ifdef QT_DEBUG + if (!cache.isEmpty() && valid) { + qDebug() << "LoadTGA: Found unused image data"; + } +#endif return valid; } @@ -480,19 +491,14 @@ bool TGAHandler::read(QImage *outImage) dev->seek(TgaHeader::SIZE + tga.id_length); } - QDataStream s(dev); - s.setByteOrder(QDataStream::LittleEndian); - // Check image file format. - if (s.atEnd()) { + if (dev->atEnd()) { // qDebug() << "This TGA file is not valid."; return false; } QImage img; - bool result = LoadTGA(s, tga, img); - - if (result == false) { + if (!LoadTGA(dev, tga, img)) { // qDebug() << "Error loading TGA file."; return false; } @@ -505,6 +511,8 @@ bool TGAHandler::write(const QImage &image) { if (image.format() == QImage::Format_Indexed8) return writeIndexed(image); + if (image.format() == QImage::Format_Grayscale8 || image.format() == QImage::Format_Grayscale16) + return writeGrayscale(image); return writeRGBA(image); } @@ -527,7 +535,7 @@ bool TGAHandler::writeIndexed(const QImage &image) s << quint16(img.width()); // Image Width s << quint16(img.height()); // Image Height - s << quint8(8); // Pixe Depth + s << quint8(8); // Pixel Depth s << quint8(TGA_ORIGIN_UPPER + TGA_ORIGIN_LEFT); // Image Descriptor for (auto &&rgb : ct) { @@ -554,6 +562,51 @@ bool TGAHandler::writeIndexed(const QImage &image) return true; } +bool TGAHandler::writeGrayscale(const QImage &image) +{ + QDataStream s(device()); + s.setByteOrder(QDataStream::LittleEndian); + + QImage img(image); + if (img.format() != QImage::Format_Grayscale8) { + img = img.convertToFormat(QImage::Format_Grayscale8); + } + if (img.isNull()) { + qCritical() << "TGAHandler::writeGrayscale: image conversion to 8 bits grayscale failed!"; + return false; + } + + s << quint8(0); // ID Length + s << quint8(0); // Color Map Type + s << quint8(TGA_TYPE_GREY); // Image Type + s << quint16(0); // First Entry Index + s << quint16(0); // Color Map Length + s << quint8(0); // Color map Entry Size + s << quint16(0); // X-origin of Image + s << quint16(0); // Y-origin of Image + + s << quint16(img.width()); // Image Width + s << quint16(img.height()); // Image Height + s << quint8(8); // Pixel Depth + s << quint8(TGA_ORIGIN_UPPER + TGA_ORIGIN_LEFT); // Image Descriptor + + if (s.status() != QDataStream::Ok) { + return false; + } + + for (int y = 0; y < img.height(); y++) { + auto ptr = img.constScanLine(y); + for (int x = 0; x < img.width(); x++) { + s << *(ptr + x); + } + if (s.status() != QDataStream::Ok) { + return false; + } + } + + return true; +} + bool TGAHandler::writeRGBA(const QImage &image) { QDataStream s(device()); @@ -574,7 +627,7 @@ bool TGAHandler::writeRGBA(const QImage &image) img = img.convertToFormat(QImage::Format_RGB32); } if (img.isNull()) { - qDebug() << "TGAHandler::write: image conversion to 32 bits failed!"; + qCritical() << "TGAHandler::writeRGBA: image conversion to 32 bits failed!"; return false; } static constexpr quint8 originTopLeft = TGA_ORIGIN_UPPER + TGA_ORIGIN_LEFT; // 0x20 diff --git a/src/imageformats/tga_p.h b/src/imageformats/tga_p.h index 27e5bc7..aaafbf0 100644 --- a/src/imageformats/tga_p.h +++ b/src/imageformats/tga_p.h @@ -29,6 +29,8 @@ public: private: bool writeIndexed(const QImage &image); + bool writeGrayscale(const QImage &image); + bool writeRGBA(const QImage &image); const QScopedPointer d;