/* ** pngtexture.cpp ** Texture class for PNG images ** **--------------------------------------------------------------------------- ** Copyright 2004-2007 Randy Heit ** All rights reserved. ** ** Redistribution and use in source and binary forms, with or without ** modification, are permitted provided that the following conditions ** are met: ** ** 1. Redistributions of source code must retain the above copyright ** notice, this list of conditions and the following disclaimer. ** 2. Redistributions in binary form must reproduce the above copyright ** notice, this list of conditions and the following disclaimer in the ** documentation and/or other materials provided with the distribution. ** 3. The name of the author may not be used to endorse or promote products ** derived from this software without specific prior written permission. ** ** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR ** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES ** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. ** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, ** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT ** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF ** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **--------------------------------------------------------------------------- ** ** */ #include "doomtype.h" #include "files.h" #include "w_wad.h" #include "templates.h" #include "m_png.h" #include "bitmap.h" #include "v_palette.h" #include "textures/textures.h" #include //========================================================================== // // A PNG texture // //========================================================================== class FPNGTexture : public FTexture { public: FPNGTexture (FileReader &lump, int lumpnum, const FString &filename, int width, int height, BYTE bitdepth, BYTE colortype, BYTE interlace); ~FPNGTexture (); const BYTE *GetColumn (unsigned int column, const Span **spans_out); const BYTE *GetPixels (); const uint32_t *GetPixelsBgra (); void Unload (); FTextureFormat GetFormat (); int CopyTrueColorPixels(FBitmap *bmp, int x, int y, int rotate, FCopyInfo *inf = NULL); bool UseBasePalette(); protected: FString SourceFile; BYTE *Pixels; Span **Spans; BYTE BitDepth; BYTE ColorType; BYTE Interlace; bool HaveTrans; WORD NonPaletteTrans[3]; std::vector PngPalette; BYTE *PaletteMap; int PaletteSize; DWORD StartOfIDAT; void MakeTexture (); void MakeTextureBgra (); friend class FTexture; }; //========================================================================== // // // //========================================================================== FTexture *PNGTexture_TryCreate(FileReader & data, int lumpnum) { union { DWORD dw; WORD w[2]; BYTE b[4]; } first4bytes; DWORD width, height; BYTE bitdepth, colortype, compression, filter, interlace; // This is most likely a PNG, but make sure. (Note that if the // first 4 bytes match, but later bytes don't, we assume it's // a corrupt PNG.) data.Seek(0, SEEK_SET); if (data.Read (first4bytes.b, 4) != 4) return NULL; if (first4bytes.dw != MAKE_ID(137,'P','N','G')) return NULL; if (data.Read (first4bytes.b, 4) != 4) return NULL; if (first4bytes.dw != MAKE_ID(13,10,26,10)) return NULL; if (data.Read (first4bytes.b, 4) != 4) return NULL; if (first4bytes.dw != MAKE_ID(0,0,0,13)) return NULL; if (data.Read (first4bytes.b, 4) != 4) return NULL; if (first4bytes.dw != MAKE_ID('I','H','D','R')) return NULL; // The PNG looks valid so far. Check the IHDR to make sure it's a // type of PNG we support. data.Read(&width, 4); data.Read(&height, 4); data >> bitdepth >> colortype >> compression >> filter >> interlace; if (compression != 0 || filter != 0 || interlace > 1) { return NULL; } if (!((1 << colortype) & 0x5D)) { return NULL; } if (!((1 << bitdepth) & 0x116)) { return NULL; } // Just for completeness, make sure the PNG has something more than an IHDR. data.Seek (4, SEEK_CUR); data.Read (first4bytes.b, 4); if (first4bytes.dw == 0) { data.Read (first4bytes.b, 4); if (first4bytes.dw == MAKE_ID('I','E','N','D')) { return NULL; } } return new FPNGTexture (data, lumpnum, FString(), BigLong((int)width), BigLong((int)height), bitdepth, colortype, interlace); } //========================================================================== // // // //========================================================================== FTexture *PNGTexture_CreateFromFile(PNGHandle *png, const FString &filename) { DWORD width, height; BYTE bitdepth, colortype, compression, filter, interlace; if (M_FindPNGChunk(png, MAKE_ID('I','H','D','R')) == 0) { return NULL; } // Check the IHDR to make sure it's a type of PNG we support. png->File->Read(&width, 4); png->File->Read(&height, 4); (*png->File) >> bitdepth >> colortype >> compression >> filter >> interlace; if (compression != 0 || filter != 0 || interlace > 1) { return NULL; } if (!((1 << colortype) & 0x5D)) { return NULL; } if (!((1 << bitdepth) & 0x116)) { return NULL; } return new FPNGTexture (*png->File, -1, filename, BigLong((int)width), BigLong((int)height), bitdepth, colortype, interlace); } //========================================================================== // // // //========================================================================== FPNGTexture::FPNGTexture (FileReader &lump, int lumpnum, const FString &filename, int width, int height, BYTE depth, BYTE colortype, BYTE interlace) : FTexture(NULL, lumpnum), SourceFile(filename), Pixels(0), Spans(0), BitDepth(depth), ColorType(colortype), Interlace(interlace), HaveTrans(false), PaletteMap(0), PaletteSize(0), StartOfIDAT(0) { union { DWORD palette[256]; BYTE pngpal[256][3]; } p; BYTE trans[256]; DWORD len, id; int i; UseType = TEX_MiscPatch; LeftOffset = 0; TopOffset = 0; bMasked = false; Width = width; Height = height; CalcBitSize (); memset(trans, 255, 256); // Parse pre-IDAT chunks. I skip the CRCs. Is that bad? lump.Seek(33, SEEK_SET); lump.Read(&len, 4); lump.Read(&id, 4); while (id != MAKE_ID('I','D','A','T') && id != MAKE_ID('I','E','N','D')) { len = BigLong((unsigned int)len); switch (id) { default: lump.Seek (len, SEEK_CUR); break; case MAKE_ID('g','r','A','b'): // This is like GRAB found in an ILBM, except coordinates use 4 bytes { DWORD hotx, hoty; int ihotx, ihoty; lump.Read(&hotx, 4); lump.Read(&hoty, 4); ihotx = BigLong((int)hotx); ihoty = BigLong((int)hoty); if (ihotx < -32768 || ihotx > 32767) { Printf ("X-Offset for PNG texture %s is bad: %d (0x%08x)\n", Wads.GetLumpFullName (lumpnum), ihotx, ihotx); ihotx = 0; } if (ihoty < -32768 || ihoty > 32767) { Printf ("Y-Offset for PNG texture %s is bad: %d (0x%08x)\n", Wads.GetLumpFullName (lumpnum), ihoty, ihoty); ihoty = 0; } LeftOffset = ihotx; TopOffset = ihoty; } break; case MAKE_ID('P','L','T','E'): PaletteSize = MIN (len / 3, 256); lump.Read (p.pngpal, PaletteSize * 3); if (PaletteSize * 3 != (int)len) { lump.Seek (len - PaletteSize * 3, SEEK_CUR); } for (i = 0; i < PaletteSize; i++) { PngPalette.push_back(p.pngpal[i][0]); PngPalette.push_back(p.pngpal[i][1]); PngPalette.push_back(p.pngpal[i][2]); } for (i = PaletteSize - 1; i >= 0; --i) { p.palette[i] = MAKERGB(p.pngpal[i][0], p.pngpal[i][1], p.pngpal[i][2]); } break; case MAKE_ID('t','R','N','S'): lump.Read (trans, len); HaveTrans = true; // Save for colortype 2 NonPaletteTrans[0] = WORD(trans[0] * 256 + trans[1]); NonPaletteTrans[1] = WORD(trans[2] * 256 + trans[3]); NonPaletteTrans[2] = WORD(trans[4] * 256 + trans[5]); break; case MAKE_ID('a','l','P','h'): bAlphaTexture = true; bMasked = true; break; } lump.Seek(4, SEEK_CUR); // Skip CRC lump.Read(&len, 4); id = MAKE_ID('I','E','N','D'); lump.Read(&id, 4); } StartOfIDAT = lump.Tell() - 8; switch (colortype) { case 4: // Grayscale + Alpha bMasked = true; // intentional fall-through case 0: // Grayscale if (!bAlphaTexture) { if (colortype == 0 && HaveTrans && NonPaletteTrans[0] < 256) { bMasked = true; PaletteSize = 256; PaletteMap = new BYTE[256]; memcpy (PaletteMap, GrayMap, 256); PaletteMap[NonPaletteTrans[0]] = 0; } else { PaletteMap = GrayMap; } } break; case 3: // Paletted PaletteMap = new BYTE[PaletteSize]; GPalette.MakeRemap (p.palette, PaletteMap, trans, PaletteSize); for (i = 0; i < PaletteSize; ++i) { if (trans[i] == 0) { bMasked = true; PaletteMap[i] = 0; } } break; case 6: // RGB + Alpha bMasked = true; break; case 2: // RGB bMasked = HaveTrans; break; } } //========================================================================== // // // //========================================================================== FPNGTexture::~FPNGTexture () { Unload (); if (Spans != NULL) { FreeSpans (Spans); Spans = NULL; } if (PaletteMap != NULL && PaletteMap != GrayMap) { delete[] PaletteMap; PaletteMap = NULL; } } //========================================================================== // // // //========================================================================== void FPNGTexture::Unload () { delete[] Pixels; Pixels = NULL; FTexture::Unload(); } //========================================================================== // // // //========================================================================== FTextureFormat FPNGTexture::GetFormat() { #if 0 switch (ColorType) { case 3: return TEX_Pal; case 0: return TEX_Gray; default: return TEX_RGB; } #else // For now, create a true color texture to preserve all colors. return TEX_RGB; #endif } //========================================================================== // // // //========================================================================== const BYTE *FPNGTexture::GetColumn (unsigned int column, const Span **spans_out) { if (Pixels == NULL) { MakeTexture (); } if ((unsigned)column >= (unsigned)Width) { if (WidthMask + 1 == Width) { column &= WidthMask; } else { column %= Width; } } if (spans_out != NULL) { if (Spans == NULL) { Spans = CreateSpans (Pixels); } *spans_out = Spans[column]; } return Pixels + column*Height; } //========================================================================== // // // //========================================================================== const BYTE *FPNGTexture::GetPixels () { if (Pixels == NULL) { MakeTexture (); } return Pixels; } const uint32_t *FPNGTexture::GetPixelsBgra() { if (PixelsBgra.empty()) { MakeTextureBgra(); } return PixelsBgra.data(); } //========================================================================== // // // //========================================================================== void FPNGTexture::MakeTexture () { FileReader *lump; if (SourceLump >= 0) { lump = new FWadLump(Wads.OpenLumpNum(SourceLump)); } else { lump = new FileReader(SourceFile.GetChars()); } Pixels = new BYTE[Width*Height]; if (StartOfIDAT == 0) { memset (Pixels, 0x99, Width*Height); } else { DWORD len, id; lump->Seek (StartOfIDAT, SEEK_SET); lump->Read(&len, 4); lump->Read(&id, 4); if (ColorType == 0 || ColorType == 3) /* Grayscale and paletted */ { M_ReadIDAT (lump, Pixels, Width, Height, Width, BitDepth, ColorType, Interlace, BigLong((unsigned int)len)); if (Width == Height) { if (PaletteMap != NULL) { FlipSquareBlockRemap (Pixels, Width, Height, PaletteMap); } else { FlipSquareBlock (Pixels, Width, Height); } } else { BYTE *newpix = new BYTE[Width*Height]; if (PaletteMap != NULL) { FlipNonSquareBlockRemap (newpix, Pixels, Width, Height, Width, PaletteMap); } else { FlipNonSquareBlock (newpix, Pixels, Width, Height, Width); } BYTE *oldpix = Pixels; Pixels = newpix; delete[] oldpix; } } else /* RGB and/or Alpha present */ { int bytesPerPixel = ColorType == 2 ? 3 : ColorType == 4 ? 2 : 4; BYTE *tempix = new BYTE[Width * Height * bytesPerPixel]; BYTE *in, *out; int x, y, pitch, backstep; M_ReadIDAT (lump, tempix, Width, Height, Width*bytesPerPixel, BitDepth, ColorType, Interlace, BigLong((unsigned int)len)); in = tempix; out = Pixels; // Convert from source format to paletted, column-major. // Formats with alpha maps are reduced to only 1 bit of alpha. switch (ColorType) { case 2: // RGB pitch = Width * 3; backstep = Height * pitch - 3; for (x = Width; x > 0; --x) { for (y = Height; y > 0; --y) { if (!HaveTrans) { *out++ = RGB32k.RGB[in[0]>>3][in[1]>>3][in[2]>>3]; } else { if (in[0] == NonPaletteTrans[0] && in[1] == NonPaletteTrans[1] && in[2] == NonPaletteTrans[2]) { *out++ = 0; } else { *out++ = RGB32k.RGB[in[0]>>3][in[1]>>3][in[2]>>3]; } } in += pitch; } in -= backstep; } break; case 4: // Grayscale + Alpha pitch = Width * 2; backstep = Height * pitch - 2; if (PaletteMap != NULL) { for (x = Width; x > 0; --x) { for (y = Height; y > 0; --y) { *out++ = in[1] < 128 ? 0 : PaletteMap[in[0]]; in += pitch; } in -= backstep; } } else { for (x = Width; x > 0; --x) { for (y = Height; y > 0; --y) { *out++ = in[1] < 128 ? 0 : in[0]; in += pitch; } in -= backstep; } } break; case 6: // RGB + Alpha pitch = Width * 4; backstep = Height * pitch - 4; for (x = Width; x > 0; --x) { for (y = Height; y > 0; --y) { *out++ = in[3] < 128 ? 0 : RGB32k.RGB[in[0]>>3][in[1]>>3][in[2]>>3]; in += pitch; } in -= backstep; } break; } delete[] tempix; } } delete lump; } void FPNGTexture::MakeTextureBgra () { FileReader *lump; if (SourceLump >= 0) { lump = new FWadLump(Wads.OpenLumpNum(SourceLump)); } else { lump = new FileReader(SourceFile.GetChars()); } PixelsBgra.resize(Width * Height, 0xffff0000); if (StartOfIDAT != 0) { DWORD len, id; lump->Seek (StartOfIDAT, SEEK_SET); lump->Read(&len, 4); lump->Read(&id, 4); if (ColorType == 0 || ColorType == 3) /* Grayscale and paletted */ { std::vector src(Width*Height); M_ReadIDAT (lump, src.data(), Width, Height, Width, BitDepth, ColorType, Interlace, BigLong((unsigned int)len)); if (!PngPalette.empty()) { for (int x = 0; x < Width; x++) { for (int y = 0; y < Height; y++) { uint32_t r = PngPalette[src[x + y * Width] * 3 + 0]; uint32_t g = PngPalette[src[x + y * Width] * 3 + 1]; uint32_t b = PngPalette[src[x + y * Width] * 3 + 2]; PixelsBgra[x * Height + y] = 0xff000000 | (r << 16) | (g << 8) | b; } } } else { for (int x = 0; x < Width; x++) { for (int y = 0; y < Height; y++) { uint32_t gray = src[x + y * Width]; PixelsBgra[x * Height + y] = 0xff000000 | (gray << 16) | (gray << 8) | gray; } } } } else /* RGB and/or Alpha present */ { int bytesPerPixel = ColorType == 2 ? 3 : ColorType == 4 ? 2 : 4; BYTE *tempix = new BYTE[Width * Height * bytesPerPixel]; BYTE *in; uint32_t *out; int x, y, pitch, backstep; M_ReadIDAT (lump, tempix, Width, Height, Width*bytesPerPixel, BitDepth, ColorType, Interlace, BigLong((unsigned int)len)); in = tempix; out = PixelsBgra.data(); // Convert from source format to paletted, column-major. // Formats with alpha maps are reduced to only 1 bit of alpha. switch (ColorType) { case 2: // RGB pitch = Width * 3; backstep = Height * pitch - 3; for (x = Width; x > 0; --x) { for (y = Height; y > 0; --y) { if (!HaveTrans) { *out++ = 0xff000000 | (((uint32_t)in[0]) << 16) | (((uint32_t)in[1]) << 8) | ((uint32_t)in[2]); } else { if (in[0] == NonPaletteTrans[0] && in[1] == NonPaletteTrans[1] && in[2] == NonPaletteTrans[2]) { *out++ = 0; } else { *out++ = 0xff000000 | (((uint32_t)in[0]) << 16) | (((uint32_t)in[1]) << 8) | ((uint32_t)in[2]); } } in += pitch; } in -= backstep; } break; case 4: // Grayscale + Alpha pitch = Width * 2; backstep = Height * pitch - 2; for (x = Width; x > 0; --x) { for (y = Height; y > 0; --y) { uint32_t alpha = in[1]; uint32_t gray = in[0]; *out++ = (alpha << 24) | (gray << 16) | (gray << 8) | gray; in += pitch; } in -= backstep; } break; case 6: // RGB + Alpha pitch = Width * 4; backstep = Height * pitch - 4; for (x = Width; x > 0; --x) { for (y = Height; y > 0; --y) { *out++ = (((uint32_t)in[3]) << 24) | (((uint32_t)in[0]) << 16) | (((uint32_t)in[1]) << 8) | ((uint32_t)in[2]); in += pitch; } in -= backstep; } break; } delete[] tempix; } } delete lump; } //=========================================================================== // // FPNGTexture::CopyTrueColorPixels // //=========================================================================== int FPNGTexture::CopyTrueColorPixels(FBitmap *bmp, int x, int y, int rotate, FCopyInfo *inf) { // Parse pre-IDAT chunks. I skip the CRCs. Is that bad? PalEntry pe[256]; DWORD len, id; FileReader *lump; static char bpp[] = {1, 0, 3, 1, 2, 0, 4}; int pixwidth = Width * bpp[ColorType]; int transpal = false; if (SourceLump >= 0) { lump = new FWadLump(Wads.OpenLumpNum(SourceLump)); } else { lump = new FileReader(SourceFile.GetChars()); } lump->Seek(33, SEEK_SET); for(int i = 0; i < 256; i++) // default to a gray map pe[i] = PalEntry(255,i,i,i); lump->Read(&len, 4); lump->Read(&id, 4); while (id != MAKE_ID('I','D','A','T') && id != MAKE_ID('I','E','N','D')) { len = BigLong((unsigned int)len); switch (id) { default: lump->Seek (len, SEEK_CUR); break; case MAKE_ID('P','L','T','E'): for(int i = 0; i < PaletteSize; i++) { (*lump) >> pe[i].r >> pe[i].g >> pe[i].b; } break; case MAKE_ID('t','R','N','S'): if (ColorType == 3) { for(DWORD i = 0; i < len; i++) { (*lump) >> pe[i].a; if (pe[i].a != 0 && pe[i].a != 255) transpal = true; } } else { lump->Seek(len, SEEK_CUR); } break; } lump->Seek(4, SEEK_CUR); // Skip CRC lump->Read(&len, 4); id = MAKE_ID('I','E','N','D'); lump->Read(&id, 4); } if (ColorType == 0 && HaveTrans && NonPaletteTrans[0] < 256) { pe[NonPaletteTrans[0]].a = 0; transpal = true; } BYTE * Pixels = new BYTE[pixwidth * Height]; lump->Seek (StartOfIDAT, SEEK_SET); lump->Read(&len, 4); lump->Read(&id, 4); M_ReadIDAT (lump, Pixels, Width, Height, pixwidth, BitDepth, ColorType, Interlace, BigLong((unsigned int)len)); delete lump; switch (ColorType) { case 0: case 3: bmp->CopyPixelData(x, y, Pixels, Width, Height, 1, Width, rotate, pe, inf); break; case 2: if (!HaveTrans) { bmp->CopyPixelDataRGB(x, y, Pixels, Width, Height, 3, pixwidth, rotate, CF_RGB, inf); } else { bmp->CopyPixelDataRGB(x, y, Pixels, Width, Height, 3, pixwidth, rotate, CF_RGBT, inf, NonPaletteTrans[0], NonPaletteTrans[1], NonPaletteTrans[2]); transpal = true; } break; case 4: bmp->CopyPixelDataRGB(x, y, Pixels, Width, Height, 2, pixwidth, rotate, CF_IA, inf); transpal = -1; break; case 6: bmp->CopyPixelDataRGB(x, y, Pixels, Width, Height, 4, pixwidth, rotate, CF_RGBA, inf); transpal = -1; break; default: break; } delete[] Pixels; return transpal; } //=========================================================================== // // This doesn't check if the palette is identical with the base palette // I don't think it's worth the hassle because it's only of importance // when compositing multipatch textures. // //=========================================================================== bool FPNGTexture::UseBasePalette() { return false; }