#include #include #include #include #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 \n"); fprintf(stderr, " 1 to 65535\n"); fprintf(stderr, " 1 to 65535\n"); fprintf(stderr, " 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 \\n' where\n"); fprintf(stderr, " 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); uint16_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; }