diff --git a/include/core/imageexport.h b/include/core/imageexport.h new file mode 100644 index 00000000..e6552323 --- /dev/null +++ b/include/core/imageexport.h @@ -0,0 +1,9 @@ +#ifndef IMAGEEXPORT_H +#define IMAGEEXPORT_H + +#include +#include + +void exportIndexed4BPPPng(QImage image, QString filepath); + +#endif // IMAGEEXPORT_H diff --git a/porymap.pro b/porymap.pro index fb81f994..0611f241 100644 --- a/porymap.pro +++ b/porymap.pro @@ -19,6 +19,7 @@ SOURCES += src/core/block.cpp \ src/core/event.cpp \ src/core/heallocation.cpp \ src/core/historyitem.cpp \ + src/core/imageexport.cpp \ src/core/map.cpp \ src/core/maplayout.cpp \ src/core/metatile.cpp \ @@ -66,6 +67,7 @@ HEADERS += include/core/block.h \ include/core/heallocation.h \ include/core/history.h \ include/core/historyitem.h \ + include/core/imageexport.h \ include/core/map.h \ include/core/mapconnection.h \ include/core/maplayout.h \ diff --git a/src/core/imageexport.cpp b/src/core/imageexport.cpp new file mode 100644 index 00000000..a4994d85 --- /dev/null +++ b/src/core/imageexport.cpp @@ -0,0 +1,186 @@ +#include "imageexport.h" +#include "log.h" +#include + +// CRC code from: http://www.libpng.org/pub/png/spec/1.2/PNG-CRCAppendix.html + +/* Table of CRCs of all 8-bit messages. */ +unsigned long crc_table[256]; + +/* Flag: has the table been computed? Initially false. */ +int crc_table_computed = 0; + +/* Make the table for a fast CRC. */ +void make_crc_table(void) +{ + unsigned long c; + int n, k; + + for (n = 0; n < 256; n++) { + c = (unsigned long) n; + for (k = 0; k < 8; k++) { + if (c & 1) + c = 0xedb88320L ^ (c >> 1); + else + c = c >> 1; + } + crc_table[n] = c; + } + crc_table_computed = 1; +} + +/* Update a running CRC with the bytes buf[0..len-1]--the CRC + should be initialized to all 1's, and the transmitted value + is the 1's complement of the final running CRC (see the + crc() routine below)). */ + +unsigned long update_crc(unsigned long crc, QByteArray buf, + int len) +{ + unsigned long c = crc; + int n; + + if (!crc_table_computed) + make_crc_table(); + for (n = 0; n < len; n++) { + c = crc_table[(c ^ static_cast(buf[n])) & 0xff] ^ (c >> 8); + } + return c; +} + +/* Return the CRC of the bytes buf[0..len-1]. */ +unsigned long crc(QByteArray buf, int len) +{ + return update_crc(0xffffffffL, buf, len) ^ 0xffffffffL; +} + +// Qt does not have the ability to export indexed PNG files with a +// bit depth of 4--it only supports 8. This can cause problems with +// some image editing programs because they will revert the bit depth, +// and re-importing into porymap (Qt), will cause the image to be +// interpreted as having too many colors. By properly exporting 16-palette +// images in porymap, we can effectively avoid that issue. +void exportIndexed4BPPPng(QImage image, QString filepath) +{ + // Header + QByteArray pngHeader; + pngHeader.append(static_cast(0x89)); + pngHeader.append("PNG"); + pngHeader.append(static_cast(0x0D)); + pngHeader.append(static_cast(0x0A)); + pngHeader.append(static_cast(0x1A)); + pngHeader.append(static_cast(0x0A)); + + // IHDR Chunk + QByteArray ihdr; + ihdr.append("IHDR"); + int width = image.width(); + ihdr.append(static_cast((width >> 24) & 0xFF)); + ihdr.append(static_cast((width >> 16) & 0xFF)); + ihdr.append(static_cast((width >> 8) & 0xFF)); + ihdr.append(static_cast((width >> 0) & 0xFF)); + int height = image.height(); + ihdr.append(static_cast((height >> 24) & 0xFF)); + ihdr.append(static_cast((height >> 16) & 0xFF)); + ihdr.append(static_cast((height >> 8) & 0xFF)); + ihdr.append(static_cast((height >> 0) & 0xFF)); + ihdr.append(static_cast(4)); // bit depth + ihdr.append(static_cast(3)); // indexed color type + ihdr.append(static_cast(0)); // compression method + ihdr.append(static_cast(0)); // filter method + ihdr.append(static_cast(0)); // interlace method + unsigned long ihdrCRC = crc(ihdr, 17); + ihdr.append(static_cast((ihdrCRC >> 24) & 0xFF)); + ihdr.append(static_cast((ihdrCRC >> 16) & 0xFF)); + ihdr.append(static_cast((ihdrCRC >> 8) & 0xFF)); + ihdr.append(static_cast((ihdrCRC >> 0) & 0xFF)); + + // PLTE Chunk + int numColors = image.colorCount(); + QByteArray plte; + plte.append("PLTE"); + for (int i = 0; i < numColors; i++) { + QRgb rgb = image.colorTable().at(i); + plte.append(static_cast(qRed(rgb))); + plte.append(static_cast(qGreen(rgb))); + plte.append(static_cast(qBlue(rgb))); + } + unsigned long plteCRC = crc(plte, numColors * 3 + 4); + plte.append(static_cast((plteCRC >> 24) & 0xFF)); + plte.append(static_cast((plteCRC >> 16) & 0xFF)); + plte.append(static_cast((plteCRC >> 8) & 0xFF)); + plte.append(static_cast((plteCRC >> 0) & 0xFF)); + + // IDAT Chunk + QByteArray idat; + idat.append("IDAT"); + unsigned long count = 0; + char val = 0; + + QByteArray pixelData; + for (int y = 0; y < image.height(); y++) { + pixelData.append(static_cast(0)); + for (int x = 0; x < image.width(); x++) { + int colorId = image.pixelIndex(x, y); + if (count % 2 == 0) { + val = static_cast(val & 0x0F) | static_cast(colorId << 4); + } else { + val = static_cast(val & 0xF0) | (static_cast(colorId)); + pixelData.append(val); + } + count++; + } + } + QByteArray compressedPixelData = qCompress(pixelData); + // Qt's qCompress/qDecompress use a pointless 4-byte header, even though + // they are using DEFLATE under the hood. If we strip the 4-byte header, + // it's perfectly compatible with the PNG compression spec. + compressedPixelData.remove(0, 4); + idat.append(compressedPixelData); + unsigned long idatCRC = crc(idat, compressedPixelData.length() + 4); + idat.append(static_cast((idatCRC >> 24) & 0xFF)); + idat.append(static_cast((idatCRC >> 16) & 0xFF)); + idat.append(static_cast((idatCRC >> 8) & 0xFF)); + idat.append(static_cast((idatCRC >> 0) & 0xFF)); + + // IEND Chunk + QByteArray iend; + iend.append("IEND"); + unsigned long iendCRC = crc(iend, 4); + iend.append(static_cast((iendCRC >> 24) & 0xFF)); + iend.append(static_cast((iendCRC >> 16) & 0xFF)); + iend.append(static_cast((iendCRC >> 8) & 0xFF)); + iend.append(static_cast((iendCRC >> 0) & 0xFF)); + + QByteArray data; + data.append(pngHeader); + data.append(static_cast(((ihdr.length() - 8) >> 24) & 0xFF)); + data.append(static_cast(((ihdr.length() - 8) >> 16) & 0xFF)); + data.append(static_cast(((ihdr.length() - 8) >> 8) & 0xFF)); + data.append(static_cast(((ihdr.length() - 8) >> 0) & 0xFF)); + data.append(ihdr); + data.append(static_cast(((plte.length() - 8) >> 24) & 0xFF)); + data.append(static_cast(((plte.length() - 8) >> 16) & 0xFF)); + data.append(static_cast(((plte.length() - 8) >> 8) & 0xFF)); + data.append(static_cast(((plte.length() - 8) >> 0) & 0xFF)); + data.append(plte); + data.append(static_cast(((idat.length() - 8) >> 24) & 0xFF)); + data.append(static_cast(((idat.length() - 8) >> 16) & 0xFF)); + data.append(static_cast(((idat.length() - 8) >> 8) & 0xFF)); + data.append(static_cast(((idat.length() - 8) >> 0) & 0xFF)); + data.append(idat); + data.append(static_cast(((iend.length() - 8) >> 24) & 0xFF)); + data.append(static_cast(((iend.length() - 8) >> 16) & 0xFF)); + data.append(static_cast(((iend.length() - 8) >> 8) & 0xFF)); + data.append(static_cast(((iend.length() - 8) >> 0) & 0xFF)); + data.append(iend); + + QFile file(filepath); + if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + logError(QString("Could not save '%1'. ").arg(filepath) + file.errorString()); + return; + } + + file.write(data); + file.close(); +} diff --git a/src/ui/tileseteditor.cpp b/src/ui/tileseteditor.cpp index f8904511..58d07217 100644 --- a/src/ui/tileseteditor.cpp +++ b/src/ui/tileseteditor.cpp @@ -4,6 +4,7 @@ #include "imageproviders.h" #include "metatileparser.h" #include "paletteparser.h" +#include "imageexport.h" #include #include #include @@ -613,7 +614,7 @@ void TilesetEditor::on_actionExport_Primary_Tiles_Image_triggered() QString filepath = QFileDialog::getSaveFileName(this, "Export Primary Tiles Image", defaultFilepath, "Image Files (*.png)"); if (!filepath.isEmpty()) { QImage image = this->tileSelector->buildPrimaryTilesIndexedImage(); - image.save(filepath); + exportIndexed4BPPPng(image, filepath); } } @@ -624,7 +625,7 @@ void TilesetEditor::on_actionExport_Secondary_Tiles_Image_triggered() QString filepath = QFileDialog::getSaveFileName(this, "Export Secondary Tiles Image", defaultFilepath, "Image Files (*.png)"); if (!filepath.isEmpty()) { QImage image = this->tileSelector->buildSecondaryTilesIndexedImage(); - image.save(filepath); + exportIndexed4BPPPng(image, filepath); } }