diff options
author | Filippos Karapetis | 2011-02-02 15:43:45 +0000 |
---|---|---|
committer | Filippos Karapetis | 2011-02-02 15:43:45 +0000 |
commit | a86cb87b98f0ec904dcc8e46b5b2c4b3d2f81aca (patch) | |
tree | 570e2e7c57e6788b5f4dc41883c461ab2f948cf4 /graphics | |
parent | a4a09ac2845f3bf4d3ba5985285f9b393546da66 (diff) | |
download | scummvm-rg350-a86cb87b98f0ec904dcc8e46b5b2c4b3d2f81aca.tar.gz scummvm-rg350-a86cb87b98f0ec904dcc8e46b5b2c4b3d2f81aca.tar.bz2 scummvm-rg350-a86cb87b98f0ec904dcc8e46b5b2c4b3d2f81aca.zip |
GRAPHICS: Implemented a PNG decoder, and set it as default for the sword25 engine
libpng is still needed for PNG encoding (for thumbnails in saved games of sword25), but
since we'll probably drop support for the original saved games anyway, the PNG encoding
code will ultimately be removed
svn-id: r55723
Diffstat (limited to 'graphics')
-rw-r--r-- | graphics/module.mk | 1 | ||||
-rw-r--r-- | graphics/png.cpp | 449 | ||||
-rw-r--r-- | graphics/png.h | 162 |
3 files changed, 612 insertions, 0 deletions
diff --git a/graphics/module.mk b/graphics/module.mk index 827d2886f7..c962f0617d 100644 --- a/graphics/module.mk +++ b/graphics/module.mk @@ -15,6 +15,7 @@ MODULE_OBJS := \ imagedec.o \ jpeg.o \ pict.o \ + png.o \ primitives.o \ scaler.o \ scaler/thumbnail_intern.o \ diff --git a/graphics/png.cpp b/graphics/png.cpp new file mode 100644 index 0000000000..cf19969b46 --- /dev/null +++ b/graphics/png.cpp @@ -0,0 +1,449 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * $URL$ + * $Id$ + * + */ + +#include "graphics/conversion.h" +#include "graphics/png.h" +#include "graphics/pixelformat.h" + +#include "common/endian.h" +#include "common/memstream.h" +#include "common/stream.h" +#include "common/util.h" +#include "common/zlib.h" + +namespace Graphics { + +enum PNGChunks { + // == Critical chunks ===================================================== + kChunkIHDR = MKID_BE('IHDR'), // Image header + kChunkIDAT = MKID_BE('IDAT'), // Image data + kChunkPLTE = MKID_BE('PLTE'), // Palette + kChunkIEND = MKID_BE('IEND'), // Image trailer + // == Ancillary chunks ==================================================== + kChunktRNS = MKID_BE('tRNS') // Transparency + // All of the other ancillary chunks are ignored. They're added here for + // reference only. + // cHRM - Primary chromacities and white point + // gAMA - Image gamma + // iCCP - Embedded ICC profile + // sBIT - Significant bits + // sRGB - Standard RGB color space + // tEXT - Textual data + // sTXt - Compressed textual data + // iTXt - International textual data + // bKGD - Background color + // hIST - Image histogram + // pHYs - Physical pixel dimensions + // sPLT - Suggested palette + // tIME - Image last-modification time +}; + +// Refer to http://www.w3.org/TR/PNG/#9Filters +enum PNGFilters { + kFilterNone = 0, + kFilterSub = 1, + kFilterUp = 2, + kFilterAverage = 3, + kFilterPaeth = 4 +}; + +#define PNG_HEADER(a, b, c, d) CONSTANT_LE_32(d | (c << 8) | (b << 16) | (a << 24)) + +PNG::PNG() : _compressedBuffer(0), _compressedBufferSize(0), + _unfilteredSurface(0), _transparentColorSpecified(false) { +} + +PNG::~PNG() { + if (_unfilteredSurface) { + _unfilteredSurface->free(); + delete _unfilteredSurface; + } +} + +Graphics::Surface *PNG::getSurface(const PixelFormat &format) { + Graphics::Surface *output = new Graphics::Surface(); + output->create(_unfilteredSurface->w, _unfilteredSurface->h, format.bytesPerPixel); + byte *src = (byte *)_unfilteredSurface->pixels; + byte a = 0xFF; + + if (_header.colorType != kIndexed) { + if (_header.colorType == kTrueColor || _header.colorType == kTrueColorWithAlpha) { + if (_unfilteredSurface->bytesPerPixel != 3 && _unfilteredSurface->bytesPerPixel != 4) + error("Unsupported truecolor PNG format"); + } else if (_header.colorType == kGrayScale || _header.colorType == kGrayScaleWithAlpha) { + if (_unfilteredSurface->bytesPerPixel != 1 && _unfilteredSurface->bytesPerPixel != 2) + error("Unsupported grayscale PNG format"); + } + + for (uint16 i = 0; i < output->h; i++) { + for (uint16 j = 0; j < output->w; j++) { + if (format.bytesPerPixel == 2) { + if (_unfilteredSurface->bytesPerPixel == 1) { // Grayscale + if (_transparentColorSpecified) + a = (src[0] == _transparentColor[0]) ? 0 : 0xFF; + *((uint16 *)output->getBasePtr(j, i)) = format.ARGBToColor( a, src[0], src[0], src[0]); + } else if (_unfilteredSurface->bytesPerPixel == 2) { // Grayscale + alpha + *((uint16 *)output->getBasePtr(j, i)) = format.ARGBToColor(src[1], src[0], src[0], src[0]); + } else if (_unfilteredSurface->bytesPerPixel == 3) { // RGB + if (_transparentColorSpecified) { + bool isTransparentColor = (src[0] == _transparentColor[0] && + src[1] == _transparentColor[1] && + src[2] == _transparentColor[2]); + a = isTransparentColor ? 0 : 0xFF; + } + *((uint16 *)output->getBasePtr(j, i)) = format.ARGBToColor( a, src[0], src[1], src[2]); + } else if (_unfilteredSurface->bytesPerPixel == 4) { // RGBA + *((uint16 *)output->getBasePtr(j, i)) = format.ARGBToColor(src[3], src[0], src[1], src[2]); + } + } else { + if (_unfilteredSurface->bytesPerPixel == 1) { // Grayscale + if (_transparentColorSpecified) + a = (src[0] == _transparentColor[0]) ? 0 : 0xFF; + *((uint32 *)output->getBasePtr(j, i)) = format.ARGBToColor( a, src[0], src[0], src[0]); + } else if (_unfilteredSurface->bytesPerPixel == 2) { // Grayscale + alpha + *((uint32 *)output->getBasePtr(j, i)) = format.ARGBToColor(src[1], src[0], src[0], src[0]); + } else if (_unfilteredSurface->bytesPerPixel == 3) { // RGB + if (_transparentColorSpecified) { + bool isTransparentColor = (src[0] == _transparentColor[0] && + src[1] == _transparentColor[1] && + src[2] == _transparentColor[2]); + a = isTransparentColor ? 0 : 0xFF; + } + *((uint32 *)output->getBasePtr(j, i)) = format.ARGBToColor( a, src[0], src[1], src[2]); + } else if (_unfilteredSurface->bytesPerPixel == 4) { // RGBA + *((uint32 *)output->getBasePtr(j, i)) = format.ARGBToColor(src[3], src[0], src[1], src[2]); + } + } + + src += _unfilteredSurface->bytesPerPixel; + } + } + } else { + byte index, r, g, b; + + // Convert the indexed surface to the target pixel format + for (uint16 i = 0; i < output->h; i++) { + for (uint16 j = 0; j < output->w; j++) { + index = *src; + r = _palette[index * 4 + 0]; + g = _palette[index * 4 + 1]; + b = _palette[index * 4 + 2]; + a = _palette[index * 4 + 3]; + + if (format.bytesPerPixel == 2) + *((uint16 *)output->getBasePtr(j, i)) = format.ARGBToColor(a, r, g, b); + else + *((uint32 *)output->getBasePtr(j, i)) = format.ARGBToColor(a, r, g, b); + + src++; + } + } + } + + return output; +} + +bool PNG::read(Common::SeekableReadStream *str) { + uint32 chunkLength = 0, chunkType = 0; + _stream = str; + + // First, check the PNG signature + if (_stream->readUint32BE() != PNG_HEADER(0x89, 0x50, 0x4e, 0x47)) { + delete _stream; + return false; + } + if (_stream->readUint32BE() != PNG_HEADER(0x0d, 0x0a, 0x1a, 0x0a)) { + delete _stream; + return false; + } + + // Start reading chunks till we reach an IEND chunk + while (chunkType != kChunkIEND) { + // The chunk length does not include the type or CRC bytes + chunkLength = _stream->readUint32BE(); + chunkType = _stream->readUint32BE(); + + switch (chunkType) { + case kChunkIHDR: + readHeaderChunk(); + break; + case kChunkIDAT: + if (_compressedBufferSize == 0) { + _compressedBufferSize += chunkLength; + _compressedBuffer = new byte[_compressedBufferSize]; + _stream->read(_compressedBuffer, chunkLength); + } else { + // Expand the buffer + uint32 prevSize = _compressedBufferSize; + _compressedBufferSize += chunkLength; + byte *tmp = new byte[prevSize]; + memcpy(tmp, _compressedBuffer, prevSize); + delete[] _compressedBuffer; + _compressedBuffer = new byte[_compressedBufferSize]; + memcpy(_compressedBuffer, tmp, prevSize); + delete[] tmp; + _stream->read(_compressedBuffer + prevSize, chunkLength); + } + break; + case kChunkPLTE: // only available in indexed PNGs + if (_header.colorType != kIndexed) + error("A palette chunk has been found in a non-indexed PNG file"); + if (chunkLength % 3 != 0) + error("Palette chunk not divisible by 3"); + _paletteEntries = chunkLength / 3; + readPaletteChunk(); + break; + case kChunkIEND: + // End of stream + break; + case kChunktRNS: + readTransparencyChunk(chunkLength); + break; + default: + // Skip the chunk content + _stream->skip(chunkLength); + break; + } + + if (chunkType != kChunkIEND) + _stream->skip(4); // skip the chunk CRC checksum + } + + // We no longer need the file stream, thus close it here + delete _stream; + _stream = 0; + + // Unpack the compressed buffer + Common::MemoryReadStream *compData = new Common::MemoryReadStream(_compressedBuffer, _compressedBufferSize, DisposeAfterUse::YES); + _imageData = Common::wrapCompressedReadStream(compData); + + // Construct the final image + constructImage(); + + // Close the uncompressed stream, which will also delete the memory stream, + // and thus the original compressed buffer + delete _imageData; + + return true; +} + +/** + * Paeth predictor, used by PNG filter type 4 + * The parameters are of signed 16-bit integers, but should come + * from unsigned chars. The integers are only needed to make + * the paeth calculation correct. + * + * Taken from lodePNG, with a slight patch: + * http://www.atalasoft.com/cs/blogs/stevehawley/archive/2010/02/23/libpng-you-re-doing-it-wrong.aspx + */ +byte PNG::paethPredictor(int16 a, int16 b, int16 c) { + int16 pa = ABS<int16>(b - c); + int16 pb = ABS<int16>(a - c); + int16 pc = ABS<int16>(a + b - c - c); + + if (pa <= MIN<int16>(pb, pc)) + return (byte)a; + else if (pb <= pc) + return (byte)b; + else + return (byte)c; +} + +/** + * Unfilters a filtered PNG scan line. + * PNG filters are defined in: http://www.w3.org/TR/PNG/#9Filters + * Note that filters are always applied to bytes + * + * Taken from lodePNG + */ +void PNG::unfilterScanLine(byte *dest, const byte *scanLine, const byte *prevLine, uint16 byteWidth, byte filterType, uint16 length) { + uint16 i; + + switch (filterType) { + case kFilterNone: // no change + for (i = 0; i < length; i++) + dest[i] = scanLine[i]; + break; + case kFilterSub: // add the bytes to the left + for (i = 0; i < byteWidth; i++) + dest[i] = scanLine[i]; + for (i = byteWidth; i < length; i++) + dest[i] = scanLine[i] + dest[i - byteWidth]; + break; + case kFilterUp: // add the bytes of the above scanline + if (prevLine) { + for (i = 0; i < length; i++) + dest[i] = scanLine[i] + prevLine[i]; + } else { + for (i = 0; i < length; i++) + dest[i] = scanLine[i]; + } + break; + case kFilterAverage: // average value of the left and top left + if (prevLine) { + for (i = 0; i < byteWidth; i++) + dest[i] = scanLine[i] + prevLine[i] / 2; + for (i = byteWidth; i < length; i++) + dest[i] = scanLine[i] + ((dest[i - byteWidth] + prevLine[i]) / 2); + } else { + for (i = 0; i < byteWidth; i++) + dest[i] = scanLine[i]; + for (i = byteWidth; i < length; i++) + dest[i] = scanLine[i] + dest[i - byteWidth] / 2; + } + break; + case kFilterPaeth: // Paeth filter: http://www.w3.org/TR/PNG/#9Filter-type-4-Paeth + if (prevLine) { + for(i = 0; i < byteWidth; i++) + dest[i] = (scanLine[i] + prevLine[i]); // paethPredictor(0, prevLine[i], 0) is always prevLine[i] + for(i = byteWidth; i < length; i++) + dest[i] = (scanLine[i] + paethPredictor(dest[i - byteWidth], prevLine[i], prevLine[i - byteWidth])); + } else { + for(i = 0; i < byteWidth; i++) + dest[i] = scanLine[i]; + for(i = byteWidth; i < length; i++) + dest[i] = (scanLine[i] + dest[i - byteWidth]); // paethPredictor(dest[i - byteWidth], 0, 0) is always dest[i - byteWidth] + } + break; + default: + error("Unknown line filter"); + } + +} + +void PNG::constructImage() { + assert (_header.bitDepth != 0); + + byte *dest; + byte *scanLine; + byte *prevLine = 0; + byte filterType; + uint16 scanLineWidth = (_header.width * getNumColorChannels() * _header.bitDepth + 7) / 8; + + if (_unfilteredSurface) { + _unfilteredSurface->free(); + delete _unfilteredSurface; + } + _unfilteredSurface = new Graphics::Surface(); + _unfilteredSurface->create(_header.width, _header.height, (getNumColorChannels() * _header.bitDepth + 7) / 8); + scanLine = new byte[_unfilteredSurface->pitch]; + dest = (byte *)_unfilteredSurface->getBasePtr(0, 0); + + switch(_header.interlaceType) { + case kNonInterlaced: + for (uint16 y = 0; y < _unfilteredSurface->h; y++) { + filterType = _imageData->readByte(); + _imageData->read(scanLine, scanLineWidth); + unfilterScanLine(dest, scanLine, prevLine, _unfilteredSurface->bytesPerPixel, filterType, scanLineWidth); + prevLine = dest; + dest += _unfilteredSurface->pitch; + } + break; + case kInterlaced: + // Theoretically, this shouldn't be needed, as interlacing is only + // useful for web images. Interlaced PNG images require more complex + // handling, so unless having support for such images is needed, there + // is no reason to add support for them. + error("TODO: Support for interlaced PNG images"); + break; + } + + delete[] scanLine; +} + +void PNG::readHeaderChunk() { + _header.width = _stream->readUint32BE(); + _header.height = _stream->readUint32BE(); + _header.bitDepth = _stream->readByte(); + if (_header.bitDepth > 8) + error("Only PNGs with a bit depth of 1-8 bits are supported (i.e. PNG24)"); + _header.colorType = (PNGColorType)_stream->readByte(); + _header.compressionMethod = _stream->readByte(); + // Compression methods: http://www.w3.org/TR/PNG/#10Compression + // Only compression method 0 (deflate) is documented and supported + if (_header.compressionMethod != 0) + error("Unknown PNG compression method: %d", _header.compressionMethod); + _header.filterMethod = _stream->readByte(); + // Filter methods: http://www.w3.org/TR/PNG/#9Filters + // Only filter method 0 is documented and supported + if (_header.filterMethod != 0) + error("Unknown PNG filter method: %d", _header.filterMethod); + _header.interlaceType = (PNGInterlaceType)_stream->readByte(); +} + +byte PNG::getNumColorChannels() { + switch (_header.colorType) { + case kGrayScale: + return 1; // Gray + case kTrueColor: + return 3; // RGB + case kIndexed: + return 1; // Indexed + case kGrayScaleWithAlpha: + return 2; // Gray + Alpha + case kTrueColorWithAlpha: + return 4; // RGBA + default: + error("Unknown color type"); + } +} + +void PNG::readPaletteChunk() { + for (byte i = 0; i < _paletteEntries; i++) { + _palette[i * 4 + 0] = _stream->readByte(); // R + _palette[i * 4 + 1] = _stream->readByte(); // G + _palette[i * 4 + 2] = _stream->readByte(); // B + _palette[i * 4 + 3] = 0xFF; // Alpha, set in the tRNS chunk + } +} + +void PNG::readTransparencyChunk(uint32 chunkLength) { + _transparentColorSpecified = true; + + switch(_header.colorType) { + case kGrayScale: + _transparentColor[0] = _stream->readUint16BE(); + _transparentColor[1] = _transparentColor[0]; + _transparentColor[2] = _transparentColor[0]; + break; + case kTrueColor: + _transparentColor[0] = _stream->readUint16BE(); + _transparentColor[1] = _stream->readUint16BE(); + _transparentColor[2] = _stream->readUint16BE(); + break; + case kIndexed: + for (uint32 i = 0; i < chunkLength; i++) + _palette[i * 4 + 3] = _stream->readByte(); + // A transparency chunk may have less entries + // than the palette entries. The remaining ones + // are unmodified (set to 255). Check here: + // http://www.w3.org/TR/PNG/#11tRNS + break; + default: + error("Transparency chunk found in a PNG that has a separate transparency channel"); + } +} + +} // End of Graphics namespace diff --git a/graphics/png.h b/graphics/png.h new file mode 100644 index 0000000000..4de55d869b --- /dev/null +++ b/graphics/png.h @@ -0,0 +1,162 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * $URL$ + * $Id$ + * + */ + +/* + * PNG decoder used in engines: + * - sword25 + */ + +#ifndef GRAPHICS_PNG_H +#define GRAPHICS_PNG_H + +#include "graphics/surface.h" + +// PNG decoder, based on the W3C specs: +// http://www.w3.org/TR/PNG/ +// and lodePNG: +// http://members.gamedev.net/lode/projects/LodePNG/ +// and the ysflight PNG decoder: +// http://homepage3.nifty.com/ysflight/pngdecoder/pngdecodere.html + +// All the numbers are BE: http://www.w3.org/TR/PNG/#7Integers-and-byte-order + +// Note: At the moment, this decoder only supports non-interlaced images, and +// does not support truecolor/grayscale images with 16bit depth. +// +// Theoretically, interlaced images shouldn't be needed for games, as +// interlacing is only useful for images in websites. +// +// PNG images with 16bit depth (i.e. 48bit images) are quite rare, and can +// theoretically contain more than 16.7 millions of colors (the so-called "deep +// color" representation). In essence, each of the R, G, B and A components in +// them is specified with 2 bytes, instead of 1. However, the PNG specification +// always refers to color components with 1 byte each, so this part of the spec +// is a bit unclear. For now, these won't be supported, until a suitable sample +// is found. + +namespace Common { +class SeekableReadStream; +} + +namespace Graphics { + +enum PNGColorType { + kGrayScale = 0, // bit depths: 1, 2, 4, 8, 16 + kTrueColor = 2, // bit depths: 8, 16 + kIndexed = 3, // bit depths: 1, 2, 4, 8 + kGrayScaleWithAlpha = 4, // bit depths: 8, 16 + kTrueColorWithAlpha = 6 // bit depths: 8, 16 +}; + +enum PNGInterlaceType { + kNonInterlaced = 0, + kInterlaced = 1 +}; + +struct PNGHeader { + uint32 width; + uint32 height; + byte bitDepth; + PNGColorType colorType; + byte compressionMethod; + byte filterMethod; + PNGInterlaceType interlaceType; +}; + +struct PixelFormat; + +class PNG { +public: + PNG(); + ~PNG(); + + /** + * Reads a PNG image from the specified stream + */ + bool read(Common::SeekableReadStream *str); + + /** + * Returns the information obtained from the PNG header. + */ + PNGHeader getHeader() const { return _header; } + + /** + * Returns the PNG image, formatted for the specified pixel format. + */ + Graphics::Surface *getSurface(const PixelFormat &format); + + /** + * Returns the indexed PNG8 image. Used for PNGs with an indexed 256 color + * palette, when they're shown on an 8-bit color screen, as no translation + * is taking place. + */ + Graphics::Surface *getIndexedSurface() { + if (_header.colorType != kIndexed) + error("Indexed surface requested for a non-indexed PNG"); + return _unfilteredSurface; + } + + /** + * Returns the palette of the specified PNG8 image + */ + void getPalette(byte *palette, byte &entries) { + if (_header.colorType != kIndexed) + error("Palette requested for a non-indexed PNG"); + palette = _palette; + entries = _paletteEntries; + } + +private: + void readHeaderChunk(); + byte getNumColorChannels(); + + void readPaletteChunk(); + void readTransparencyChunk(uint32 chunkLength); + + void constructImage(); + void unfilterScanLine(byte *dest, const byte *scanLine, const byte *prevLine, uint16 byteWidth, byte filterType, uint16 length); + byte paethPredictor(int16 a, int16 b, int16 c); + + // The original file stream + Common::SeekableReadStream *_stream; + // The unzipped image data stream + Common::SeekableReadStream *_imageData; + + PNGHeader _header; + + byte _palette[256 * 4]; // RGBA + byte _paletteEntries; + uint16 _transparentColor[3]; + bool _transparentColorSpecified; + + byte *_compressedBuffer; + uint32 _compressedBufferSize; + + Graphics::Surface *_unfilteredSurface; +}; + +} // End of Graphics namespace + +#endif // GRAPHICS_PNG_H |