/* * Copyright 2005, Axel Dörfler, axeld@pinc-software.de. All rights reserved. * Distributed under the terms of the MIT License. */ // ToDo: This definitely needs to be worked over for endian issues #include "ICO.h" #include "ICOTranslator.h" #include #include #include #include //#define TRACE_ICO #ifdef TRACE_ICO # define TRACE(x) printf x #else # define TRACE(x) ; #endif using namespace ICO; static const rgba32_color kMagicTransparentColor = *(rgba32_color *)&B_TRANSPARENT_MAGIC_RGBA32; class TempAllocator { public: TempAllocator() : fMemory(NULL) {} ~TempAllocator() { free(fMemory); } void *Allocate(size_t size) { return fMemory = malloc(size); } private: void *fMemory; }; bool ico_header::IsValid() const { return reserved == 0 && (type == kTypeIcon || type == kTypeCursor) && entry_count < 32; } void ico_header::SwapToHost() { swap_data(B_UINT16_TYPE, this, sizeof(ico_header), B_SWAP_LENDIAN_TO_HOST); } void ico_header::SwapFromHost() { swap_data(B_UINT16_TYPE, this, sizeof(ico_header), B_SWAP_HOST_TO_LENDIAN); } // #pragma mark - void ico_dir_entry::SwapToHost() { swap_data(B_UINT16_TYPE, &planes, sizeof(uint16) * 2, B_SWAP_LENDIAN_TO_HOST); swap_data(B_UINT32_TYPE, &size, sizeof(uint32) * 2, B_SWAP_LENDIAN_TO_HOST); } void ico_dir_entry::SwapFromHost() { swap_data(B_UINT16_TYPE, &planes, sizeof(uint16) * 2, B_SWAP_HOST_TO_LENDIAN); swap_data(B_UINT32_TYPE, &size, sizeof(uint32) * 2, B_SWAP_HOST_TO_LENDIAN); } // #pragma mark - bool ico_bitmap_header::IsValid() const { return size == sizeof(ico_bitmap_header) && compression == 0 && (bits_per_pixel == 1 || bits_per_pixel == 4 || bits_per_pixel == 8 || bits_per_pixel == 16 || bits_per_pixel == 24 || bits_per_pixel == 32); } void ico_bitmap_header::SwapToHost() { swap_data(B_UINT32_TYPE, &size, sizeof(uint32) * 3, B_SWAP_LENDIAN_TO_HOST); swap_data(B_UINT16_TYPE, &planes, sizeof(uint16) * 2, B_SWAP_LENDIAN_TO_HOST); swap_data(B_UINT32_TYPE, &compression, sizeof(uint32) * 6, B_SWAP_LENDIAN_TO_HOST); } void ico_bitmap_header::SwapFromHost() { swap_data(B_UINT32_TYPE, &size, sizeof(uint32) * 3, B_SWAP_HOST_TO_LENDIAN); swap_data(B_UINT16_TYPE, &planes, sizeof(uint16) * 2, B_SWAP_HOST_TO_LENDIAN); swap_data(B_UINT32_TYPE, &compression, sizeof(uint32) * 6, B_SWAP_HOST_TO_LENDIAN); } // #pragma mark - static inline uint8 * get_data_row(uint8 *data, int32 dataSize, int32 rowBytes, int32 row) { return data + dataSize - (row + 1) * rowBytes; } static inline int32 get_bytes_per_row(int32 width, int32 bitsPerPixel) { return (((bitsPerPixel * width + 7) / 8) + 3) & ~3; } static inline void set_1_bit_per_pixel(uint8 *line, int32 x, int32 value) { int32 mask = 1 << (7 - (x & 7)); if (value) line[x / 8] |= mask; else line[x / 8] &= ~mask; } static inline void set_4_bits_per_pixel(uint8 *line, int32 x, int32 value) { int32 shift = x & 1 ? 0 : 4; int32 mask = ~0L & (0xf0 >> shift); line[x / 2] &= mask; line[x / 2] |= value << shift; } static inline int32 get_1_bit_per_pixel(uint8 *line, int32 x) { return (line[x / 8] >> (7 - (x & 7))) & 1; } static inline int32 get_4_bits_per_pixel(uint8 *line, int32 x) { return (line[x / 2] >> (4 * ((x + 1) & 1))) & 0xf; } static uint8 get_alpha_value(color_space space, uint32 value) { // ToDo: support more color spaces if (space == B_RGBA32) return value >> 24; return 0; } static uint16 rgba32_color_to_16_bit_color(rgba32_color &color) { return ((color.blue >> 3) << 11) | ((color.green >> 2) << 5) | (color.red >> 3); } static int32 find_rgba32_color(rgba32_color *palette, int32 numColors, rgba32_color &color) { // ToDo: sorting and binary search? for (int32 i = 0; i < numColors; i++) { if (palette[i] == color) return i; } return -1; } static inline rgba32_color get_rgba32_color_from_bits(TranslatorBitmap &bitsHeader, uint8 *data, int32 x, int32 y) { data += bitsHeader.rowBytes * y; switch (bitsHeader.colors) { case B_RGBA32: return *(rgba32_color *)(data + 4 * x); case B_RGB32: default: // stupid applications like ArtPaint use the alpha channel in B_RGB32 images... rgba32_color color = *(rgba32_color *)(data + 4 * x); if (color.alpha >= 128) color.alpha = 255; else color.alpha = 0; return color; // ToDo: support some more color spaces... } } static int32 fill_palette(TranslatorBitmap &bitsHeader, uint8 *data, rgba32_color *palette) { int32 numColors = 0; for (int32 y = 0; y < bitsHeader.bounds.IntegerHeight() + 1; y++) { for (int32 x = 0; x < bitsHeader.bounds.IntegerWidth() + 1; x++) { rgba32_color color = get_rgba32_color_from_bits(bitsHeader, data, x, y); int32 index = find_rgba32_color(palette, numColors, color); if (index == -1) { // add this color if there is space left if (numColors == 256) return -1; color.alpha = 0; // the alpha channel is actually unused palette[numColors++] = color; } } } return numColors; } /** This function is used to determine, if a true alpha channel has to * be used in order to preserve all information. */ static bool has_true_alpha_channel(color_space space, uint8 *data, int32 width, int32 height, int32 bytesPerRow) { for (int32 y = 0; y < height; y++) { for (int32 x = 0; x < width; x++) { uint8 value = get_alpha_value(space, ((uint32 *)data)[x]); if (value != 0 && value != 255) return true; } data += bytesPerRow; } return false; } static status_t convert_data_to_bits(ico_dir_entry &entry, ico_bitmap_header &header, const rgba32_color *palette, BPositionIO &source, BPositionIO &target) { uint16 bitsPerPixel = header.bits_per_pixel; // round row bytes to next 4 byte boundary int32 xorRowBytes = get_bytes_per_row(entry.width, header.bits_per_pixel); int32 andRowBytes = 0; if (bitsPerPixel != 32) andRowBytes = get_bytes_per_row(entry.width, 1); int32 outRowBytes = entry.width * 4; // allocate buffers TempAllocator xorAllocator, andAllocator, rowAllocator; int32 xorDataSize = xorRowBytes * entry.height; uint8 *xorData = (uint8 *)xorAllocator.Allocate(xorDataSize); if (xorData == NULL) return B_NO_MEMORY; int32 andDataSize = andRowBytes * entry.height; uint8 *andData = NULL; if (bitsPerPixel != 32) { andData = (uint8 *)andAllocator.Allocate(andDataSize); if (andData == NULL) return B_NO_MEMORY; } rgba32_color *outRowData = (rgba32_color *)rowAllocator.Allocate(outRowBytes); if (outRowData == NULL) return B_NO_MEMORY; ssize_t bytesRead = source.Read(xorData, xorDataSize); if (bytesRead != xorDataSize) return B_BAD_DATA; if (bitsPerPixel != 32) { bytesRead = source.Read(andData, andDataSize); if (bytesRead != andDataSize) { // reading the alpha channel failed, so we're ignoring it // (but we're still able to show the image data) andData = NULL; } } for (uint32 row = 0; row < entry.height; row++) { for (uint32 x = 0; x < entry.width; x++) { uint8 *line = get_data_row(xorData, xorDataSize, xorRowBytes, row); if (palette != NULL) { uint8 index; switch (bitsPerPixel) { case 1: index = get_1_bit_per_pixel(line, x); break; case 4: index = get_4_bits_per_pixel(line, x); break; case 8: default: index = line[x]; break; } outRowData[x] = palette[index]; } else { switch (bitsPerPixel) { case 16: { uint16 color = ((uint16 *)line)[x]; outRowData[x].blue = (color >> 11) << 3; outRowData[x].green = ((color >> 5) & 0x3f) << 3; outRowData[x].red = (color & 0x1f) << 3; break; } case 24: outRowData[x].blue = line[x * 3 + 0]; outRowData[x].green = line[x * 3 + 1]; outRowData[x].red = line[x * 3 + 2]; break; case 32: outRowData[x] = ((rgba32_color *)line)[x]; break; } } if (bitsPerPixel != 32) { // set alpha channel if (andData != NULL && get_1_bit_per_pixel(get_data_row(andData, andDataSize, andRowBytes, row), x)) outRowData[x] = kMagicTransparentColor; else outRowData[x].alpha = 255; } else if (outRowData[x].alpha == 0) outRowData[x] = kMagicTransparentColor; } ssize_t bytesWritten = target.Write(outRowData, outRowBytes); if (bytesWritten < B_OK) return bytesWritten; if (bytesWritten != outRowBytes) return B_IO_ERROR; } return B_OK; } static status_t convert_bits_to_data(TranslatorBitmap &bitsHeader, uint8 *bitsData, ico_dir_entry &entry, ico_bitmap_header &header, rgba32_color *palette, BPositionIO &target) { int32 bitsPerPixel = header.bits_per_pixel; // round row bytes to next 4 byte boundary int32 xorRowBytes = get_bytes_per_row(entry.width, bitsPerPixel); int32 andRowBytes = get_bytes_per_row(entry.width, 1); TempAllocator xorAllocator, andAllocator; uint8 *xorRowData = (uint8 *)xorAllocator.Allocate(xorRowBytes); if (xorRowData == NULL) return B_NO_MEMORY; uint8 *andRowData = (uint8 *)andAllocator.Allocate(andRowBytes); if (andRowData == NULL) return B_NO_MEMORY; int32 numColors = 1 << bitsPerPixel; // write XOR data (the actual image data) // (ICO data is upside down, so we're starting at the last line) for (uint32 row = entry.height; row-- > 0;) { for (uint32 x = 0; x < entry.width; x++) { rgba32_color color = get_rgba32_color_from_bits(bitsHeader, bitsData, x, row); if (palette != NULL) { uint8 index = find_rgba32_color(palette, numColors, color); switch (bitsPerPixel) { case 1: set_1_bit_per_pixel(xorRowData, x, index); break; case 4: set_4_bits_per_pixel(xorRowData, x, index); break; case 8: default: xorRowData[x] = index; break; } } else { switch (bitsPerPixel) { default: case 16: { uint16 *data = (uint16 *)xorRowData; data[x] = rgba32_color_to_16_bit_color(color); break; } case 24: { xorRowData[x * 3 + 0] = color.blue; xorRowData[x * 3 + 1] = color.green; xorRowData[x * 3 + 2] = color.red; break; } case 32: { rgba32_color *data = (rgba32_color *)xorRowData; data[x] = color; break; } } } } ssize_t bytesWritten = target.Write(xorRowData, xorRowBytes); if (bytesWritten < B_OK) return bytesWritten; if (bytesWritten != xorRowBytes) return B_IO_ERROR; } if (bitsPerPixel == 32) { // the alpha channel has already been written with the image data return B_OK; } // write AND data (the transparency bit) for (uint32 row = entry.height; row-- > 0;) { for (uint32 x = 0; x < entry.width; x++) { rgba32_color color = get_rgba32_color_from_bits(bitsHeader, bitsData, x, row); bool transparent = *(uint32 *)&color == B_TRANSPARENT_MAGIC_RGBA32 || color.alpha == 0; set_1_bit_per_pixel(andRowData, x, transparent ? 1 : 0); } ssize_t bytesWritten = target.Write(andRowData, andRowBytes); if (bytesWritten < B_OK) return bytesWritten; if (bytesWritten != andRowBytes) return B_IO_ERROR; } return B_OK; } // #pragma mark - bool ICO::is_valid_size(int32 size) { return size == 16 || size == 32 || size == 48; } status_t ICO::identify(BMessage *settings, BPositionIO &stream, uint8 &type, int32 &bitsPerPixel) { // read in the header ico_header header; if (stream.Read(&header, sizeof(ico_header)) != (ssize_t)sizeof(ico_header)) return B_BAD_VALUE; header.SwapToHost(); // check header if (!header.IsValid()) return B_BAD_VALUE; int32 iconIndex = 0; type = header.type; if (settings) { // Add page count to ioExtension settings->RemoveName(kDocumentCount); settings->AddInt32(kDocumentCount, header.entry_count); // Check if a document index has been specified if (settings->FindInt32(kDocumentIndex, &iconIndex) == B_OK) iconIndex--; else iconIndex = 0; if (iconIndex < 0 || iconIndex >= header.entry_count) return B_NO_TRANSLATOR; } TRACE(("iconIndex = %ld, count = %ld\n", iconIndex, header.entry_count)); // read in directory entries for (uint32 i = 0; i < header.entry_count; i++) { ico_dir_entry entry; if (stream.Read(&entry, sizeof(ico_dir_entry)) != (ssize_t)sizeof(ico_dir_entry)) return B_BAD_VALUE; entry.SwapToHost(); TRACE(("width: %d, height: %d, planes: %d, color_count: %d, bits_per_pixel: %d, size: %ld, offset: %ld\n", entry.width, entry.height, entry.planes, entry.color_count, entry.bits_per_pixel, entry.size, entry.offset)); ico_bitmap_header bitmapHeader; if (stream.ReadAt(entry.offset, &bitmapHeader, sizeof(ico_bitmap_header)) != (ssize_t)sizeof(ico_bitmap_header)) return B_BAD_VALUE; bitmapHeader.SwapToHost(); TRACE(("size: %ld, width: %ld, height: %ld, bits_per_pixel: %d, x/y per meter: %ld:%ld, compression: %ld, image_size: %ld, colors used: %ld, important colors: %ld\n", bitmapHeader.size, bitmapHeader.width, bitmapHeader.height, bitmapHeader.bits_per_pixel, bitmapHeader.x_pixels_per_meter, bitmapHeader.y_pixels_per_meter, bitmapHeader.compression, bitmapHeader.image_size, bitmapHeader.colors_used, bitmapHeader.important_colors)); if (!bitmapHeader.IsValid()) return B_BAD_VALUE; if ((uint32)iconIndex == i) bitsPerPixel = bitmapHeader.bits_per_pixel; } return B_OK; } /** Converts an ICO image of any type into a B_RGBA32 B_TRANSLATOR_BITMAP. */ status_t ICO::convert_ico_to_bits(BMessage *settings, BPositionIO &source, BPositionIO &target) { ico_header header; if (source.Read(&header, sizeof(ico_header)) != (ssize_t)sizeof(ico_header)) return B_BAD_VALUE; header.SwapToHost(); // check header if (!header.IsValid()) return B_BAD_VALUE; int32 iconIndex = 0; if (settings) { // Check if a document index has been specified if (settings->FindInt32(kDocumentIndex, &iconIndex) == B_OK) iconIndex--; else iconIndex = 0; if (iconIndex < 0 || iconIndex >= header.entry_count) return B_BAD_VALUE; } // read in selected entry ico_dir_entry entry; if (source.ReadAt(sizeof(ico_header) + sizeof(ico_dir_entry) * iconIndex, &entry, sizeof(ico_dir_entry)) != (ssize_t)sizeof(ico_dir_entry)) return B_BAD_VALUE; entry.SwapToHost(); source.Seek(entry.offset, SEEK_SET); ico_bitmap_header bitmapHeader; if (source.Read(&bitmapHeader, sizeof(ico_bitmap_header)) != (ssize_t)sizeof(ico_bitmap_header)) return B_BAD_VALUE; bitmapHeader.SwapToHost(); if (!bitmapHeader.IsValid()) return B_BAD_VALUE; if (bitmapHeader.compression != 0) return EOPNOTSUPP; int32 numColors = 0; if (bitmapHeader.bits_per_pixel <= 8) numColors = 1L << bitmapHeader.bits_per_pixel; // This is a work-around for a broken ICO file writer that publishes // a wrong image height in the ico_dir_entry structure if (entry.size != 0 && 2 * entry.width == entry.height && numColors != 0 && sizeof(rgba32_color) * numColors + entry.width * entry.height > entry.size) entry.height = entry.width; TranslatorBitmap bitsHeader; bitsHeader.magic = B_TRANSLATOR_BITMAP; bitsHeader.bounds.left = 0; bitsHeader.bounds.top = 0; bitsHeader.bounds.right = entry.width - 1; bitsHeader.bounds.bottom = entry.height - 1; bitsHeader.bounds.Set(0, 0, entry.width - 1, entry.height - 1); bitsHeader.rowBytes = entry.width * 4; bitsHeader.colors = B_RGBA32; bitsHeader.dataSize = bitsHeader.rowBytes * entry.height; // read in palette rgba32_color palette[256]; if (numColors > 0) { if (source.Read(palette, numColors * 4) != numColors * 4) return B_BAD_VALUE; // clear alpha channel (it's not used in ICO color information) for (int32 i = 0; i < numColors; i++) palette[i].alpha = 0; } // write out Be's Bitmap header swap_data(B_UINT32_TYPE, &bitsHeader, sizeof(TranslatorBitmap), B_SWAP_HOST_TO_BENDIAN); target.Write(&bitsHeader, sizeof(TranslatorBitmap)); return convert_data_to_bits(entry, bitmapHeader, numColors > 0 ? palette : NULL, source, target); } status_t ICO::convert_bits_to_ico(BMessage *settings, BPositionIO &source, TranslatorBitmap &bitsHeader, BPositionIO &target) { int32 width = bitsHeader.bounds.IntegerWidth() + 1; int32 height = bitsHeader.bounds.IntegerHeight() + 1; if (!is_valid_size(width) || !is_valid_size(height)) return B_BAD_VALUE; int32 bitsPerPixel; switch (bitsHeader.colors) { case B_RGBA32: bitsPerPixel = 32; break; case B_RGB32: bitsPerPixel = 24; break; case B_RGB16: bitsPerPixel = 16; break; case B_CMAP8: case B_GRAY8: bitsPerPixel = 8; break; case B_GRAY1: bitsPerPixel = 1; break; default: fprintf(stderr, "unsupported color space.\n"); return B_BAD_VALUE; } TempAllocator dataAllocator; uint8 *bitsData = (uint8 *)dataAllocator.Allocate(bitsHeader.rowBytes * height); if (bitsData == NULL) return B_NO_MEMORY; ssize_t bytesRead = source.Read(bitsData, bitsHeader.rowBytes * height); if (bytesRead < B_OK) return bytesRead; rgba32_color palette[256]; if (bitsPerPixel > 8) { // it's a non-palette mode - but does it have to be? if (bitsHeader.colors != B_RGBA32 || !has_true_alpha_channel(bitsHeader.colors, bitsData, width, height, bitsHeader.rowBytes)) { memset(palette, 0, sizeof(palette)); // count colors int32 colors = fill_palette(bitsHeader, bitsData, palette); if (colors != -1) { // we fit into a palette mode if (colors > 16) bitsPerPixel = 8; else if (colors > 2) bitsPerPixel = 4; else bitsPerPixel = 1; } } } int32 numColors = 1 << bitsPerPixel; ico_header header; header.type = B_HOST_TO_LENDIAN_INT16(1); header.entry_count = B_HOST_TO_LENDIAN_INT16(1); header.reserved = 0; ssize_t bytesWritten = target.Write(&header, sizeof(ico_header)); if (bytesWritten < B_OK) return bytesWritten; ico_dir_entry entry; entry.width = width; entry.height = height; entry.planes = 1; entry.bits_per_pixel = bitsPerPixel; entry.color_count = 0; if (bitsPerPixel <= 8) entry.color_count = numColors; // When bits_per_pixel == 32, the data already contains the alpha channel int32 xorRowBytes = get_bytes_per_row(width, bitsPerPixel); int32 andRowBytes = 0; if (bitsPerPixel != 32) andRowBytes = get_bytes_per_row(width, 1); entry.size = sizeof(ico_bitmap_header) + width * (xorRowBytes + andRowBytes); if (bitsPerPixel <= 8) entry.size += numColors * sizeof(rgba32_color); entry.offset = sizeof(ico_header) + sizeof(ico_dir_entry); entry.reserved = 0; ico_bitmap_header bitmapHeader; memset(&bitmapHeader, 0, sizeof(ico_bitmap_header)); bitmapHeader.size = sizeof(ico_bitmap_header); bitmapHeader.width = width; bitmapHeader.height = height + (bitsPerPixel == 32 ? 0 : height); bitmapHeader.bits_per_pixel = bitsPerPixel; bitmapHeader.planes = 1; bitmapHeader.image_size = 0; if (bitsPerPixel <= 8) bitmapHeader.colors_used = numColors; entry.SwapFromHost(); bitmapHeader.SwapFromHost(); bytesWritten = target.Write(&entry, sizeof(ico_dir_entry)); if (bytesWritten < B_OK) return bytesWritten; bytesWritten = target.Write(&bitmapHeader, sizeof(ico_bitmap_header)); if (bytesWritten < B_OK) return bytesWritten; // we'll need them in convert_bits_to_data() entry.SwapToHost(); bitmapHeader.SwapToHost(); if (bitsPerPixel <= 8) { bytesWritten = target.Write(palette, numColors * sizeof(rgba32_color)); if (bytesWritten < B_OK) return bytesWritten; } return convert_bits_to_data(bitsHeader, bitsData, entry, bitmapHeader, bitsPerPixel <= 8 ? palette : NULL, target); }