706 lines
25 KiB
C++
706 lines
25 KiB
C++
|
|
#include "imagepreviewwidget.h"
|
||
|
|
#include <QImageReader>
|
||
|
|
#include <QBuffer>
|
||
|
|
#include <QFileInfo>
|
||
|
|
#include <QDir>
|
||
|
|
#include <QScrollBar>
|
||
|
|
#include <QWheelEvent>
|
||
|
|
#include <QEvent>
|
||
|
|
#include "statusbarmanager.h"
|
||
|
|
|
||
|
|
ImagePreviewWidget::ImagePreviewWidget(QWidget *parent)
|
||
|
|
: QWidget(parent)
|
||
|
|
, mImageLabel(new QLabel(this))
|
||
|
|
, mInfoLabel(new QLabel(this))
|
||
|
|
, mScrollArea(new QScrollArea(this))
|
||
|
|
, mDragging(false)
|
||
|
|
, mZoomFactor(1.0)
|
||
|
|
{
|
||
|
|
mImageLabel->setAlignment(Qt::AlignCenter);
|
||
|
|
mImageLabel->setBackgroundRole(QPalette::Base);
|
||
|
|
mImageLabel->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
|
||
|
|
mImageLabel->setScaledContents(false);
|
||
|
|
|
||
|
|
mScrollArea->setWidget(mImageLabel);
|
||
|
|
mScrollArea->setWidgetResizable(false); // Don't auto-resize, allow scrolling
|
||
|
|
mScrollArea->setBackgroundRole(QPalette::Dark);
|
||
|
|
mScrollArea->setAlignment(Qt::AlignCenter);
|
||
|
|
mScrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||
|
|
mScrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||
|
|
|
||
|
|
mInfoLabel->setAlignment(Qt::AlignCenter);
|
||
|
|
mInfoLabel->setStyleSheet("QLabel { background-color: #333; color: #fff; padding: 4px; }");
|
||
|
|
|
||
|
|
QVBoxLayout *layout = new QVBoxLayout(this);
|
||
|
|
layout->setContentsMargins(0, 0, 0, 0);
|
||
|
|
layout->setSpacing(0);
|
||
|
|
layout->addWidget(mInfoLabel);
|
||
|
|
layout->addWidget(mScrollArea, 1);
|
||
|
|
setLayout(layout);
|
||
|
|
|
||
|
|
// Enable mouse tracking for drag-to-pan
|
||
|
|
setMouseTracking(true);
|
||
|
|
mScrollArea->setMouseTracking(true);
|
||
|
|
mScrollArea->viewport()->installEventFilter(this);
|
||
|
|
mScrollArea->viewport()->setCursor(Qt::OpenHandCursor);
|
||
|
|
}
|
||
|
|
|
||
|
|
void ImagePreviewWidget::setFilename(const QString &filename)
|
||
|
|
{
|
||
|
|
mFilename = filename;
|
||
|
|
}
|
||
|
|
|
||
|
|
QSize ImagePreviewWidget::imageSize() const
|
||
|
|
{
|
||
|
|
return mImageSize;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool ImagePreviewWidget::exportDebugImage(const QString &suffix)
|
||
|
|
{
|
||
|
|
QPixmap pixmap = mImageLabel->pixmap();
|
||
|
|
if (pixmap.isNull()) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
QDir().mkdir("exports");
|
||
|
|
|
||
|
|
QString filename = mFilename;
|
||
|
|
if (filename.isEmpty()) {
|
||
|
|
filename = "debug_image";
|
||
|
|
}
|
||
|
|
// Remove extension and add suffix
|
||
|
|
int dotPos = filename.lastIndexOf('.');
|
||
|
|
if (dotPos > 0) {
|
||
|
|
filename = filename.left(dotPos);
|
||
|
|
}
|
||
|
|
if (!suffix.isEmpty()) {
|
||
|
|
filename += "_" + suffix;
|
||
|
|
}
|
||
|
|
filename += "_DEBUG.png";
|
||
|
|
|
||
|
|
QString path = "exports/" + filename;
|
||
|
|
bool success = pixmap.save(path, "PNG");
|
||
|
|
if (success) {
|
||
|
|
StatusBarManager::instance().updateStatus(QString("Exported debug image: %1").arg(path), 2000);
|
||
|
|
}
|
||
|
|
return success;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool ImagePreviewWidget::eventFilter(QObject *obj, QEvent *event)
|
||
|
|
{
|
||
|
|
if (obj == mScrollArea->viewport()) {
|
||
|
|
if (event->type() == QEvent::MouseButtonPress) {
|
||
|
|
QMouseEvent *me = static_cast<QMouseEvent*>(event);
|
||
|
|
if (me->button() == Qt::LeftButton) {
|
||
|
|
mDragging = true;
|
||
|
|
mLastDragPos = me->globalPosition().toPoint();
|
||
|
|
mScrollArea->viewport()->setCursor(Qt::ClosedHandCursor);
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
} else if (event->type() == QEvent::MouseMove) {
|
||
|
|
if (mDragging) {
|
||
|
|
QMouseEvent *me = static_cast<QMouseEvent*>(event);
|
||
|
|
QPoint globalPos = me->globalPosition().toPoint();
|
||
|
|
QPoint delta = globalPos - mLastDragPos;
|
||
|
|
mLastDragPos = globalPos;
|
||
|
|
|
||
|
|
QScrollBar *hBar = mScrollArea->horizontalScrollBar();
|
||
|
|
QScrollBar *vBar = mScrollArea->verticalScrollBar();
|
||
|
|
hBar->setValue(hBar->value() - delta.x());
|
||
|
|
vBar->setValue(vBar->value() - delta.y());
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
} else if (event->type() == QEvent::MouseButtonRelease) {
|
||
|
|
QMouseEvent *me = static_cast<QMouseEvent*>(event);
|
||
|
|
if (me->button() == Qt::LeftButton) {
|
||
|
|
mDragging = false;
|
||
|
|
mScrollArea->viewport()->setCursor(Qt::OpenHandCursor);
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
} else if (event->type() == QEvent::Wheel) {
|
||
|
|
QWheelEvent *we = static_cast<QWheelEvent*>(event);
|
||
|
|
int delta = we->angleDelta().y();
|
||
|
|
if (delta > 0) {
|
||
|
|
mZoomFactor *= 1.15;
|
||
|
|
} else if (delta < 0) {
|
||
|
|
mZoomFactor /= 1.15;
|
||
|
|
}
|
||
|
|
mZoomFactor = qBound(0.1, mZoomFactor, 10.0);
|
||
|
|
updateZoom();
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return QWidget::eventFilter(obj, event);
|
||
|
|
}
|
||
|
|
|
||
|
|
void ImagePreviewWidget::wheelEvent(QWheelEvent *event)
|
||
|
|
{
|
||
|
|
// Forward to viewport event filter
|
||
|
|
int delta = event->angleDelta().y();
|
||
|
|
if (delta > 0) {
|
||
|
|
mZoomFactor *= 1.15;
|
||
|
|
} else if (delta < 0) {
|
||
|
|
mZoomFactor /= 1.15;
|
||
|
|
}
|
||
|
|
mZoomFactor = qBound(0.1, mZoomFactor, 10.0);
|
||
|
|
updateZoom();
|
||
|
|
event->accept();
|
||
|
|
}
|
||
|
|
|
||
|
|
void ImagePreviewWidget::updateZoom()
|
||
|
|
{
|
||
|
|
if (mOriginalPixmap.isNull()) return;
|
||
|
|
|
||
|
|
QSize newSize = mOriginalPixmap.size() * mZoomFactor;
|
||
|
|
QPixmap scaled = mOriginalPixmap.scaled(newSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||
|
|
mImageLabel->setPixmap(scaled);
|
||
|
|
mImageLabel->adjustSize();
|
||
|
|
|
||
|
|
// Update info label with zoom level
|
||
|
|
mInfoLabel->setText(QString("%1 - %2x%3 (%.0f%%)")
|
||
|
|
.arg(mFilename)
|
||
|
|
.arg(mImageSize.width())
|
||
|
|
.arg(mImageSize.height())
|
||
|
|
.arg(mZoomFactor * 100));
|
||
|
|
}
|
||
|
|
|
||
|
|
bool ImagePreviewWidget::loadFromData(const QByteArray &data, const QString &format)
|
||
|
|
{
|
||
|
|
// Check for Xbox texture formats (XBTX2D, TX2D, etc.)
|
||
|
|
if (data.size() >= 8) {
|
||
|
|
QString magic = QString::fromLatin1(data.left(8));
|
||
|
|
if (magic.startsWith("XBTX2D") || magic.startsWith("TX2D") || magic.startsWith("TX1D") || magic.startsWith("TX3D")) {
|
||
|
|
// Try to decode Xbox 360 texture
|
||
|
|
QImage image = loadXBTX2D(data);
|
||
|
|
if (!image.isNull()) {
|
||
|
|
mImageSize = image.size();
|
||
|
|
mOriginalPixmap = QPixmap::fromImage(image);
|
||
|
|
mZoomFactor = 1.0;
|
||
|
|
mImageLabel->setPixmap(mOriginalPixmap);
|
||
|
|
mImageLabel->adjustSize();
|
||
|
|
mInfoLabel->setText(QString("%1 - %2x%3 (Xbox 360)").arg(mFilename).arg(image.width()).arg(image.height()));
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
// Fallback to info display if decode fails
|
||
|
|
mImageLabel->setText(QString("Xbox 360 Texture Format\n\nMagic: %1\nSize: %2 bytes\n\nCould not decode texture.")
|
||
|
|
.arg(magic.left(6).trimmed())
|
||
|
|
.arg(data.size()));
|
||
|
|
mInfoLabel->setText(QString("%1 - Xbox 360 Texture (%2 bytes)").arg(mFilename).arg(data.size()));
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check for DDS format (starts with "DDS ")
|
||
|
|
if (data.size() >= 4 && data.left(4) == "DDS ") {
|
||
|
|
mImageLabel->setText(QString("DDS Texture Format\n\nSize: %1 bytes\n\nDDS preview not yet implemented.")
|
||
|
|
.arg(data.size()));
|
||
|
|
mInfoLabel->setText(QString("%1 - DDS Texture (%2 bytes)").arg(mFilename).arg(data.size()));
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
QImage image;
|
||
|
|
|
||
|
|
// Try standard Qt loading first
|
||
|
|
if (!format.isEmpty()) {
|
||
|
|
image.loadFromData(data, format.toUtf8().constData());
|
||
|
|
}
|
||
|
|
|
||
|
|
if (image.isNull()) {
|
||
|
|
image.loadFromData(data);
|
||
|
|
}
|
||
|
|
|
||
|
|
// If still null and looks like TGA, try manual loading
|
||
|
|
if (image.isNull() && (format.toLower() == "tga" ||
|
||
|
|
(data.size() > 18 && mFilename.toLower().endsWith(".tga")))) {
|
||
|
|
image = loadTGA(data);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (image.isNull()) {
|
||
|
|
// Show hex dump of first bytes for debugging
|
||
|
|
QString hexDump;
|
||
|
|
for (int i = 0; i < qMin(16, data.size()); i++) {
|
||
|
|
hexDump += QString("%1 ").arg((unsigned char)data[i], 2, 16, QChar('0'));
|
||
|
|
}
|
||
|
|
mImageLabel->setText(QString("Failed to load image\n\nFirst bytes: %1\nSize: %2 bytes")
|
||
|
|
.arg(hexDump.trimmed())
|
||
|
|
.arg(data.size()));
|
||
|
|
mInfoLabel->setText(QString("%1 - Load failed").arg(mFilename));
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
mImageSize = image.size();
|
||
|
|
mOriginalPixmap = QPixmap::fromImage(image);
|
||
|
|
mZoomFactor = 1.0;
|
||
|
|
mImageLabel->setPixmap(mOriginalPixmap);
|
||
|
|
mImageLabel->adjustSize();
|
||
|
|
|
||
|
|
QString info = QString("%1 - %2x%3 - %4 bytes")
|
||
|
|
.arg(mFilename)
|
||
|
|
.arg(image.width())
|
||
|
|
.arg(image.height())
|
||
|
|
.arg(data.size());
|
||
|
|
mInfoLabel->setText(info);
|
||
|
|
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool ImagePreviewWidget::loadFromFile(const QString &path)
|
||
|
|
{
|
||
|
|
QFile file(path);
|
||
|
|
if (!file.open(QIODevice::ReadOnly)) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
mFilename = QFileInfo(path).fileName();
|
||
|
|
QByteArray data = file.readAll();
|
||
|
|
|
||
|
|
QString format;
|
||
|
|
if (path.toLower().endsWith(".tga")) format = "TGA";
|
||
|
|
else if (path.toLower().endsWith(".dds")) format = "DDS";
|
||
|
|
else if (path.toLower().endsWith(".png")) format = "PNG";
|
||
|
|
else if (path.toLower().endsWith(".jpg") || path.toLower().endsWith(".jpeg")) format = "JPG";
|
||
|
|
else if (path.toLower().endsWith(".bmp")) format = "BMP";
|
||
|
|
|
||
|
|
return loadFromData(data, format);
|
||
|
|
}
|
||
|
|
|
||
|
|
QImage ImagePreviewWidget::loadTGA(const QByteArray &data)
|
||
|
|
{
|
||
|
|
// Simple TGA loader for uncompressed RGB/RGBA images
|
||
|
|
if (data.size() < 18) return QImage();
|
||
|
|
|
||
|
|
const uchar *d = reinterpret_cast<const uchar*>(data.constData());
|
||
|
|
|
||
|
|
// TGA header
|
||
|
|
uchar idLength = d[0];
|
||
|
|
uchar colorMapType = d[1];
|
||
|
|
uchar imageType = d[2];
|
||
|
|
// Skip color map spec (5 bytes: 3-7)
|
||
|
|
// Image spec starts at byte 8
|
||
|
|
quint16 xOrigin = d[8] | (d[9] << 8);
|
||
|
|
quint16 yOrigin = d[10] | (d[11] << 8);
|
||
|
|
quint16 width = d[12] | (d[13] << 8);
|
||
|
|
quint16 height = d[14] | (d[15] << 8);
|
||
|
|
uchar bitsPerPixel = d[16];
|
||
|
|
uchar descriptor = d[17];
|
||
|
|
|
||
|
|
Q_UNUSED(xOrigin);
|
||
|
|
Q_UNUSED(yOrigin);
|
||
|
|
Q_UNUSED(colorMapType);
|
||
|
|
|
||
|
|
// Only support uncompressed true-color (type 2) or grayscale (type 3)
|
||
|
|
if (imageType != 2 && imageType != 3 && imageType != 10) {
|
||
|
|
return QImage();
|
||
|
|
}
|
||
|
|
|
||
|
|
int bytesPerPixel = bitsPerPixel / 8;
|
||
|
|
if (bytesPerPixel < 1 || bytesPerPixel > 4) {
|
||
|
|
return QImage();
|
||
|
|
}
|
||
|
|
|
||
|
|
int dataOffset = 18 + idLength;
|
||
|
|
int expectedSize = dataOffset + width * height * bytesPerPixel;
|
||
|
|
|
||
|
|
// For RLE images, we can't easily predict size, so just check minimum
|
||
|
|
if (imageType != 10 && data.size() < expectedSize) {
|
||
|
|
return QImage();
|
||
|
|
}
|
||
|
|
|
||
|
|
QImage::Format format = (bytesPerPixel == 4) ? QImage::Format_ARGB32 : QImage::Format_RGB32;
|
||
|
|
QImage image(width, height, format);
|
||
|
|
if (image.isNull()) return QImage();
|
||
|
|
|
||
|
|
const uchar *src = d + dataOffset;
|
||
|
|
bool flipVertical = !(descriptor & 0x20); // Bit 5: origin is top-left if set
|
||
|
|
|
||
|
|
if (imageType == 2 || imageType == 3) {
|
||
|
|
// Uncompressed
|
||
|
|
for (int y = 0; y < height; y++) {
|
||
|
|
int destY = flipVertical ? (height - 1 - y) : y;
|
||
|
|
QRgb *destLine = reinterpret_cast<QRgb*>(image.scanLine(destY));
|
||
|
|
|
||
|
|
for (int x = 0; x < width; x++) {
|
||
|
|
int idx = (y * width + x) * bytesPerPixel;
|
||
|
|
if (dataOffset + idx + bytesPerPixel > data.size()) break;
|
||
|
|
|
||
|
|
uchar b = src[idx];
|
||
|
|
uchar g = (bytesPerPixel > 1) ? src[idx + 1] : b;
|
||
|
|
uchar r = (bytesPerPixel > 2) ? src[idx + 2] : b;
|
||
|
|
uchar a = (bytesPerPixel > 3) ? src[idx + 3] : 255;
|
||
|
|
|
||
|
|
destLine[x] = qRgba(r, g, b, a);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} else if (imageType == 10) {
|
||
|
|
// RLE compressed
|
||
|
|
int pixelCount = width * height;
|
||
|
|
int currentPixel = 0;
|
||
|
|
int srcIdx = 0;
|
||
|
|
int maxSrcIdx = data.size() - dataOffset;
|
||
|
|
|
||
|
|
while (currentPixel < pixelCount && srcIdx < maxSrcIdx) {
|
||
|
|
uchar header = src[srcIdx++];
|
||
|
|
int count = (header & 0x7F) + 1;
|
||
|
|
|
||
|
|
if (header & 0x80) {
|
||
|
|
// RLE packet
|
||
|
|
if (srcIdx + bytesPerPixel > maxSrcIdx) break;
|
||
|
|
|
||
|
|
uchar b = src[srcIdx];
|
||
|
|
uchar g = (bytesPerPixel > 1) ? src[srcIdx + 1] : b;
|
||
|
|
uchar r = (bytesPerPixel > 2) ? src[srcIdx + 2] : b;
|
||
|
|
uchar a = (bytesPerPixel > 3) ? src[srcIdx + 3] : 255;
|
||
|
|
srcIdx += bytesPerPixel;
|
||
|
|
|
||
|
|
QRgb color = qRgba(r, g, b, a);
|
||
|
|
for (int i = 0; i < count && currentPixel < pixelCount; i++, currentPixel++) {
|
||
|
|
int x = currentPixel % width;
|
||
|
|
int y = currentPixel / width;
|
||
|
|
int destY = flipVertical ? (height - 1 - y) : y;
|
||
|
|
QRgb *destLine = reinterpret_cast<QRgb*>(image.scanLine(destY));
|
||
|
|
destLine[x] = color;
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
// Raw packet
|
||
|
|
for (int i = 0; i < count && currentPixel < pixelCount; i++, currentPixel++) {
|
||
|
|
if (srcIdx + bytesPerPixel > maxSrcIdx) break;
|
||
|
|
|
||
|
|
uchar b = src[srcIdx];
|
||
|
|
uchar g = (bytesPerPixel > 1) ? src[srcIdx + 1] : b;
|
||
|
|
uchar r = (bytesPerPixel > 2) ? src[srcIdx + 2] : b;
|
||
|
|
uchar a = (bytesPerPixel > 3) ? src[srcIdx + 3] : 255;
|
||
|
|
srcIdx += bytesPerPixel;
|
||
|
|
|
||
|
|
int x = currentPixel % width;
|
||
|
|
int y = currentPixel / width;
|
||
|
|
int destY = flipVertical ? (height - 1 - y) : y;
|
||
|
|
QRgb *destLine = reinterpret_cast<QRgb*>(image.scanLine(destY));
|
||
|
|
destLine[x] = qRgba(r, g, b, a);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return image;
|
||
|
|
}
|
||
|
|
|
||
|
|
// DXT1 block decoder - read colors as big-endian (Xbox 360 native)
|
||
|
|
static void decodeDXT1BlockStatic(const uchar *src, QRgb *dest, int destPitch)
|
||
|
|
{
|
||
|
|
// Read colors as big-endian (Xbox 360 native format)
|
||
|
|
quint16 c0 = (src[0] << 8) | src[1];
|
||
|
|
quint16 c1 = (src[2] << 8) | src[3];
|
||
|
|
|
||
|
|
// Extract RGB565 components: bits 15-11=R, 10-5=G, 4-0=B
|
||
|
|
// Xbox 360 stores as BGR565, so swap R and B
|
||
|
|
int b0 = ((c0 >> 11) & 0x1F) * 255 / 31;
|
||
|
|
int g0 = ((c0 >> 5) & 0x3F) * 255 / 63;
|
||
|
|
int r0 = (c0 & 0x1F) * 255 / 31;
|
||
|
|
|
||
|
|
int b1 = ((c1 >> 11) & 0x1F) * 255 / 31;
|
||
|
|
int g1 = ((c1 >> 5) & 0x3F) * 255 / 63;
|
||
|
|
int r1 = (c1 & 0x1F) * 255 / 31;
|
||
|
|
|
||
|
|
// Build color table
|
||
|
|
QRgb colors[4];
|
||
|
|
colors[0] = qRgb(r0, g0, b0);
|
||
|
|
colors[1] = qRgb(r1, g1, b1);
|
||
|
|
|
||
|
|
if (c0 > c1) {
|
||
|
|
colors[2] = qRgb((2 * r0 + r1) / 3, (2 * g0 + g1) / 3, (2 * b0 + b1) / 3);
|
||
|
|
colors[3] = qRgb((r0 + 2 * r1) / 3, (g0 + 2 * g1) / 3, (b0 + 2 * b1) / 3);
|
||
|
|
} else {
|
||
|
|
colors[2] = qRgb((r0 + r1) / 2, (g0 + g1) / 2, (b0 + b1) / 2);
|
||
|
|
colors[3] = qRgba(0, 0, 0, 0); // Transparent
|
||
|
|
}
|
||
|
|
|
||
|
|
// Read indices - standard order (no swap needed)
|
||
|
|
for (int y = 0; y < 4; y++) {
|
||
|
|
uchar rowByte = src[4 + y];
|
||
|
|
for (int x = 0; x < 4; x++) {
|
||
|
|
int idx = (rowByte >> (x * 2)) & 0x03;
|
||
|
|
dest[y * destPitch + x] = colors[idx];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// DXT5 block decoder - read as big-endian (Xbox 360 native)
|
||
|
|
static void decodeDXT5BlockStatic(const uchar *src, QRgb *dest, int destPitch)
|
||
|
|
{
|
||
|
|
// Alpha block (first 8 bytes) - standard order
|
||
|
|
uchar alpha0 = src[0];
|
||
|
|
uchar alpha1 = src[1];
|
||
|
|
|
||
|
|
// Build alpha table
|
||
|
|
uchar alphas[8];
|
||
|
|
alphas[0] = alpha0;
|
||
|
|
alphas[1] = alpha1;
|
||
|
|
|
||
|
|
if (alpha0 > alpha1) {
|
||
|
|
alphas[2] = (6 * alpha0 + 1 * alpha1) / 7;
|
||
|
|
alphas[3] = (5 * alpha0 + 2 * alpha1) / 7;
|
||
|
|
alphas[4] = (4 * alpha0 + 3 * alpha1) / 7;
|
||
|
|
alphas[5] = (3 * alpha0 + 4 * alpha1) / 7;
|
||
|
|
alphas[6] = (2 * alpha0 + 5 * alpha1) / 7;
|
||
|
|
alphas[7] = (1 * alpha0 + 6 * alpha1) / 7;
|
||
|
|
} else {
|
||
|
|
alphas[2] = (4 * alpha0 + 1 * alpha1) / 5;
|
||
|
|
alphas[3] = (3 * alpha0 + 2 * alpha1) / 5;
|
||
|
|
alphas[4] = (2 * alpha0 + 3 * alpha1) / 5;
|
||
|
|
alphas[5] = (1 * alpha0 + 4 * alpha1) / 5;
|
||
|
|
alphas[6] = 0;
|
||
|
|
alphas[7] = 255;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Alpha indices (6 bytes, 48 bits = 16 pixels * 3 bits each) - standard order
|
||
|
|
quint64 alphaIndices = src[2] | ((quint64)src[3] << 8) | ((quint64)src[4] << 16) |
|
||
|
|
((quint64)src[5] << 24) | ((quint64)src[6] << 32) | ((quint64)src[7] << 40);
|
||
|
|
|
||
|
|
// Color block (bytes 8-15, same as DXT1) - big-endian
|
||
|
|
const uchar *colorSrc = src + 8;
|
||
|
|
quint16 c0 = (colorSrc[0] << 8) | colorSrc[1]; // Big-endian (Xbox 360)
|
||
|
|
quint16 c1 = (colorSrc[2] << 8) | colorSrc[3]; // Big-endian (Xbox 360)
|
||
|
|
|
||
|
|
// BGR565: bits 15-11=B, 10-5=G, 4-0=R (Xbox 360 swapped)
|
||
|
|
int b0 = ((c0 >> 11) & 0x1F) * 255 / 31;
|
||
|
|
int g0 = ((c0 >> 5) & 0x3F) * 255 / 63;
|
||
|
|
int r0 = (c0 & 0x1F) * 255 / 31;
|
||
|
|
|
||
|
|
int b1 = ((c1 >> 11) & 0x1F) * 255 / 31;
|
||
|
|
int g1 = ((c1 >> 5) & 0x3F) * 255 / 63;
|
||
|
|
int r1 = (c1 & 0x1F) * 255 / 31;
|
||
|
|
|
||
|
|
QRgb colors[4];
|
||
|
|
colors[0] = qRgb(r0, g0, b0);
|
||
|
|
colors[1] = qRgb(r1, g1, b1);
|
||
|
|
colors[2] = qRgb((2 * r0 + r1) / 3, (2 * g0 + g1) / 3, (2 * b0 + b1) / 3);
|
||
|
|
colors[3] = qRgb((r0 + 2 * r1) / 3, (g0 + 2 * g1) / 3, (b0 + 2 * b1) / 3);
|
||
|
|
|
||
|
|
// Decode 4x4 block - standard order
|
||
|
|
for (int y = 0; y < 4; y++) {
|
||
|
|
uchar colorRowByte = colorSrc[4 + y];
|
||
|
|
for (int x = 0; x < 4; x++) {
|
||
|
|
int pixelIdx = y * 4 + x;
|
||
|
|
int alphaIdx = (alphaIndices >> (pixelIdx * 3)) & 0x07;
|
||
|
|
int colorIdx = (colorRowByte >> (x * 2)) & 0x03;
|
||
|
|
|
||
|
|
QRgb color = colors[colorIdx];
|
||
|
|
dest[y * destPitch + x] = qRgba(qRed(color), qGreen(color), qBlue(color), alphas[alphaIdx]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Swap endianness in 16-bit chunks (required for Xbox 360)
|
||
|
|
static void swapEndian16(uchar *data, int size)
|
||
|
|
{
|
||
|
|
for (int i = 0; i < size - 1; i += 2) {
|
||
|
|
uchar tmp = data[i];
|
||
|
|
data[i] = data[i + 1];
|
||
|
|
data[i + 1] = tmp;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Xbox 360 XGAddress2DTiledX - convert tiled offset to linear X
|
||
|
|
// From Noesis inc_xbox360_untile.py (credits: Banz99's Nier-Texture-Manager, GTA XTD editor)
|
||
|
|
static unsigned int xgAddress2DTiledX(unsigned int offset, unsigned int width, unsigned int texelPitch)
|
||
|
|
{
|
||
|
|
unsigned int alignedWidth = (width + 31) & ~31;
|
||
|
|
unsigned int logBpp = (texelPitch >> 2) + ((texelPitch >> 1) >> (texelPitch >> 2));
|
||
|
|
unsigned int offsetB = offset << logBpp;
|
||
|
|
unsigned int offsetT = ((offsetB & ~4095) >> 3) + ((offsetB & 1792) >> 2) + (offsetB & 63);
|
||
|
|
unsigned int offsetM = offsetT >> (7 + logBpp);
|
||
|
|
unsigned int macroX = ((offsetM % (alignedWidth >> 5)) << 2);
|
||
|
|
unsigned int tile = ((((offsetT >> (5 + logBpp)) & 2) + (offsetB >> 6)) & 3);
|
||
|
|
unsigned int macro = (macroX + tile) << 3;
|
||
|
|
unsigned int micro = ((((offsetT >> 1) & ~15) + (offsetT & 15)) & ((texelPitch << 3) - 1)) >> logBpp;
|
||
|
|
return macro + micro;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Xbox 360 XGAddress2DTiledY - convert tiled offset to linear Y
|
||
|
|
static unsigned int xgAddress2DTiledY(unsigned int offset, unsigned int width, unsigned int texelPitch)
|
||
|
|
{
|
||
|
|
unsigned int alignedWidth = (width + 31) & ~31;
|
||
|
|
unsigned int logBpp = (texelPitch >> 2) + ((texelPitch >> 1) >> (texelPitch >> 2));
|
||
|
|
unsigned int offsetB = offset << logBpp;
|
||
|
|
unsigned int offsetT = ((offsetB & ~4095) >> 3) + ((offsetB & 1792) >> 2) + (offsetB & 63);
|
||
|
|
unsigned int offsetM = offsetT >> (7 + logBpp);
|
||
|
|
unsigned int macroY = ((offsetM / (alignedWidth >> 5)) << 2);
|
||
|
|
unsigned int tile = ((offsetT >> (6 + logBpp)) & 1) + (((offsetB & 2048) >> 10));
|
||
|
|
unsigned int macro = (macroY + tile) << 3;
|
||
|
|
unsigned int micro = ((((offsetT & (((texelPitch << 6) - 1) & ~31)) + ((offsetT & 15) << 1)) >> (3 + logBpp)) & ~1);
|
||
|
|
return macro + micro + ((offsetT & 16) >> 4);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Xbox 360 texture untiling - iterate tiled source, write to linear dest
|
||
|
|
// Based on official Xbox 360 SDK XGAddress2DTiledX/Y functions
|
||
|
|
static void untileXbox360(const uchar *src, uchar *dest, int width, int height, int blockSize)
|
||
|
|
{
|
||
|
|
// For DXT, work in blocks (4x4 pixels per block)
|
||
|
|
unsigned int blocksWide = (width + 3) / 4;
|
||
|
|
unsigned int blocksHigh = (height + 3) / 4;
|
||
|
|
|
||
|
|
// Calculate aligned dimensions for iteration (SDK aligns internally, but we need to iterate aligned space)
|
||
|
|
unsigned int alignedBlocksWide = (blocksWide + 31) & ~31;
|
||
|
|
unsigned int alignedBlocksHigh = (blocksHigh + 31) & ~31;
|
||
|
|
unsigned int totalAlignedBlocks = alignedBlocksWide * alignedBlocksHigh;
|
||
|
|
|
||
|
|
// texelPitch is the block size (8 for DXT1, 16 for DXT5)
|
||
|
|
unsigned int texelPitch = blockSize;
|
||
|
|
|
||
|
|
// Iterate through aligned source blocks
|
||
|
|
for (unsigned int blockOffset = 0; blockOffset < totalAlignedBlocks; blockOffset++) {
|
||
|
|
// Pass the ACTUAL width in blocks - XGAddress functions align internally!
|
||
|
|
// This is the key fix: SDK expects real width, not pre-aligned
|
||
|
|
unsigned int x = xgAddress2DTiledX(blockOffset, blocksWide, texelPitch);
|
||
|
|
unsigned int y = xgAddress2DTiledY(blockOffset, blocksWide, texelPitch);
|
||
|
|
|
||
|
|
// Skip blocks outside actual texture bounds
|
||
|
|
if (x >= blocksWide || y >= blocksHigh) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Source is sequential tiled (aligned), dest is linear (actual size)
|
||
|
|
unsigned int srcOffset = blockOffset * texelPitch;
|
||
|
|
unsigned int destOffset = (y * blocksWide + x) * texelPitch;
|
||
|
|
|
||
|
|
memcpy(dest + destOffset, src + srcOffset, texelPitch);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Helper to decode DXT data into an image
|
||
|
|
static QImage decodeDXTToImage(const uchar *srcData, int width, int height, int blockSize, bool isDXT5, int textureDataSize)
|
||
|
|
{
|
||
|
|
int blocksWide = (width + 3) / 4;
|
||
|
|
int blocksHigh = (height + 3) / 4;
|
||
|
|
|
||
|
|
QImage image(width, height, QImage::Format_ARGB32);
|
||
|
|
if (image.isNull()) return QImage();
|
||
|
|
image.fill(Qt::black);
|
||
|
|
|
||
|
|
for (int by = 0; by < blocksHigh; by++) {
|
||
|
|
for (int bx = 0; bx < blocksWide; bx++) {
|
||
|
|
int blockIdx = by * blocksWide + bx;
|
||
|
|
int blockOffset = blockIdx * blockSize;
|
||
|
|
|
||
|
|
if (blockOffset + blockSize > textureDataSize) break;
|
||
|
|
|
||
|
|
const uchar *blockData = srcData + blockOffset;
|
||
|
|
|
||
|
|
// Decode into temporary 4x4 block
|
||
|
|
QRgb block[16];
|
||
|
|
|
||
|
|
if (isDXT5) {
|
||
|
|
decodeDXT5BlockStatic(blockData, block, 4);
|
||
|
|
} else {
|
||
|
|
decodeDXT1BlockStatic(blockData, block, 4);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Copy block to image
|
||
|
|
int px = bx * 4;
|
||
|
|
int py = by * 4;
|
||
|
|
for (int y = 0; y < 4 && py + y < height; y++) {
|
||
|
|
QRgb *destLine = reinterpret_cast<QRgb*>(image.scanLine(py + y));
|
||
|
|
for (int x = 0; x < 4 && px + x < width; x++) {
|
||
|
|
destLine[px + x] = block[y * 4 + x];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return image;
|
||
|
|
}
|
||
|
|
|
||
|
|
QImage ImagePreviewWidget::loadXBTX2D(const QByteArray &data)
|
||
|
|
{
|
||
|
|
if (data.size() < 0x60) return QImage();
|
||
|
|
|
||
|
|
const uchar *d = reinterpret_cast<const uchar*>(data.constData());
|
||
|
|
|
||
|
|
// XBTX2D header (big-endian) - actual format from hex analysis:
|
||
|
|
// 0x00-0x07: Magic "XBTX2D " (8 bytes)
|
||
|
|
// 0x08-0x0B: Version (4 bytes, BE)
|
||
|
|
// 0x0C-0x0F: Width (4 bytes, BE)
|
||
|
|
// 0x10-0x13: Height (4 bytes, BE)
|
||
|
|
// 0x14-0x17: Hash/ID
|
||
|
|
// 0x18-0x1B: Mip count (4 bytes, BE)
|
||
|
|
// 0x1C-0x1F: Size field 1
|
||
|
|
// 0x20-0x23: Size field 2 (base mip size for DXT1)
|
||
|
|
// 0x24-0x27: Format (3=DXT1?)
|
||
|
|
// 0x58: Data start
|
||
|
|
|
||
|
|
quint32 width = (d[0x0C] << 24) | (d[0x0D] << 16) | (d[0x0E] << 8) | d[0x0F];
|
||
|
|
quint32 height = (d[0x10] << 24) | (d[0x11] << 16) | (d[0x12] << 8) | d[0x13];
|
||
|
|
quint32 baseMipSize = (d[0x20] << 24) | (d[0x21] << 16) | (d[0x22] << 8) | d[0x23];
|
||
|
|
|
||
|
|
// Sanity checks
|
||
|
|
if (width == 0 || height == 0 || width > 8192 || height > 8192) {
|
||
|
|
return QImage();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Determine format from base mip size
|
||
|
|
// DXT1: 8 bytes per 4x4 block = width*height/2
|
||
|
|
// DXT5: 16 bytes per 4x4 block = width*height
|
||
|
|
int blockSize;
|
||
|
|
int dataOffset = 0x58; // Fixed header size
|
||
|
|
bool isDXT5;
|
||
|
|
|
||
|
|
quint32 expectedDXT1Size = (width * height) / 2;
|
||
|
|
quint32 expectedDXT5Size = width * height;
|
||
|
|
|
||
|
|
if (baseMipSize == expectedDXT5Size) {
|
||
|
|
// DXT5 (or similar 16-byte block format)
|
||
|
|
blockSize = 16;
|
||
|
|
isDXT5 = true;
|
||
|
|
StatusBarManager::instance().updateStatus(
|
||
|
|
QString("XBTX2D: %1 - DXT5 %2x%3").arg(mFilename).arg(width).arg(height), 3000);
|
||
|
|
} else {
|
||
|
|
// DXT1 (or similar 8-byte block format)
|
||
|
|
blockSize = 8;
|
||
|
|
isDXT5 = false;
|
||
|
|
StatusBarManager::instance().updateStatus(
|
||
|
|
QString("XBTX2D: %1 - DXT1 %2x%3").arg(mFilename).arg(width).arg(height), 3000);
|
||
|
|
}
|
||
|
|
|
||
|
|
int blocksWide = (width + 3) / 4;
|
||
|
|
int blocksHigh = (height + 3) / 4;
|
||
|
|
int totalBlocks = blocksWide * blocksHigh;
|
||
|
|
int textureDataSize = totalBlocks * blockSize;
|
||
|
|
|
||
|
|
if (data.size() < dataOffset) return QImage();
|
||
|
|
|
||
|
|
int availableData = data.size() - dataOffset;
|
||
|
|
if (availableData < textureDataSize) {
|
||
|
|
textureDataSize = availableData;
|
||
|
|
}
|
||
|
|
|
||
|
|
const uchar *srcData = d + dataOffset;
|
||
|
|
|
||
|
|
// Xbox 360 DXT textures require:
|
||
|
|
// 1. Untiling (GPU memory layout to linear)
|
||
|
|
// 2. DXT decode with big-endian color reading
|
||
|
|
|
||
|
|
// Try both untiled and linear to compare
|
||
|
|
QByteArray untiled(textureDataSize, 0);
|
||
|
|
untileXbox360(srcData, reinterpret_cast<uchar*>(untiled.data()), width, height, blockSize);
|
||
|
|
|
||
|
|
// Decode both versions
|
||
|
|
QImage untiledResult = decodeDXTToImage(reinterpret_cast<const uchar*>(untiled.constData()),
|
||
|
|
width, height, blockSize, isDXT5, textureDataSize);
|
||
|
|
QImage linearResult = decodeDXTToImage(srcData, width, height, blockSize, isDXT5, textureDataSize);
|
||
|
|
|
||
|
|
// Export both for comparison
|
||
|
|
QDir().mkdir("exports");
|
||
|
|
QString debugName = mFilename.isEmpty() ? "xbtx2d_debug" : mFilename;
|
||
|
|
int dot = debugName.lastIndexOf('.');
|
||
|
|
if (dot > 0) debugName = debugName.left(dot);
|
||
|
|
if (!untiledResult.isNull()) {
|
||
|
|
untiledResult.save("exports/" + debugName + "_UNTILED.png", "PNG");
|
||
|
|
}
|
||
|
|
if (!linearResult.isNull()) {
|
||
|
|
linearResult.save("exports/" + debugName + "_LINEAR.png", "PNG");
|
||
|
|
}
|
||
|
|
|
||
|
|
// Return the untiled version
|
||
|
|
return untiledResult;
|
||
|
|
}
|