ezResult ezBmpFileFormat::WriteImage(ezStreamWriterBase& stream, const ezImage& image, ezLogInterface* pLog) const { // Technically almost arbitrary formats are supported, but we only use the common ones. ezImageFormat::Enum compatibleFormats[] = { ezImageFormat::B8G8R8X8_UNORM, ezImageFormat::B8G8R8A8_UNORM, ezImageFormat::B8G8R8_UNORM, ezImageFormat::B5G5R5X1_UNORM, ezImageFormat::B5G6R5_UNORM, }; // Find a compatible format closest to the one the image currently has ezImageFormat::Enum format = ezImageConversionBase::FindClosestCompatibleFormat(image.GetImageFormat(), compatibleFormats); if (format == ezImageFormat::UNKNOWN) { ezLog::Error(pLog, "No conversion from format '%s' to a format suitable for BMP files known.", ezImageFormat::GetName(image.GetImageFormat())); return EZ_FAILURE; } // Convert if not already in a compatible format if (format != image.GetImageFormat()) { ezImage convertedImage; if (ezImageConversionBase::Convert(image, convertedImage, format) != EZ_SUCCESS) { // This should never happen EZ_ASSERT_DEV(false, "ezImageConversion::Convert failed even though the conversion was to the format returned by FindClosestCompatibleFormat."); return EZ_FAILURE; } return WriteImage(stream, convertedImage, pLog); } ezUInt32 uiRowPitch = image.GetRowPitch(0); ezUInt32 uiHeight = image.GetHeight(0); int dataSize = uiRowPitch * uiHeight; ezBmpFileInfoHeader fileInfoHeader; fileInfoHeader.m_width = image.GetWidth(0); fileInfoHeader.m_height = uiHeight; fileInfoHeader.m_planes = 1; fileInfoHeader.m_bitCount = ezImageFormat::GetBitsPerPixel(format); fileInfoHeader.m_sizeImage = 0; // Can be zero unless we store the data compressed fileInfoHeader.m_xPelsPerMeter = 0; fileInfoHeader.m_yPelsPerMeter = 0; fileInfoHeader.m_clrUsed = 0; fileInfoHeader.m_clrImportant = 0; bool bWriteColorMask = false; // Prefer to write a V3 header ezUInt32 uiHeaderVersion = 3; switch (format) { case ezImageFormat::B8G8R8X8_UNORM: case ezImageFormat::B5G5R5X1_UNORM: case ezImageFormat::B8G8R8_UNORM: fileInfoHeader.m_compression = RGB; break; case ezImageFormat::B8G8R8A8_UNORM: fileInfoHeader.m_compression = BITFIELDS; uiHeaderVersion = 4; break; case ezImageFormat::B5G6R5_UNORM: fileInfoHeader.m_compression = BITFIELDS; bWriteColorMask = true; break; default: return EZ_FAILURE; } EZ_ASSERT_DEV(!bWriteColorMask || uiHeaderVersion <= 3, "Internal bug"); ezUInt32 uiFileInfoHeaderSize = sizeof(ezBmpFileInfoHeader); ezUInt32 uiHeaderSize = sizeof(ezBmpFileHeader); if (uiHeaderVersion >= 4) { uiFileInfoHeaderSize += sizeof(ezBmpFileInfoHeaderV4); } else if (bWriteColorMask) { uiHeaderSize += 3 * sizeof(ezUInt32); } uiHeaderSize += uiFileInfoHeaderSize; fileInfoHeader.m_size = uiFileInfoHeaderSize; ezBmpFileHeader header; header.m_type = ezBmpFileMagic; header.m_size = uiHeaderSize + dataSize; header.m_reserved1 = 0; header.m_reserved2 = 0; header.m_offBits = uiHeaderSize; const void* dataPtr = image.GetDataPointer<void>(); // Write all data if (stream.WriteBytes(&header, sizeof(header)) != EZ_SUCCESS) { ezLog::Error(pLog, "Failed to write header."); return EZ_FAILURE; } if (stream.WriteBytes(&fileInfoHeader, sizeof(fileInfoHeader)) != EZ_SUCCESS) { ezLog::Error(pLog, "Failed to write fileInfoHeader."); return EZ_FAILURE; } if (uiHeaderVersion >= 4) { ezBmpFileInfoHeaderV4 fileInfoHeaderV4; memset(&fileInfoHeaderV4, 0, sizeof(fileInfoHeaderV4)); fileInfoHeaderV4.m_redMask = ezImageFormat::GetRedMask(format); fileInfoHeaderV4.m_greenMask = ezImageFormat::GetGreenMask(format); fileInfoHeaderV4.m_blueMask = ezImageFormat::GetBlueMask(format); fileInfoHeaderV4.m_alphaMask = ezImageFormat::GetAlphaMask(format); if (stream.WriteBytes(&fileInfoHeaderV4, sizeof(fileInfoHeaderV4)) != EZ_SUCCESS) { ezLog::Error(pLog, "Failed to write fileInfoHeaderV4."); return EZ_FAILURE; } } else if (bWriteColorMask) { struct { ezUInt32 m_red; ezUInt32 m_green; ezUInt32 m_blue; } colorMask; colorMask.m_red = ezImageFormat::GetRedMask(format); colorMask.m_green = ezImageFormat::GetGreenMask(format); colorMask.m_blue = ezImageFormat::GetBlueMask(format); if (stream.WriteBytes(&colorMask, sizeof(colorMask)) != EZ_SUCCESS) { ezLog::Error(pLog, "Failed to write colorMask."); return EZ_FAILURE; } } const ezUInt32 uiPaddedRowPitch = ((uiRowPitch - 1) / 4 + 1) * 4; // Write rows in reverse order for (ezInt32 iRow = uiHeight - 1; iRow >= 0; iRow--) { if (stream.WriteBytes(image.GetPixelPointer<void>(0, 0, 0, 0, iRow, 0), uiPaddedRowPitch) != EZ_SUCCESS) { ezLog::Error(pLog, "Failed to write data."); return EZ_FAILURE; } } return EZ_SUCCESS; }
ezResult ezBmpFileFormat::ReadImage(ezStreamReaderBase& stream, ezImage& image, ezLogInterface* pLog) const { ezBmpFileHeader fileHeader; if (stream.ReadBytes(&fileHeader, sizeof(ezBmpFileHeader)) != sizeof(ezBmpFileHeader)) { ezLog::Error(pLog, "Failed to read header data."); return EZ_FAILURE; } // Some very old BMP variants may have different magic numbers, but we don't support them. if (fileHeader.m_type != ezBmpFileMagic) { ezLog::Error(pLog, "The file is not a recognized BMP file."); return EZ_FAILURE; } // We expect at least header version 3 ezUInt32 uiHeaderVersion = 3; ezBmpFileInfoHeader fileInfoHeader; if (stream.ReadBytes(&fileInfoHeader, sizeof(ezBmpFileInfoHeader)) != sizeof(ezBmpFileInfoHeader)) { ezLog::Error(pLog, "Failed to read header data (V3)."); return EZ_FAILURE; } int remainingHeaderBytes = fileInfoHeader.m_size - sizeof(fileInfoHeader); // File header shorter than expected - happens with corrupt files or e.g. with OS/2 BMP files which may have shorter headers if (remainingHeaderBytes < 0) { ezLog::Error(pLog, "The file header was shorter than expected."); return EZ_FAILURE; } // Newer files may have a header version 4 (required for transparency) ezBmpFileInfoHeaderV4 fileInfoHeaderV4; if (remainingHeaderBytes >= sizeof(ezBmpFileInfoHeaderV4)) { uiHeaderVersion = 4; if (stream.ReadBytes(&fileInfoHeaderV4, sizeof(ezBmpFileInfoHeaderV4)) != sizeof(ezBmpFileInfoHeaderV4)) { ezLog::Error(pLog, "Failed to read header data (V4)."); return EZ_FAILURE; } remainingHeaderBytes -= sizeof(ezBmpFileInfoHeaderV4); } // Skip rest of header if (stream.SkipBytes(remainingHeaderBytes) != remainingHeaderBytes) { ezLog::Error(pLog, "Failed to skip remaining header data."); return EZ_FAILURE; } ezUInt32 uiBpp = fileInfoHeader.m_bitCount; // Find target format to load the image ezImageFormat::Enum format = ezImageFormat::UNKNOWN; bool bIndexed = false; bool bCompressed = false; switch (fileInfoHeader.m_compression) { // RGB or indexed data case RGB: switch (uiBpp) { case 1: case 4: case 8: bIndexed = true; // We always decompress indexed to BGRX, since the palette is specified in this format format = ezImageFormat::B8G8R8X8_UNORM; break; case 16: format = ezImageFormat::B5G5R5X1_UNORM; break; case 24: format = ezImageFormat::B8G8R8_UNORM; break; case 32: format = ezImageFormat::B8G8R8X8_UNORM; } break; // RGB data, but with the color masks specified in place of the palette case BITFIELDS: switch (uiBpp) { case 16: case 32: // In case of old headers, the color masks appear after the header (and aren't counted as part of it) if (uiHeaderVersion < 4) { // Color masks (w/o alpha channel) struct { ezUInt32 m_red; ezUInt32 m_green; ezUInt32 m_blue; } colorMask; if (stream.ReadBytes(&colorMask, sizeof(colorMask)) != sizeof(colorMask)) { return EZ_FAILURE; } format = ezImageFormat::FromPixelMask(colorMask.m_red, colorMask.m_green, colorMask.m_blue, 0); } else { // For header version four and higher, the color masks are part of the header format = ezImageFormat::FromPixelMask( fileInfoHeaderV4.m_redMask, fileInfoHeaderV4.m_greenMask, fileInfoHeaderV4.m_blueMask, fileInfoHeaderV4.m_alphaMask); } break; } break; case RLE4: if (uiBpp == 4) { bIndexed = true; bCompressed = true; format = ezImageFormat::B8G8R8X8_UNORM; } break; case RLE8: if (uiBpp == 8) { bIndexed = true; bCompressed = true; format = ezImageFormat::B8G8R8X8_UNORM; } break; } if (format == ezImageFormat::UNKNOWN) { ezLog::Error(pLog, "Unknown or unsupported BMP encoding."); return EZ_FAILURE; } const ezUInt32 uiWidth = fileInfoHeader.m_width; if (uiWidth > 65536) { ezLog::Error(pLog, "Image specifies width > 65536. Header corrupted?"); return EZ_FAILURE; } const ezUInt32 uiHeight = fileInfoHeader.m_height; if (uiHeight > 65536) { ezLog::Error(pLog, "Image specifies height > 65536. Header corrupted?"); return EZ_FAILURE; } ezUInt32 uiDataSize = fileInfoHeader.m_sizeImage; if (uiDataSize > 1024 * 1024 * 1024) { ezLog::Error(pLog, "Image specifies data size > 1GiB. Header corrupted?"); return EZ_FAILURE; } int uiRowPitchIn = (uiWidth * uiBpp + 31) / 32 * 4; if (uiDataSize == 0) { if (fileInfoHeader.m_compression != RGB) { ezLog::Error(pLog, "The data size wasn't specified in the header."); return EZ_FAILURE; } uiDataSize = uiRowPitchIn * uiHeight; } // Set image data image.SetImageFormat(format); image.SetNumMipLevels(1); image.SetNumArrayIndices(1); image.SetNumFaces(1); image.SetWidth(uiWidth); image.SetHeight(uiHeight); image.SetDepth(1); image.AllocateImageData(); ezUInt32 uiRowPitch = image.GetRowPitch(0); if (bIndexed) { // If no palette size was specified, the full available palette size will be used ezUInt32 paletteSize = fileInfoHeader.m_clrUsed; if (paletteSize == 0) { paletteSize = 1U << uiBpp; } else if(paletteSize > 65536) { ezLog::Error(pLog, "Palette size > 65536."); return EZ_FAILURE; } ezDynamicArray<ezBmpBgrxQuad> palette; palette.SetCount(paletteSize); if (stream.ReadBytes(&palette[0], paletteSize * sizeof(ezBmpBgrxQuad)) != paletteSize * sizeof(ezBmpBgrxQuad)) { ezLog::Error(pLog, "Failed to read palette data."); return EZ_FAILURE; } if (bCompressed) { // Compressed data is always in pairs of bytes if (uiDataSize % 2 != 0) { ezLog::Error(pLog, "The data size is not a multiple of 2 bytes in an RLE-compressed file."); return EZ_FAILURE; } ezDynamicArray<ezUInt8> compressedData; compressedData.SetCount(uiDataSize); if (stream.ReadBytes(&compressedData[0], uiDataSize) != uiDataSize) { ezLog::Error(pLog, "Failed to read data."); return EZ_FAILURE; } const ezUInt8* pIn = &compressedData[0]; const ezUInt8* pInEnd = pIn + uiDataSize; // Current output position ezUInt32 uiRow = uiHeight - 1; ezUInt32 uiCol = 0; ezBmpBgrxQuad* pLine = image.GetPixelPointer<ezBmpBgrxQuad>(0, 0, 0, 0, uiRow, 0); // Decode RLE data directly to RGBX while(pIn < pInEnd) { ezUInt32 uiByte1 = *pIn++; ezUInt32 uiByte2 = *pIn++; // Relative mode - the first byte specified a number of indices to be repeated, the second one the indices if (uiByte1 > 0) { // Clamp number of repetitions to row width. // The spec isn't clear on this point, but some files pad the number of encoded indices for some reason. uiByte1 = ezMath::Min(uiByte1, uiWidth - uiCol); if (uiBpp == 4) { // Alternate between two indices. for (ezUInt32 uiRep = 0; uiRep < uiByte1 / 2; uiRep++) { pLine[uiCol++] = palette[uiByte2 >> 4]; pLine[uiCol++] = palette[uiByte2 & 0x0F]; } // Repeat the first index for odd numbers of repetitions. if (uiByte1 & 1) { pLine[uiCol++] = palette[uiByte2 >> 4]; } } else /* if (uiBpp == 8) */ { // Repeat a single index. for (ezUInt32 uiRep = 0; uiRep < uiByte1; uiRep++) { pLine[uiCol++] = palette[uiByte2]; } } } else { // Absolute mode - the first byte specifies a number of indices encoded separately, or is a special marker switch (uiByte2)
ezResult ezImageConversion::ConvertSingleStepDecompress(const ezImageView& source, ezImage& target, ezImageFormat::Enum sourceFormat, ezImageFormat::Enum targetFormat, const ezImageConversionStep* pStep) { for (ezUInt32 arrayIndex = 0; arrayIndex < source.GetNumArrayIndices(); arrayIndex++) { for (ezUInt32 face = 0; face < source.GetNumFaces(); face++) { for (ezUInt32 mipLevel = 0; mipLevel < source.GetNumMipLevels(); mipLevel++) { const ezUInt32 width = target.GetWidth(mipLevel); const ezUInt32 height = target.GetHeight(mipLevel); const ezUInt32 blockSizeX = ezImageFormat::GetBlockWidth(sourceFormat); const ezUInt32 blockSizeY = ezImageFormat::GetBlockHeight(sourceFormat); const ezUInt32 numBlocksX = source.GetNumBlocksX(mipLevel); const ezUInt32 numBlocksY = source.GetNumBlocksY(mipLevel); const ezUInt32 targetRowPitch = target.GetRowPitch(mipLevel); const ezUInt32 targetBytesPerPixel = ezImageFormat::GetBitsPerPixel(targetFormat) / 8; const ezUInt32 blockSizeInBytes = ezImageFormat::GetBitsPerBlock(sourceFormat) / 8; // Decompress into a temp memory block so we don't have to explicitly handle the case where the image is not a multiple of the block // size ezHybridArray<ezUInt8, 256> tempBuffer; tempBuffer.SetCount(numBlocksX * blockSizeX * blockSizeY * targetBytesPerPixel); for (ezUInt32 slice = 0; slice < source.GetDepth(mipLevel); slice++) { for (ezUInt32 blockY = 0; blockY < numBlocksY; blockY++) { ezImageView sourceRowView = source.GetRowView(mipLevel, face, arrayIndex, blockY, slice); if (static_cast<const ezImageConversionStepDecompressBlocks*>(pStep) ->DecompressBlocks(sourceRowView.GetArrayPtr<void>(), ezArrayPtr<void>(tempBuffer.GetData(), tempBuffer.GetCount()), numBlocksX, sourceFormat, targetFormat) .Failed()) { return EZ_FAILURE; } for (ezUInt32 blockX = 0; blockX < numBlocksX; blockX++) { ezUInt8* targetPointer = target.GetPixelPointer<ezUInt8>(mipLevel, face, arrayIndex, blockX * blockSizeX, blockY * blockSizeY, slice); // Copy into actual target, clamping to image dimensions ezUInt32 copyWidth = ezMath::Min(blockSizeX, width - blockX * blockSizeX); ezUInt32 copyHeight = ezMath::Min(blockSizeY, height - blockY * blockSizeY); for (ezUInt32 row = 0; row < copyHeight; row++) { memcpy(targetPointer, &tempBuffer[(blockX * blockSizeX + row) * blockSizeY * targetBytesPerPixel], copyWidth * targetBytesPerPixel); targetPointer += targetRowPitch; } } } } } } } return EZ_SUCCESS; }