// Copyright Epic Games, Inc. All Rights Reserved. #include "BmpImageWrapper.h" #include "ImageWrapperPrivate.h" #include "BmpImageSupport.h" /** * BMP image wrapper class. * This code was adapted from UTextureFactory::ImportTexture, but has not been throughly tested. */ FBmpImageWrapper::FBmpImageWrapper(bool bInHasHeader, bool bInHalfHeight) : FImageWrapperBase() , bHasHeader(bInHasHeader) , bHalfHeight(bInHalfHeight) { } void FBmpImageWrapper::Uncompress(const ERGBFormat InFormat, const int32 InBitDepth) { RawData.Empty(); if ( CompressedData.IsEmpty() ) { return; } const uint8* Buffer = CompressedData.GetData(); if (!bHasHeader || ((CompressedData.Num()>=sizeof(FBitmapFileHeader)+sizeof(FBitmapInfoHeader)) && Buffer[0]=='B' && Buffer[1]=='M')) { UncompressBMPData(InFormat, InBitDepth); } } void FBmpImageWrapper::UncompressBMPData(const ERGBFormat InFormat, const int32 InBitDepth) { // always writes BGRA8 : check( InFormat == ERGBFormat::BGRA ); check( InBitDepth == 8 ); check( ! CompressedData.IsEmpty() ); const uint8* Buffer = CompressedData.GetData(); const FBitmapInfoHeader* bmhdr = nullptr; const uint8* Bits = nullptr; EBitmapHeaderVersion HeaderVersion = EBitmapHeaderVersion::BHV_BITMAPINFOHEADER; if (bHasHeader) { bmhdr = (FBitmapInfoHeader *)(Buffer + sizeof(FBitmapFileHeader)); Bits = Buffer + ((FBitmapFileHeader *)Buffer)->bfOffBits; HeaderVersion = ((FBitmapFileHeader *)Buffer)->GetHeaderVersion(); } else { bmhdr = (FBitmapInfoHeader *)Buffer; Bits = Buffer + sizeof(FBitmapInfoHeader); } if (bmhdr->biCompression != BCBI_RGB && bmhdr->biCompression != BCBI_BITFIELDS) { UE_LOG(LogImageWrapper, Error, TEXT("RLE compression of BMP images not supported")); return; } if (bmhdr->biPlanes==1 && bmhdr->biBitCount==8) { // Do palette. const uint8* bmpal = (uint8*)CompressedData.GetData() + sizeof(FBitmapFileHeader) + sizeof(FBitmapInfoHeader); // Set texture properties. Width = bmhdr->biWidth; const bool bNegativeHeight = (bmhdr->biHeight < 0); Height = FMath::Abs(bHalfHeight ? bmhdr->biHeight / 2 : bmhdr->biHeight); Format = ERGBFormat::BGRA; RawData.Empty(Height * Width * 4); RawData.AddUninitialized(Height * Width * 4); FColor* ImageData = (FColor*)RawData.GetData(); // If the number for color palette entries is 0, we need to default to 2^biBitCount entries. In this case 2^8 = 256 int32 clrPaletteCount = bmhdr->biClrUsed ? bmhdr->biClrUsed : 256; TArray Palette; for (int32 i = 0; i < clrPaletteCount; i++) { Palette.Add(FColor(bmpal[i * 4 + 2], bmpal[i * 4 + 1], bmpal[i * 4 + 0], 255)); } while (Palette.Num() < 256) { Palette.Add(FColor(0, 0, 0, 255)); } // Copy scanlines, accounting for scanline direction according to the Height field. const int32 SrcStride = Align(Width, 4); const int32 SrcPtrDiff = bNegativeHeight ? SrcStride : -SrcStride; const uint8* SrcPtr = Bits + (bNegativeHeight ? 0 : Height - 1) * SrcStride; for (int32 Y = 0; Y < Height; Y++) { for (int32 X = 0; X < Width; X++) { *ImageData++ = Palette[SrcPtr[X]]; } SrcPtr += SrcPtrDiff; } } else if (bmhdr->biPlanes==1 && bmhdr->biBitCount==24) { // Set texture properties. Width = bmhdr->biWidth; const bool bNegativeHeight = (bmhdr->biHeight < 0); Height = FMath::Abs(bHalfHeight ? bmhdr->biHeight / 2 : bmhdr->biHeight); Format = ERGBFormat::BGRA; RawData.Empty(Height * Width * 4); RawData.AddUninitialized(Height * Width * 4); uint8* ImageData = RawData.GetData(); // Copy scanlines, accounting for scanline direction according to the Height field. const int32 SrcStride = Align(Width * 3, 4); const int32 SrcPtrDiff = bNegativeHeight ? SrcStride : -SrcStride; const uint8* SrcPtr = Bits + (bNegativeHeight ? 0 : Height - 1) * SrcStride; for (int32 Y = 0; Y < Height; Y++) { const uint8* SrcRowPtr = SrcPtr; for (int32 X = 0; X < Width; X++) { *ImageData++ = *SrcRowPtr++; *ImageData++ = *SrcRowPtr++; *ImageData++ = *SrcRowPtr++; *ImageData++ = 0xFF; } SrcPtr += SrcPtrDiff; } } else if (bmhdr->biPlanes==1 && bmhdr->biBitCount==32) { // Set texture properties. Width = bmhdr->biWidth; const bool bNegativeHeight = (bmhdr->biHeight < 0); Height = FMath::Abs(bHalfHeight ? bmhdr->biHeight / 2 : bmhdr->biHeight); Format = ERGBFormat::BGRA; RawData.Empty(Height * Width * 4); RawData.AddUninitialized(Height * Width * 4); uint8* ImageData = RawData.GetData(); // Copy scanlines, accounting for scanline direction according to the Height field. const int32 SrcStride = Width * 4; const int32 SrcPtrDiff = bNegativeHeight ? SrcStride : -SrcStride; const uint8* SrcPtr = Bits + (bNegativeHeight ? 0 : Height - 1) * SrcStride; // Getting the bmiColors member from the BITMAPINFO, which is used as a mask on BitFields compression. const FBmiColorsMask* ColorMask = (FBmiColorsMask*)(CompressedData.GetData() + sizeof(FBitmapFileHeader) + sizeof(FBitmapInfoHeader)); // Header version 4 introduced the option to declare custom color space, so we can't just assume sRGB past that version. const bool bAssumeRGBCompression = bmhdr->biCompression == BCBI_RGB || (bmhdr->biCompression == BCBI_BITFIELDS && ColorMask->IsMaskRGB8() && HeaderVersion < EBitmapHeaderVersion::BHV_BITMAPV4HEADER); if (bAssumeRGBCompression) { for (int32 Y = 0; Y < Height; Y++) { const uint8* SrcRowPtr = SrcPtr; for (int32 X = 0; X < Width; X++) { *ImageData++ = *SrcRowPtr++; *ImageData++ = *SrcRowPtr++; *ImageData++ = *SrcRowPtr++; *ImageData++ = 0xFF; //In BCBI_RGB compression the last 8 bits of the pixel are not used. SrcRowPtr++; } SrcPtr += SrcPtrDiff; } } else if (bmhdr->biCompression == BCBI_BITFIELDS) { //If the header version is V4 or higher we need to make sure we are still using sRGB format if (HeaderVersion >= EBitmapHeaderVersion::BHV_BITMAPV4HEADER) { const FBitmapInfoHeaderV4* bmhdrV4 = (FBitmapInfoHeaderV4*)(Buffer + sizeof(FBitmapFileHeader)); if (bmhdrV4->biCSType != (uint32)EBitmapCSType::BCST_LCS_sRGB && bmhdrV4->biCSType != (uint32)EBitmapCSType::BCST_LCS_WINDOWS_COLOR_SPACE) { UE_LOG(LogImageWrapper, Error, TEXT("BMP uses an unsupported custom color space definition, sRGB color space will be used instead.")); } } //Calculating the bit mask info needed to remap the pixels' color values. uint32 TrailingBits[4]; float MappingRatio[4]; for (uint32 MaskIndex = 0; MaskIndex < 4; MaskIndex++) { TrailingBits[MaskIndex] = FMath::CountTrailingZeros(ColorMask->RGBAMask[MaskIndex]); const uint32 NumberOfBits = 32 - (TrailingBits[MaskIndex] + FMath::CountLeadingZeros(ColorMask->RGBAMask[MaskIndex])); MappingRatio[MaskIndex] = NumberOfBits == 0 ? 0 : (FMath::Exp2(8.f) - 1) / (FMath::Exp2(static_cast(NumberOfBits)) - 1); } //In header pre-version 4, we should ignore the last 32bit (alpha) content. const bool bHasAlphaChannel = ColorMask->RGBAMask[3] != 0 && HeaderVersion >= EBitmapHeaderVersion::BHV_BITMAPV4HEADER; for (int32 Y = 0; Y < Height; Y++) { const uint32* SrcPixel = (uint32*)SrcPtr; for (int32 X = 0; X < Width; X++) { // Set the color values in BGRA order. for (int32 ColorIndex = 2; ColorIndex >= 0; ColorIndex--) { *ImageData++ = FMath::RoundToInt(((*SrcPixel & ColorMask->RGBAMask[ColorIndex]) >> TrailingBits[ColorIndex]) * MappingRatio[ColorIndex]); } *ImageData++ = bHasAlphaChannel ? FMath::RoundToInt(((*SrcPixel & ColorMask->RGBAMask[3]) >> TrailingBits[3]) * MappingRatio[3]) : 0xFF; SrcPixel++; } SrcPtr += SrcPtrDiff; } } else { UE_LOG(LogImageWrapper, Error, TEXT("BMP uses an unsupported compression format (%i)"), bmhdr->biCompression) } } else if (bmhdr->biPlanes==1 && bmhdr->biBitCount==16) { UE_LOG(LogImageWrapper, Error, TEXT("BMP 16 bit format no longer supported. Use terrain tools for importing/exporting heightmaps.")); } else { UE_LOG(LogImageWrapper, Error, TEXT("BMP uses an unsupported format (%i/%i)"), bmhdr->biPlanes, bmhdr->biBitCount); } } bool FBmpImageWrapper::SetCompressed(const void* InCompressedData, int64 InCompressedSize) { bool bResult = FImageWrapperBase::SetCompressed(InCompressedData, InCompressedSize); bResult = bResult && (bHasHeader ? LoadBMPHeader() : LoadBMPInfoHeader()); // Fetch the variables from the header info if ( ! bResult ) { CompressedData.Reset(); } return bResult; } bool FBmpImageWrapper::LoadBMPHeader() { //note: not Endian correct const FBitmapInfoHeader* bmhdr = (FBitmapInfoHeader *)(CompressedData.GetData() + sizeof(FBitmapFileHeader)); const FBitmapFileHeader* bmf = (FBitmapFileHeader *)(CompressedData.GetData() + 0); if ((CompressedData.Num() >= sizeof(FBitmapFileHeader) + sizeof(FBitmapInfoHeader)) && CompressedData.GetData()[0] == 'B' && CompressedData.GetData()[1] == 'M') { if (bmhdr->biCompression != BCBI_RGB && bmhdr->biCompression != BCBI_BITFIELDS) { UE_LOG(LogImageWrapper, Error, TEXT("RLE compression of BMP images not supported")); return false; } if (bmhdr->biPlanes==1 && (bmhdr->biBitCount==8 || bmhdr->biBitCount==24 || bmhdr->biBitCount==32)) { // Set texture properties. Width = bmhdr->biWidth; Height = FMath::Abs(bmhdr->biHeight); Format = ERGBFormat::BGRA; // YUCK // BmpImageWrapper was reporting BitDepth *total* not per-channel (eg. 24) // some legacy code knew that and expected to see the bad values, beware //BitDepth = bmhdr->biBitCount; BitDepth = 8; return true; } if (bmhdr->biPlanes == 1 && bmhdr->biBitCount == 16) { UE_LOG(LogImageWrapper, Error, TEXT("BMP 16 bit format no longer supported. Use terrain tools for importing/exporting heightmaps.")); } else { UE_LOG(LogImageWrapper, Error, TEXT("BMP uses an unsupported format (%i/%i)"), bmhdr->biPlanes, bmhdr->biBitCount); } } return false; } bool FBmpImageWrapper::LoadBMPInfoHeader() { //note: not Endian correct const FBitmapInfoHeader* bmhdr = (FBitmapInfoHeader *)CompressedData.GetData(); if (bmhdr->biCompression != BCBI_RGB && bmhdr->biCompression != BCBI_BITFIELDS) { UE_LOG(LogImageWrapper, Error, TEXT("RLE compression of BMP images not supported")); return false; } if (bmhdr->biPlanes==1 && (bmhdr->biBitCount==8 || bmhdr->biBitCount==24 || bmhdr->biBitCount==32)) { // Set texture properties. Width = bmhdr->biWidth; Height = FMath::Abs(bmhdr->biHeight); Format = ERGBFormat::BGRA; // BmpImageWrapper was reporting BitDepth *total* not per-channel (eg. 24) //BitDepth = bmhdr->biBitCount; BitDepth = 8; return true; } if (bmhdr->biPlanes == 1 && bmhdr->biBitCount == 16) { UE_LOG(LogImageWrapper, Error, TEXT("BMP 16 bit format no longer supported. Use terrain tools for importing/exporting heightmaps.")); } else { UE_LOG(LogImageWrapper, Error, TEXT("BMP uses an unsupported format (%i/%i)"), bmhdr->biPlanes, bmhdr->biBitCount); } return false; } bool FBmpImageWrapper::CanSetRawFormat(const ERGBFormat InFormat, const int32 InBitDepth) const { return ((InFormat == ERGBFormat::BGRA || InFormat == ERGBFormat::Gray) && InBitDepth == 8); } ERawImageFormat::Type FBmpImageWrapper::GetSupportedRawFormat(const ERawImageFormat::Type InFormat) const { switch(InFormat) { case ERawImageFormat::G8: case ERawImageFormat::BGRA8: return InFormat; // directly supported case ERawImageFormat::G16: return ERawImageFormat::G8; // needs conversion case ERawImageFormat::BGRE8: case ERawImageFormat::RGBA16: case ERawImageFormat::RGBA16F: case ERawImageFormat::RGBA32F: case ERawImageFormat::R16F: case ERawImageFormat::R32F: // @@!! can these go to G8 ? return ERawImageFormat::BGRA8; // needs conversion default: check(0); return ERawImageFormat::BGRA8; }; } void FBmpImageWrapper::Compress(int32 Quality) { check( Format == ERGBFormat::BGRA || Format == ERGBFormat::Gray ); check( BitDepth == 8 ); // write 8,24, or 32 bit bmp int64 NumPixels = Width*Height; int64 RawDataSize = RawData.Num(); int RawBytesPerPel = (Format == ERGBFormat::BGRA) ? 4 : 1; check( RawDataSize == NumPixels*RawBytesPerPel ); int OutputBytesPerPel = RawBytesPerPel; if ( RawBytesPerPel == 4 ) { // scan for A to choose 24 bit output const FColor * RawColors = (const FColor *)RawData.GetData(); bool bHasAnyAlpha = false; for(int64 i=0;i=0;y--) { memcpy(PayloadPtr,RawPtr + y * Width,Width); PayloadPtr += Width; memset(PayloadPtr,0,OutputRowPadBytes); PayloadPtr += OutputRowPadBytes; } break; } case 3: { const FColor * RawColors = (const FColor *) RawData.GetData(); for(int y=Height-1;y>=0;y--) { const FColor * RawRow = RawColors + y * Width; for(int x=0;x=0;y--) { memcpy(PayloadPtr,RawPtr + y * OutputRowBytes,OutputRowBytes); PayloadPtr += OutputRowBytes; } break; } default: check(0); break; } check( PayloadPtr == CompressedData.GetData() + CompressedData.Num() ); }