mirror of
https://github.com/specht/champ.git
synced 2024-10-07 17:55:55 +00:00
475 lines
14 KiB
C
475 lines
14 KiB
C
#include <stdio.h>
|
|
#include <stdint.h>
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
|
|
#pragma pack(push, 1)
|
|
|
|
#define ANIMATION_OPTIMIZATION_CROP
|
|
// don't use transparency, it's not saving space with 1 bit graphics
|
|
// #define ANIMATION_OPTIMIZATION_TRANSPARENCY
|
|
|
|
struct header_block {
|
|
char signature[3];
|
|
char version[3];
|
|
};
|
|
|
|
struct logical_screen_descriptor {
|
|
uint16_t canvas_width;
|
|
uint16_t canvas_height;
|
|
uint8_t size_of_global_color_table: 3;
|
|
uint8_t sort_flag: 1;
|
|
uint8_t color_resolution: 3;
|
|
uint8_t global_color_table_flag: 1;
|
|
uint8_t background_color_index;
|
|
uint8_t pixel_aspect_ratio;
|
|
};
|
|
|
|
struct graphics_control_extension {
|
|
uint8_t extension_introducer;
|
|
uint8_t graphics_control_label;
|
|
uint8_t byte_size;
|
|
uint8_t transparent_color_flag: 1;
|
|
uint8_t user_input_flag: 1;
|
|
uint8_t disposal_method: 3;
|
|
uint8_t reserved: 3;
|
|
uint16_t delay_time;
|
|
uint8_t transparent_color_index;
|
|
uint8_t block_terminator;
|
|
};
|
|
|
|
struct image_descriptor {
|
|
uint8_t image_separator;
|
|
uint16_t image_left;
|
|
uint16_t image_top;
|
|
uint16_t image_width;
|
|
uint16_t image_height;
|
|
uint8_t size_of_local_color_table: 3;
|
|
uint8_t reserved: 2;
|
|
uint8_t sort_flag: 1;
|
|
uint8_t interlace_flag: 1;
|
|
uint8_t local_color_table_flag: 1;
|
|
};
|
|
|
|
struct application_extension {
|
|
uint8_t gif_extension_code;
|
|
uint8_t application_extension_label;
|
|
uint8_t length_of_application_block;
|
|
char label[11];
|
|
uint8_t length_of_data_sub_block;
|
|
uint8_t one;
|
|
uint16_t loop_count;
|
|
uint8_t terminator;
|
|
};
|
|
|
|
#pragma pack(pop)
|
|
|
|
void put8(uint8_t i)
|
|
{
|
|
fputc(i, stdout);
|
|
}
|
|
|
|
void put_struct(void* p, size_t size)
|
|
{
|
|
for (int i = 0; i < size; i++)
|
|
put8(*(unsigned char*)(p + i));
|
|
}
|
|
|
|
struct lzw_emitter {
|
|
uint8_t byte_buffer[255];
|
|
uint8_t byte_buffer_size;
|
|
uint32_t buffer;
|
|
int8_t offset;
|
|
uint8_t code_size;
|
|
};
|
|
|
|
void flush_bytes(struct lzw_emitter* emitter)
|
|
{
|
|
put8(emitter->byte_buffer_size);
|
|
put_struct(emitter->byte_buffer, emitter->byte_buffer_size);
|
|
emitter->byte_buffer_size = 0;
|
|
}
|
|
|
|
void emit_byte(struct lzw_emitter* emitter)
|
|
{
|
|
emitter->byte_buffer[emitter->byte_buffer_size++] = emitter->buffer & 0xff;
|
|
emitter->buffer >>= 8;
|
|
emitter->offset -= 8;
|
|
if (emitter->byte_buffer_size == 0xff)
|
|
flush_bytes(emitter);
|
|
}
|
|
|
|
void emit_code(struct lzw_emitter* emitter, uint32_t code)
|
|
{
|
|
emitter->buffer |= code << emitter->offset;
|
|
emitter->offset += emitter->code_size;
|
|
while (emitter->offset > 7)
|
|
emit_byte(emitter);
|
|
}
|
|
|
|
void flush_emitter(struct lzw_emitter* emitter)
|
|
{
|
|
while (emitter->offset > 0)
|
|
emit_byte(emitter);
|
|
if (emitter->byte_buffer_size > 0)
|
|
flush_bytes(emitter);
|
|
}
|
|
|
|
void encode_image(uint8_t* pixels, uint8_t* previous_pixels,
|
|
uint16_t width, uint16_t height,
|
|
uint8_t colors_used, uint16_t frame_delay)
|
|
{
|
|
uint8_t color_depth = 1;
|
|
while (colors_used > (1 << color_depth))
|
|
color_depth++;
|
|
if (color_depth < 2)
|
|
color_depth = 2;
|
|
|
|
uint16_t cropped_left = 0;
|
|
uint16_t cropped_top = 0;
|
|
uint16_t cropped_width = width;
|
|
uint16_t cropped_height = height;
|
|
int16_t transparent_color = -1;
|
|
|
|
if (previous_pixels)
|
|
{
|
|
// if previous_pixels is not null, we can do some differential encoding!
|
|
|
|
#ifdef ANIMATION_OPTIMIZATION_CROP
|
|
// crop left and right
|
|
for (int dir = 0; dir < 2; dir++)
|
|
{
|
|
while (cropped_width > 0)
|
|
{
|
|
uint8_t same_pixels = 1;
|
|
int x, y;
|
|
x = (dir == 0) ? cropped_left : cropped_left + cropped_width - 1;
|
|
for (int y = cropped_top; same_pixels && (y < cropped_top + cropped_height); y++)
|
|
{
|
|
int offset = y * width + x;
|
|
if (pixels[offset] != previous_pixels[offset])
|
|
same_pixels = 0;
|
|
}
|
|
if (same_pixels)
|
|
{
|
|
if (dir == 0)
|
|
cropped_left++;
|
|
cropped_width--;
|
|
}
|
|
else
|
|
break;
|
|
}
|
|
}
|
|
|
|
// crop top and bottom
|
|
for (int dir = 0; dir < 2; dir++)
|
|
{
|
|
while (cropped_height > 0)
|
|
{
|
|
uint8_t same_pixels = 1;
|
|
int x, y;
|
|
y = (dir == 0) ? cropped_top : cropped_top + cropped_height - 1;
|
|
for (int x = cropped_left; same_pixels && (x < cropped_left + cropped_width); x++)
|
|
{
|
|
int offset = y * width + x;
|
|
if (pixels[offset] != previous_pixels[offset])
|
|
same_pixels = 0;
|
|
}
|
|
if (same_pixels)
|
|
{
|
|
if (dir == 0)
|
|
cropped_top++;
|
|
cropped_height--;
|
|
}
|
|
else
|
|
break;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#ifdef ANIMATION_OPTIMIZATION_TRANSPARENCY
|
|
// replace unchanged pixels with tranparency
|
|
if (colors_used < 255)
|
|
{
|
|
transparent_color = colors_used;
|
|
colors_used++;
|
|
while (colors_used >= (1 << color_depth))
|
|
color_depth++;
|
|
for (int y = 0; y < cropped_height; y++)
|
|
{
|
|
uint32_t offset = (y + cropped_top) * width + cropped_left;
|
|
uint8_t* p = pixels + offset;
|
|
uint8_t* pp = previous_pixels + offset;
|
|
for (int x = 0; x < cropped_width; x++)
|
|
{
|
|
if (*p == *pp)
|
|
{
|
|
*p = (uint8_t)transparent_color;
|
|
}
|
|
p++;
|
|
pp++;
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
// write graphics control extension
|
|
struct graphics_control_extension gce;
|
|
memset(&gce, 0, sizeof(gce));
|
|
gce.extension_introducer = 0x21;
|
|
gce.graphics_control_label = 0xf9;
|
|
gce.byte_size = 4;
|
|
gce.disposal_method = 1; // draw on top
|
|
gce.delay_time = frame_delay; // in 1/100 seconds
|
|
|
|
#ifdef ANIMATION_OPTIMIZATION_TRANSPARENCY
|
|
if (transparent_color >= 0)
|
|
{
|
|
gce.transparent_color_flag = 1;
|
|
gce.transparent_color_index = transparent_color;
|
|
}
|
|
#endif
|
|
put_struct(&gce, sizeof(gce));
|
|
|
|
// write image descriptor
|
|
struct image_descriptor id;
|
|
memset(&id, 0, sizeof(id));
|
|
id.image_separator = 0x2c;
|
|
id.image_left = cropped_left;
|
|
id.image_top = cropped_top;
|
|
id.image_width = cropped_width;
|
|
id.image_height = cropped_height;
|
|
put_struct(&id, sizeof(id));
|
|
|
|
uint8_t lzw_minimum_code_size = color_depth;
|
|
// if (lzw_minimum_code_size < 2)
|
|
// lzw_minimum_code_size = 2;
|
|
|
|
put8(lzw_minimum_code_size);
|
|
|
|
struct lzw_emitter emitter;
|
|
emitter.byte_buffer_size = 0;
|
|
emitter.buffer = 0;
|
|
emitter.offset = 0;
|
|
emitter.code_size = lzw_minimum_code_size + 1;
|
|
|
|
// set up LZW encoder
|
|
uint16_t clear_code = (1 << color_depth);
|
|
uint16_t end_of_information_code = clear_code + 1;
|
|
|
|
uint16_t* prefix_table = malloc(sizeof(uint16_t) * (2048 - 1 - end_of_information_code));
|
|
uint8_t* suffix_table = malloc(sizeof(uint8_t) * (2048 - 1 - end_of_information_code));
|
|
uint16_t table_length = 0;
|
|
|
|
emit_code(&emitter, clear_code);
|
|
|
|
uint8_t* p = pixels;
|
|
uint16_t index_buffer = 0;
|
|
for (int y = 0; y < cropped_height; y++)
|
|
{
|
|
uint8_t* p = pixels + (y + cropped_top) * width + cropped_left;
|
|
for (int x = 0; x < cropped_width; x++)
|
|
{
|
|
if (x == 0 && y == 0)
|
|
{
|
|
index_buffer = (uint16_t)(*(p++));
|
|
continue;
|
|
}
|
|
uint8_t k = *(p++);
|
|
uint16_t found_table_entry = 0xffff;
|
|
for (int i = 0; i < table_length; i++)
|
|
if (index_buffer == prefix_table[i] && k == suffix_table[i])
|
|
found_table_entry = i + end_of_information_code + 1;
|
|
if (found_table_entry < 0xffff)
|
|
index_buffer = found_table_entry;
|
|
else
|
|
{
|
|
prefix_table[table_length] = index_buffer;
|
|
suffix_table[table_length] = k;
|
|
table_length += 1;
|
|
if (table_length + end_of_information_code > (1 << emitter.code_size))
|
|
emitter.code_size++;
|
|
emit_code(&emitter, index_buffer);
|
|
index_buffer = k;
|
|
if (table_length >= 2048 - 1 - end_of_information_code)
|
|
{
|
|
emit_code(&emitter, clear_code);
|
|
table_length = 0;
|
|
emitter.code_size = lzw_minimum_code_size + 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
emit_code(&emitter, index_buffer);
|
|
emit_code(&emitter, end_of_information_code);
|
|
flush_emitter(&emitter);
|
|
|
|
free(suffix_table);
|
|
free(prefix_table);
|
|
|
|
put8(0);
|
|
}
|
|
|
|
int main(int argc, char** argv)
|
|
{
|
|
if (argc < 4)
|
|
{
|
|
fprintf(stderr, "This program creates an animated GIF from a series of frames\n");
|
|
fprintf(stderr, "passed via stdin.\n");
|
|
fprintf(stderr, "\n");
|
|
fprintf(stderr, "Usage: ./pgif <width> <height> <number of colors>\n");
|
|
fprintf(stderr, " <width> 1 to 65535\n");
|
|
fprintf(stderr, " <height> 1 to 65535\n");
|
|
fprintf(stderr, " <number of colors> 1 to 255\n");
|
|
fprintf(stderr, "\n");
|
|
fprintf(stderr, "After the program has started, write to its stdin:\n");
|
|
fprintf(stderr, "- the palette (one color per line, HTML notation without #)\n");
|
|
fprintf(stderr, " example: '000000\\nffffff\\n' if you specified two colors\n");
|
|
fprintf(stderr, "- as many frames as you wish, one per line, formatted as\n");
|
|
fprintf(stderr, " one big hex string and starting with f\n");
|
|
fprintf(stderr, " example: 'f 000100000100\\n' if you specified a 3x2 image\n");
|
|
fprintf(stderr, "\n");
|
|
fprintf(stderr, "The default frame delay is 100 ms. You may change the frame delay\n");
|
|
fprintf(stderr, "for all following frames by specifying 'd <number>\\n' where\n");
|
|
fprintf(stderr, "<number> is a decimal number and specifies the delay in 1/100 seconds.\n");
|
|
fprintf(stderr, "\n");
|
|
fprintf(stderr, "To finalize the GIF, close the stdin stream. Output is written to stdout.\n");
|
|
exit(1);
|
|
}
|
|
char* temp = 0;
|
|
uint16_t width = strtol(argv[1], &temp, 0);
|
|
uint16_t height = strtol(argv[2], &temp, 0);
|
|
uint8_t colors_used = strtol(argv[3], &temp, 0);
|
|
|
|
uint8_t color_depth = 1;
|
|
while (colors_used > (1 << color_depth))
|
|
color_depth++;
|
|
if (color_depth < 2)
|
|
color_depth = 2;
|
|
|
|
// write header
|
|
struct header_block header;
|
|
strncpy(header.signature, "GIF", 3);
|
|
strncpy(header.version, "89a", 3);
|
|
put_struct(&header, sizeof(header));
|
|
|
|
// write logical screen descriptor
|
|
struct logical_screen_descriptor lsd;
|
|
memset(&lsd, 0, sizeof(lsd));
|
|
lsd.canvas_width = width;
|
|
lsd.canvas_height = height;
|
|
lsd.size_of_global_color_table = color_depth - 1;
|
|
lsd.color_resolution = color_depth - 1;
|
|
lsd.global_color_table_flag = 1;
|
|
put_struct(&lsd, sizeof(lsd));
|
|
|
|
size_t max_line_size = width + 1024;
|
|
char* line = malloc(max_line_size);
|
|
if (!line)
|
|
{
|
|
fprintf(stderr, "Error allocating line!\n");
|
|
exit(1);
|
|
}
|
|
char* line_p;
|
|
char hex[4];
|
|
memset(hex, 0, 4);
|
|
|
|
// write global color table, transfer palette from stdin to GIF
|
|
for (int i = 0; i < colors_used; i++)
|
|
{
|
|
fgets(line, max_line_size, stdin);
|
|
line_p = line;
|
|
for (int k = 0; k < 3; k++)
|
|
{
|
|
strncpy(hex, line_p, 2);
|
|
put8(strtol(hex, &temp, 16));
|
|
line_p += 2;
|
|
}
|
|
}
|
|
|
|
// fill remaining colors, if any
|
|
for (int i = colors_used; i < (1 << color_depth); i++)
|
|
{
|
|
put8(0); put8(0); put8(0);
|
|
}
|
|
|
|
// write application extension NETSCAPE2.0 to loop the animation
|
|
// (otherwise it just plays once, duh...)
|
|
struct application_extension ae;
|
|
memset(&ae, 0, sizeof(ae));
|
|
ae.gif_extension_code = 0x21;
|
|
ae.application_extension_label = 0xff;
|
|
ae.length_of_application_block = 0x0b;
|
|
strncpy(ae.label, "NETSCAPE2.0", 0x0b);
|
|
ae.length_of_data_sub_block = 3;
|
|
ae.one = 1;
|
|
put_struct(&ae, sizeof(ae));
|
|
|
|
uint8_t *previous_pixels = 0;
|
|
|
|
uint8_t* pixels = malloc(width * height);
|
|
if (!pixels)
|
|
{
|
|
fprintf(stderr, "Error allocating buffer for image.\n");
|
|
exit(1);
|
|
}
|
|
|
|
uint16_t frame_delay = 10;
|
|
while (fgets(line, max_line_size, stdin))
|
|
{
|
|
if (line[0] == 'f' || line[0] == 'l')
|
|
{
|
|
uint8_t* p = pixels;
|
|
if (line[0] == 'f')
|
|
{
|
|
for (int y = 0; y < height; y++)
|
|
{
|
|
for (int x = 0; x < width; x++)
|
|
{
|
|
fgets(hex, 4, stdin);
|
|
*(p++) = strtol(hex, &temp, 16);
|
|
}
|
|
}
|
|
}
|
|
else if (line[0] == 'l')
|
|
{
|
|
for (int y = 0; y < height; y++)
|
|
{
|
|
fgets(line, max_line_size, stdin);
|
|
line_p = line;
|
|
hex[1] = 0;
|
|
for (int x = 0; x < width; x++)
|
|
{
|
|
hex[0] = *(line_p++);
|
|
*(p++) = strtol(hex, &temp, 16);
|
|
}
|
|
}
|
|
}
|
|
encode_image(pixels, previous_pixels, width, height, colors_used, frame_delay);
|
|
if (!previous_pixels)
|
|
{
|
|
previous_pixels = malloc(width * height);
|
|
if (!previous_pixels)
|
|
{
|
|
fprintf(stderr, "Error allocating buffer for image.\n");
|
|
exit(1);
|
|
}
|
|
}
|
|
memcpy(previous_pixels, pixels, width * height);
|
|
}
|
|
else if (line[0] == 'd')
|
|
frame_delay = strtol(line + 2, &temp, 0);
|
|
}
|
|
|
|
if (previous_pixels)
|
|
free(previous_pixels);
|
|
free(pixels);
|
|
free(line);
|
|
|
|
// write trailer
|
|
put8(0x3b);
|
|
|
|
return 0;
|
|
}
|