Exemple #1
0
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;
}
Exemple #2
0
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;
}