/* -*- mode: C; c-file-style: "linux" -*- */
/*
* GdkPixbuf library - TGA image loader
* Copyright (C) 1999 Nicola Girardi <nikke@swlibero.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this library; if not, see <http://www.gnu.org/licenses/>.
*
*/
/*
* Some NOTES about the TGA loader (2001/06/07, nikke@swlibero.org)
*
* - The TGAFooter isn't present in all TGA files. In fact, there's an older
* format specification, still in use, which doesn't cover the TGAFooter.
* Actually, most TGA files I have are of the older type. Anyway I put the
* struct declaration here for completeness.
*
* - Error handling was designed to be very paranoid.
*/
#include "config.h"
#include <stdio.h>
#include <string.h>
#include "gdk-pixbuf-private.h"
#include "gdk-pixbuf-buffer-queue-private.h"
#undef DEBUG_TGA
#define TGA_INTERLEAVE_MASK 0xc0
#define TGA_INTERLEAVE_NONE 0x00
#define TGA_INTERLEAVE_2WAY 0x40
#define TGA_INTERLEAVE_4WAY 0x80
#define TGA_ORIGIN_MASK 0x30
#define TGA_ORIGIN_RIGHT 0x10
#define TGA_ORIGIN_UPPER 0x20
enum {
TGA_TYPE_NODATA = 0,
TGA_TYPE_PSEUDOCOLOR = 1,
TGA_TYPE_TRUECOLOR = 2,
TGA_TYPE_GRAYSCALE = 3,
TGA_TYPE_RLE_PSEUDOCOLOR = 9,
TGA_TYPE_RLE_TRUECOLOR = 10,
TGA_TYPE_RLE_GRAYSCALE = 11
};
#define LE16(p) ((p)[0] + ((p)[1] << 8))
typedef struct _TGAHeader TGAHeader;
typedef struct _TGAFooter TGAFooter;
typedef struct _TGAColor TGAColor;
typedef struct _TGAColormap TGAColormap;
typedef struct _TGAContext TGAContext;
struct _TGAHeader {
guint8 infolen;
guint8 has_cmap;
guint8 type;
guint8 cmap_start[2];
guint8 cmap_n_colors[2];
guint8 cmap_bpp;
guint8 x_origin[2];
guint8 y_origin[2];
guint8 width[2];
guint8 height[2];
guint8 bpp;
guint8 flags;
};
struct _TGAFooter {
guint32 extension_area_offset;
guint32 developer_directory_offset;
/* Standard TGA signature, "TRUEVISION-XFILE.\0". */
union {
gchar sig_full[18];
struct {
gchar sig_chunk[16];
gchar dot, null;
} sig_struct;
} sig;
};
struct _TGAColor {
guchar r, g, b, a;
};
struct _TGAColormap {
guint n_colors;
TGAColor colors[1];
};
typedef gboolean (* TGAProcessFunc) (TGAContext *ctx, GError **error);
struct _TGAContext {
TGAHeader *hdr;
TGAColormap *cmap;
guint cmap_size;
GdkPixbuf *pbuf;
int pbuf_x;
int pbuf_y;
int pbuf_y_notified;
GdkPixbufBufferQueue *input;
TGAProcessFunc process;
GdkPixbufModuleSizeFunc sfunc;
GdkPixbufModulePreparedFunc pfunc;
GdkPixbufModuleUpdatedFunc ufunc;
gpointer udata;
};
static TGAColormap *
colormap_new (guint n_colors)
{
TGAColormap *cmap;
g_assert (n_colors <= G_MAXUINT16);
cmap = g_try_malloc0 (sizeof (TGAColormap) + (MAX (n_colors, 1) - 1) * sizeof (TGAColor));
if (cmap == NULL)
return NULL;
cmap->n_colors = n_colors;
return cmap;
}
static const TGAColor *
colormap_get_color (TGAColormap *cmap,
guint id)
{
static const TGAColor transparent_black = { 0, 0, 0, 0 };
if (id >= cmap->n_colors)
return &transparent_black;
return &cmap->colors[id];
}
static void
colormap_set_color (TGAColormap *cmap,
guint id,
const TGAColor *color)
{
if (id >= cmap->n_colors)
return;
cmap->colors[id] = *color;
}
static void
colormap_free (TGAColormap *cmap)
{
g_free (cmap);
}
static gboolean
tga_skip_rest_of_image (TGAContext *ctx,
GError **err)
{
gdk_pixbuf_buffer_queue_flush (ctx->input, gdk_pixbuf_buffer_queue_get_size (ctx->input));
return TRUE;
}
static inline void
tga_write_pixel (TGAContext *ctx,
const TGAColor *color)
{
guint x = (ctx->hdr->flags & TGA_ORIGIN_RIGHT) ? ctx->pbuf->width - ctx->pbuf_x - 1 : ctx->pbuf_x;
guint y = (ctx->hdr->flags & TGA_ORIGIN_UPPER) ? ctx->pbuf_y : ctx->pbuf->height - ctx->pbuf_y - 1;
memcpy (ctx->pbuf->pixels + y * ctx->pbuf->rowstride + x * ctx->pbuf->n_channels, color, ctx->pbuf->n_channels);
ctx->pbuf_x++;
if (ctx->pbuf_x >= ctx->pbuf->width)
{
ctx->pbuf_x = 0;
ctx->pbuf_y++;
}
}
static gsize
tga_pixels_remaining (TGAContext *ctx)
{
return ctx->pbuf->width * (ctx->pbuf->height - ctx->pbuf_y) - ctx->pbuf_x;
}
static gboolean
tga_all_pixels_written (TGAContext *ctx)
{
return ctx->pbuf_y >= ctx->pbuf->height;
}
static void
tga_emit_update (TGAContext *ctx)
{
if (!ctx->ufunc)
return;
/* We only notify row-by-row for now.
* I was too lazy to handle line-breaks.
*/
if (ctx->pbuf_y_notified == ctx->pbuf_y)
return;
if (ctx->hdr->flags & TGA_ORIGIN_UPPER)
(*ctx->ufunc) (ctx->pbuf,
0, ctx->pbuf_y_notified,
ctx->pbuf->width, ctx->pbuf_y - ctx->pbuf_y_notified,
ctx->udata);
else
(*ctx->ufunc) (ctx->pbuf,
0, ctx->pbuf->height - ctx->pbuf_y,
ctx->pbuf->width, ctx->pbuf_y - ctx->pbuf_y_notified,
ctx->udata);
ctx->pbuf_y_notified = ctx->pbuf_y;
}
static gboolean
tga_format_supported (guint type,
guint bits_per_pixel)
{
switch (type)
{
case TGA_TYPE_PSEUDOCOLOR:
case TGA_TYPE_RLE_PSEUDOCOLOR:
return bits_per_pixel == 8;
case TGA_TYPE_TRUECOLOR:
case TGA_TYPE_RLE_TRUECOLOR:
return bits_per_pixel == 16
|| bits_per_pixel == 24
|| bits_per_pixel == 32;
case TGA_TYPE_GRAYSCALE:
case TGA_TYPE_RLE_GRAYSCALE:
return bits_per_pixel == 8
|| bits_per_pixel == 16;
default:
return FALSE;
}
}
static inline void
tga_read_pixel (TGAContext *ctx,
const guchar *data,
TGAColor *color)
{
switch (ctx->hdr->type)
{
case TGA_TYPE_PSEUDOCOLOR:
case TGA_TYPE_RLE_PSEUDOCOLOR:
*color = *colormap_get_color (ctx->cmap, data[0]);
break;
case TGA_TYPE_TRUECOLOR:
case TGA_TYPE_RLE_TRUECOLOR:
if (ctx->hdr->bpp == 16)
{
guint16 col = data[0] + (data[1] << 8);
color->r = (col >> 7) & 0xf8;
color->r |= color->r >> 5;
color->g = (col >> 2) & 0xf8;
color->g |= color->g >> 5;
color->b = col << 3;
color->b |= color->b >> 5;
color->a = 255;
}
else
{
color->b = data[0];
color->g = data[1];
color->r = data[2];
if (ctx->hdr->bpp == 32)
color->a = data[3];
else
color->a = 255;
}
break;
case TGA_TYPE_GRAYSCALE:
case TGA_TYPE_RLE_GRAYSCALE:
color->r = color->g = color->b = data[0];
if (ctx->hdr->bpp == 16)
color->a = data[1];
else
color->a = 255;
break;
default:
g_assert_not_reached ();
}
}
static gboolean fill_in_context(TGAContext *ctx, GError **err)
{
gboolean alpha;
guint w, h;
g_return_val_if_fail(ctx != NULL, FALSE);
ctx->cmap_size = ((ctx->hdr->cmap_bpp + 7) >> 3) *
LE16(ctx->hdr->cmap_n_colors);
ctx->cmap = colormap_new (LE16(ctx->hdr->cmap_n_colors));
if (!ctx->cmap) {
g_set_error_literal(err, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_INSUFFICIENT_MEMORY,
_("Cannot allocate colormap"));
return FALSE;
}
alpha = ((ctx->hdr->bpp == 16) ||
(ctx->hdr->bpp == 32) ||
(ctx->hdr->has_cmap && (ctx->hdr->cmap_bpp == 32)));
w = LE16(ctx->hdr->width);
h = LE16(ctx->hdr->height);
if (ctx->sfunc) {
gint wi = w;
gint hi = h;
(*ctx->sfunc) (&wi, &hi, ctx->udata);
if (wi == 0 || hi == 0)
return FALSE;
}
ctx->pbuf = gdk_pixbuf_new (GDK_COLORSPACE_RGB, alpha, 8, w, h);
if (!ctx->pbuf) {
g_set_error_literal(err, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_INSUFFICIENT_MEMORY,
_("Cannot allocate new pixbuf"));
return FALSE;
}
return TRUE;
}
static gboolean
tga_load_image (TGAContext *ctx,
GError **err)
{
TGAColor color;
GBytes *bytes;
gsize i, size, bytes_per_pixel;
const guchar *data;
bytes_per_pixel = (ctx->hdr->bpp + 7) / 8;
size = gdk_pixbuf_buffer_queue_get_size (ctx->input) / bytes_per_pixel;
size = MIN (size, tga_pixels_remaining (ctx));
bytes = gdk_pixbuf_buffer_queue_pull (ctx->input, size * bytes_per_pixel);
g_assert (bytes != NULL);
data = g_bytes_get_data (bytes, NULL);
for (i = 0; i < size; i++)
{
tga_read_pixel (ctx, data, &color);
tga_write_pixel (ctx, &color);
data += bytes_per_pixel;
}
g_bytes_unref (bytes);
tga_emit_update (ctx);
if (tga_all_pixels_written (ctx))
ctx->process = tga_skip_rest_of_image;
return TRUE;
}
static gboolean
tga_load_rle_image (TGAContext *ctx,
GError **err)
{
GBytes *bytes;
TGAColor color;
guint rle_num, raw_num;
const guchar *s;
guchar tag;
gsize n, size, bytes_per_pixel;
bytes_per_pixel = (ctx->hdr->bpp + 7) / 8;
bytes = gdk_pixbuf_buffer_queue_peek (ctx->input, gdk_pixbuf_buffer_queue_get_size (ctx->input));
s = g_bytes_get_data (bytes, &size);
for (n = 0; n < size; ) {
tag = *s;
s++, n++;
if (tag & 0x80) {
if (n + bytes_per_pixel > size) {
--n;
break;
} else {
rle_num = (tag & 0x7f) + 1;
tga_read_pixel (ctx, s, &color);
s += bytes_per_pixel;
n += bytes_per_pixel;
rle_num = MIN (rle_num, tga_pixels_remaining (ctx));
for (; rle_num; rle_num--)
{
tga_write_pixel (ctx, &color);
}
if (tga_all_pixels_written (ctx))
break;
}
} else {
raw_num = tag + 1;
if (n + (raw_num * bytes_per_pixel) > size) {
--n;
break;
} else {
raw_num = MIN (raw_num, tga_pixels_remaining (ctx));
for (; raw_num; raw_num--) {
tga_read_pixel (ctx, s, &color);
s += bytes_per_pixel;
n += bytes_per_pixel;
tga_write_pixel (ctx, &color);
}
if (tga_all_pixels_written (ctx))
break;
}
}
}
g_bytes_unref (bytes);
gdk_pixbuf_buffer_queue_flush (ctx->input, n);
tga_emit_update (ctx);
if (tga_all_pixels_written (ctx))
ctx->process = tga_skip_rest_of_image;
return TRUE;
}
static gboolean
tga_load_colormap (TGAContext *ctx,
GError **err)
{
GBytes *bytes;
TGAColor color;
const guchar *p;
guint i, n_colors;
if (ctx->hdr->has_cmap)
{
bytes = gdk_pixbuf_buffer_queue_pull (ctx->input, ctx->cmap_size);
if (bytes == NULL)
return TRUE;
n_colors = LE16(ctx->hdr->cmap_n_colors);
p = g_bytes_get_data (bytes, NULL);
color.a = 255;
for (i = 0; i < n_colors; i++)
{
if ((ctx->hdr->cmap_bpp == 15) || (ctx->hdr->cmap_bpp == 16))
{
guint16 col = p[0] + (p[1] << 8);
color.b = (col >> 7) & 0xf8;
color.g = (col >> 2) & 0xf8;
color.r = col << 3;
p += 2;
}
else if ((ctx->hdr->cmap_bpp == 24) || (ctx->hdr->cmap_bpp == 32))
{
color.b = *p++;
color.g = *p++;
color.r = *p++;
if (ctx->hdr->cmap_bpp == 32)
color.a = *p++;
}
else
{
g_set_error_literal (err, GDK_PIXBUF_ERROR,
GDK_PIXBUF_ERROR_CORRUPT_IMAGE,
_("Unexpected bitdepth for colormap entries"));
g_bytes_unref (bytes);
return FALSE;
}
colormap_set_color (ctx->cmap, i, &color);
}
g_bytes_unref (bytes);
}
else
{
if ((ctx->hdr->type == TGA_TYPE_PSEUDOCOLOR)
|| (ctx->hdr->type == TGA_TYPE_RLE_PSEUDOCOLOR))
{
g_set_error_literal (err, GDK_PIXBUF_ERROR,
GDK_PIXBUF_ERROR_CORRUPT_IMAGE,
_("Pseudocolor image does not contain a colormap"));
return FALSE;
}
}
if ((ctx->hdr->type == TGA_TYPE_RLE_PSEUDOCOLOR)
|| (ctx->hdr->type == TGA_TYPE_RLE_TRUECOLOR)
|| (ctx->hdr->type == TGA_TYPE_RLE_GRAYSCALE))
ctx->process = tga_load_rle_image;
else
ctx->process = tga_load_image;
return TRUE;
}
static gboolean
tga_read_info (TGAContext *ctx,
GError **err)
{
if (gdk_pixbuf_buffer_queue_get_size (ctx->input) < ctx->hdr->infolen)
return TRUE;
gdk_pixbuf_buffer_queue_flush (ctx->input, ctx->hdr->infolen);
ctx->process = tga_load_colormap;
return TRUE;
}
static gboolean
tga_load_header (TGAContext *ctx,
GError **err)
{
GBytes *bytes;
bytes = gdk_pixbuf_buffer_queue_pull (ctx->input, sizeof (TGAHeader));
if (bytes == NULL)
return TRUE;
ctx->hdr = g_try_malloc (sizeof (TGAHeader));
if (!ctx->hdr)
{
g_set_error_literal (err, GDK_PIXBUF_ERROR,
GDK_PIXBUF_ERROR_INSUFFICIENT_MEMORY,
_("Cannot allocate TGA header memory"));
return FALSE;
}
g_memmove(ctx->hdr, g_bytes_get_data (bytes, NULL), sizeof(TGAHeader));
g_bytes_unref (bytes);
#ifdef DEBUG_TGA
g_print ("infolen %d "
"has_cmap %d "
"type %d "
"cmap_start %d "
"cmap_n_colors %d "
"cmap_bpp %d "
"x %d y %d width %d height %d bpp %d "
"flags %#x",
ctx->hdr->infolen,
ctx->hdr->has_cmap,
ctx->hdr->type,
LE16(ctx->hdr->cmap_start),
LE16(ctx->hdr->cmap_n_colors),
ctx->hdr->cmap_bpp,
LE16(ctx->hdr->x_origin),
LE16(ctx->hdr->y_origin),
LE16(ctx->hdr->width),
LE16(ctx->hdr->height),
ctx->hdr->bpp,
ctx->hdr->flags);
#endif
if (LE16(ctx->hdr->width) == 0 ||
LE16(ctx->hdr->height) == 0) {
g_set_error_literal(err, GDK_PIXBUF_ERROR,
GDK_PIXBUF_ERROR_CORRUPT_IMAGE,
_("TGA image has invalid dimensions"));
return FALSE;
}
if ((ctx->hdr->flags & TGA_INTERLEAVE_MASK) != TGA_INTERLEAVE_NONE) {
g_set_error_literal(err, GDK_PIXBUF_ERROR,
GDK_PIXBUF_ERROR_UNKNOWN_TYPE,
_("TGA image type not supported"));
return FALSE;
}
if (!tga_format_supported (ctx->hdr->type, ctx->hdr->bpp))
{
g_set_error_literal(err, GDK_PIXBUF_ERROR,
GDK_PIXBUF_ERROR_UNKNOWN_TYPE,
_("TGA image type not supported"));
return FALSE;
}
if (!fill_in_context(ctx, err))
return FALSE;
if (ctx->pfunc)
(*ctx->pfunc) (ctx->pbuf, NULL, ctx->udata);
ctx->process = tga_read_info;
return TRUE;
}
static gpointer gdk_pixbuf__tga_begin_load(GdkPixbufModuleSizeFunc f0,
GdkPixbufModulePreparedFunc f1,
GdkPixbufModuleUpdatedFunc f2,
gpointer udata, GError **err)
{
TGAContext *ctx;
ctx = g_try_malloc(sizeof(TGAContext));
if (!ctx) {
g_set_error_literal(err, GDK_PIXBUF_ERROR,
GDK_PIXBUF_ERROR_INSUFFICIENT_MEMORY,
_("Cannot allocate memory for TGA context struct"));
return NULL;
}
ctx->hdr = NULL;
ctx->cmap = NULL;
ctx->cmap_size = 0;
ctx->pbuf = NULL;
ctx->pbuf_x = 0;
ctx->pbuf_y = 0;
ctx->pbuf_y_notified = 0;
ctx->input = gdk_pixbuf_buffer_queue_new ();
ctx->process = tga_load_header;
ctx->sfunc = f0;
ctx->pfunc = f1;
ctx->ufunc = f2;
ctx->udata = udata;
return ctx;
}
static gboolean gdk_pixbuf__tga_load_increment(gpointer data,
const guchar *buffer,
guint size,
GError **err)
{
TGAContext *ctx = (TGAContext*) data;
TGAProcessFunc process;
g_return_val_if_fail(buffer != NULL, TRUE);
gdk_pixbuf_buffer_queue_push (ctx->input, g_bytes_new (buffer, size));
do
{
process = ctx->process;
if (!process (ctx, err))
return FALSE;
}
while (process != ctx->process);
return TRUE;
}
static gboolean gdk_pixbuf__tga_stop_load(gpointer data, GError **err)
{
TGAContext *ctx = (TGAContext *) data;
gboolean result = TRUE;
g_return_val_if_fail (ctx != NULL, FALSE);
if (ctx->pbuf == NULL || tga_pixels_remaining (ctx))
{
g_set_error_literal (err,
GDK_PIXBUF_ERROR,
GDK_PIXBUF_ERROR_CORRUPT_IMAGE,
_("TGA image was truncated or incomplete."));
result = FALSE;
}
g_free (ctx->hdr);
if (ctx->cmap)
colormap_free (ctx->cmap);
if (ctx->pbuf)
g_object_unref (ctx->pbuf);
gdk_pixbuf_buffer_queue_unref (ctx->input);
g_free (ctx);
return result;
}
#ifndef INCLUDE_tga
#define MODULE_ENTRY(function) G_MODULE_EXPORT void function
#else
#define MODULE_ENTRY(function) void _gdk_pixbuf__tga_ ## function
#endif
MODULE_ENTRY (fill_vtable) (GdkPixbufModule *module)
{
module->begin_load = gdk_pixbuf__tga_begin_load;
module->stop_load = gdk_pixbuf__tga_stop_load;
module->load_increment = gdk_pixbuf__tga_load_increment;
}
MODULE_ENTRY (fill_info) (GdkPixbufFormat *info)
{
static const GdkPixbufModulePattern signature[] = {
{ " \x1\x1", "x ", 100 },
{ " \x1\x9", "x ", 100 },
{ " \x2", "xz ", 99 }, /* only 99 since .CUR also matches this */
{ " \x3", "xz ", 100 },
{ " \xa", "xz ", 100 },
{ " \xb", "xz ", 100 },
{ NULL, NULL, 0 }
};
static const gchar *mime_types[] = {
"image/x-tga",
NULL
};
static const gchar *extensions[] = {
"tga",
"targa",
NULL
};
info->name = "tga";
info->signature = (GdkPixbufModulePattern *) signature;
info->description = NC_("image format", "Targa");
info->mime_types = (gchar **) mime_types;
info->extensions = (gchar **) extensions;
info->flags = GDK_PIXBUF_FORMAT_THREADSAFE;
info->license = "LGPL";
}