#include "qndsimage.h" QNDSImage::QNDSImage() {} QNDSImage::QNDSImage(const QImage& img, const QVector& pal, int alphaThreshold) { replace(img, pal, alphaThreshold); } QNDSImage::QNDSImage(const QImage& img, int colorCount, int alphaThreshold) { replace(img, colorCount, alphaThreshold); } QNDSImage::QNDSImage(const QVector& ncg, const QVector& ncl, bool is4bpp) { replace(ncg, ncl, is4bpp); } u16 QNDSImage::toRgb15(u32 rgb24) { u8 r, g, b; r = (rgb24 >> 16) & 0xFF; g = (rgb24 >> 8) & 0xFF; b = (rgb24 >> 0) & 0xFF; r = qRound(r * 31.0 / 255.0); g = qRound(g * 31.0 / 255.0); b = qRound(b * 31.0 / 255.0); return (b << 10) | (g << 5) | r; } u32 QNDSImage::toRgb24(u16 rgb15) { u8 r, g, b; r = (rgb15 >> 0) & 0x1F; g = (rgb15 >> 5) & 0x1F; b = (rgb15 >> 10) & 0x1F; r = qRound(r * 255.0 / 31.0); g = qRound(g * 255.0 / 31.0); b = qRound(b * 255.0 / 31.0); return (r << 16) | (g << 8) | b; } void QNDSImage::replace(const QImage& img, const QVector& pal, int alphaThreshold) { palette = pal; const int newPalSize = pal.size(); QVector newPal24(newPalSize); for (int i = 0; i < newPalSize; i++) newPal24[i] = toRgb24(pal[i]); const int width = img.width(); const int height = img.height(); texture.resize(width * height); if(img.depth() == 8 && img.colorCount() <= 16) { for (int i = 0, y = 0; y < height; y++) for (int x = 0; x < width; x++, i++) texture[i] = img.pixelIndex(x, y); } else { for (int i = 0, y = 0; y < height; y++) for (int x = 0; x < width; x++, i++) texture[i] = closestMatch(img.pixelColor(x, y), newPal24, alphaThreshold); } texture = getTiled(width / 8, true); } void QNDSImage::replace(const QImage& img, int colorCount, int alphaThreshold) { const int width = img.width(); const int height = img.height(); QVector pal; pal.append(QColor(0xFF, 0x00, 0xFF, 0x00)); //Make transparent the first color if(img.depth() == 8) { // We already have a palette, just use it if(img.colorCount() <= 16) // Valid palette, don't add transparent pal.clear(); for(const QRgb &color : img.colorTable()) pal.append(color); } else { // No palette, get all the colors QColor magenta = QColor(0xFF, 0x00, 0xFF, 0xFF); for(int y = 0; y < height; y++) { for(int x = 0; x < width; x++) { QColor c = img.pixelColor(x, y); if(!pal.contains(c) && c.alpha() >= alphaThreshold && c != magenta) pal.append(c); } } } QVector newPal = createPalette(pal, colorCount); replace(img, newPal, alphaThreshold); } void QNDSImage::replace(const QVector& ncg, const QVector& ncl, bool is4bpp) { if(is4bpp) { const int texSize = ncg.size() << 1; texture.resize(texSize); for (int i = 0, j = 0; i < texSize; i += 2, j++) { texture[i] = ncg[j] & 0xF; texture[i + 1] = (ncg[j] >> 4) & 0xF; } } else texture = ncg; palette = ncl; } QVector QNDSImage::getTiled(int tileWidth, bool inverse) { const int textureSize = texture.size(); const int width = tileWidth * 8; const int height = textureSize / width; QVector out(textureSize); for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { u32 bf = x + y * width; u32 tf = bf % 64; u32 tx = tf % 8; u32 ty = tf / 8; u32 gf = bf / 64; u32 gx = (gf % tileWidth) * 8; u32 gy = (gf / tileWidth) * 8; u32 dsti, srci; if (inverse) { dsti = x + y * width; srci = (gx + tx) + (gy + ty) * width; } else { dsti = (gx + tx) + (gy + ty) * width; srci = x + y * width; } out[dsti] = texture[srci]; } } return out; } #define _QNDSIMAGE_PLTT_IMAGE_DEBUG 0 QImage QNDSImage::toImage(int tileWidth) { #if _QNDSIMAGE_PLTT_IMAGE_DEBUG QImage out(16, 16, QImage::Format_ARGB32); out.fill(Qt::transparent); for (int i = 0, y = 0; y < 16; y++) { for (int x = 0; x < 16; x++, i++) { if (i == palette.size()) break; out.setPixelColor(x, y, toRgb24(palette[i])); } } return out; #else const int width = tileWidth * 8; const int height = texture.size() / width; QImage out(width, height, QImage::Format_Indexed8); QVector pal; for(u16 color : this->palette) pal.append(0xFF000000 | toRgb24(color)); pal[0] &= 0x00FFFFFF; // Make color 0 transparent out.setColorTable(pal); QVector tiled = getTiled(tileWidth, false); for(int i = 0, y = 0; y < height; y++) { for(int x = 0; x < width; x++, i++) { out.setPixel(x, y, tiled[i]); } } return out; #endif } void QNDSImage::toNitro(QVector& ncg, QVector& ncl, bool is4bpp) { if(is4bpp) { const int texSize = texture.size() >> 1; ncg.resize(texSize); for (int i = 0, j = 0; i < texSize; i++, j += 2) { ncg[i] = texture[j] & 0xF; ncg[i] |= (texture[j + 1] & 0xF) << 4; } } else ncg = texture; ncl = palette; } QVector QNDSImage::createPalette(QVector pal, int colorCount) { if(pal.size() < colorCount) pal.resize(colorCount); // Sort colors and reduce to needed amount if(pal.size() > colorCount) { // For finding color channel that has the most wide range, // we need to keep their lower and upper bound. int lower_red = pal[0].red(), lower_green = pal[0].green(), lower_blue = pal[0].blue(); int upper_red = 0, upper_green = 0, upper_blue = 0; // Loop trough all the colors for (QColor c : pal) { lower_red = std::min(lower_red, c.red()); lower_green = std::min(lower_green, c.green()); lower_blue = std::min(lower_blue, c.blue()); upper_red = std::max(upper_red, c.red()); upper_green = std::max(upper_green, c.green()); upper_blue = std::max(upper_blue, c.blue()); } int red = upper_red - lower_red; int green = upper_green - lower_green; int blue = upper_blue - lower_blue; int max = std::max(std::max(red, green), blue); // Compare two rgb color according to our selected color channel. std::sort(pal.begin(), pal.end(), [max, red, green/*, blue*/](const QColor& c1, const QColor& c2) { // Always keep transparent at the start if(c1.alpha() == 0) return true; else if(c2.alpha() == 0) return false; if (max == red) // if red is our color that has the widest range return c1.red() < c2.red(); // just compare their red channel else if (max == green) //... return c1.green() < c2.green(); else //if (max == blue) return c1.blue() < c2.blue(); }); // Reduce to the desired number of colors double groupSize = pal.size() / colorCount; for (int i = 0; i < colorCount; ++i) palette.append(toRgb15(pal[qRound((groupSize * i) + (groupSize / 2))].rgb())); } else { // Already a valid palette, just use it for (int i = 0; i < colorCount; ++i) { palette.append(toRgb15(pal[i].rgb())); } } return palette; } inline int QNDSImage::pixelDistance(QColor p1, QColor p2) { int r1 = p1.red(); int g1 = p1.green(); int b1 = p1.blue(); int a1 = p1.alpha(); int r2 = p2.red(); int g2 = p2.green(); int b2 = p2.blue(); int a2 = p2.alpha(); return abs(r1 - r2) + abs(g1 - g2) + abs(b1 - b2) + abs(a1 - a2); } inline int QNDSImage::closestMatch(QColor pixel, const QVector& clut, int alphaThreshold) { // If it's transparency, index 0 if(pixel.alpha() < alphaThreshold || pixel == QColor(0xFF, 0x00, 0xFF, 0xFF)) { return 0; } // Otherwise find the closest match int idx = 0; int current_distance = INT_MAX; for (int i = 1; i < clut.size(); ++i) { int dist = pixelDistance(pixel, clut[i]); if (dist < current_distance) { current_distance = dist; idx = i; } } return idx; }