/* gif-playtime - v2020-11-01 - public domain (details at end of file) Parses the playtime duration of a GIF file, with browser interpretations. By Johnathan Roatch - https://jroatch.xyz/software/ Compile with any standard C99 compiler: gcc -O2 -std=c99 -DUSE_MAIN_CLI_APP -o gif-playtime gif-playtime.c or to use as a single header styled library, rename this to "jrr-gif-playtime.h", and then #define JRR_GIF_PLAYTIME_IMPLEMENTATION in *one* C/C++ file before the #include. For example: #define JRR_GIF_PLAYTIME_IMPLEMENTATION #include "jrr-gif-playtime.h" It all started out with a chat message "Does anybody know of a tool that will display the total lenth in time for a GIF? ... My workaround is to reencode it with Abode Media Encoder" I couldn't imagine that being quick whatsoever, so I quickly made this tool to do this one task without any overhead. For reference I started with [gif_load] and the [html formatted version] of [gif89a.txt], ignored all pixel decoding aspects, and then refined timing interpretation according Nullsleep's findings for GIF's [Minimum Frame Delay]. [gif_load]: https://github.com/hidefromkgb/gif_load [gif89a.txt]: https://www.w3.org/Graphics/GIF/spec-gif89a.txt [html formatted version]: https://web.archive.org/web/20160304075538/http://qalle.net/gif89a.php [Minimum Frame Delay]: https://nullsleep.tumblr.com/post/16524517190/animated-gif-minimum-frame-delay-browser */ #ifndef INCLUDE_JRR_GIF_PLAYTIME_H #define INCLUDE_JRR_GIF_PLAYTIME_H #ifdef __cplusplus extern "C" { #endif #include // size_t /* Returns the total play time of a GIF file in 1/100 s units. On error returns 1 after the negative position of the error in data. in Chrome/Firefox delays below 0.02 rounds up to 0.10 in Internet Explorer delays below 0.06 rounds up to 0.10 If not NULL, chrome_time and ie_time will be written with those browser interpretations examples: - clear.gif returns 0 - dancing-tux.gif returns 60, and writes 300 to ie_time if not NULL. - if data[5] != 'a', -6 is returned. - if reading goes beyond the end of data, -(data_length+1) is returned. */ int jrr_gif_playtime(const unsigned char* data, size_t data_length, int* chrome_time, int* ie_time); #ifdef __cplusplus } #endif #endif // INCLUDE_JRR_GIF_PLAYTIME_H #ifdef USE_MAIN_CLI_APP #define JRR_GIF_PLAYTIME_IMPLEMENTATION #endif #ifdef JRR_GIF_PLAYTIME_IMPLEMENTATION #define read_if_within_or_die(x,cur,data,len) \ do{ \ if ((cur) < (len)) { \ (x) = (data)[(cur)]; \ } else { \ return -((len) + 1); \ } \ } while(0) int jrr_gif_playtime(const unsigned char* data, size_t data_length, int* chrome_time, int* ie_time) { const unsigned char* magic = (unsigned char*)"GIF89a"; size_t cur; int x; if (!data) return -1; if (data_length < 13) return -(data_length + 1); for (cur = 0; cur < 6; ++cur) { // GIF87a can't have animations, but we'll parse them anyway. x = data[cur]; if ((x != magic[cur]) && !(cur == 4 && x == '7')) return -(cur+1); } // skip over the header, and the global color table if present. x = data[10]; cur = 13; if (x & 0x80) cur += 6 << (x & 0x07); enum { GIF_BLK_IMG = 0x2C, // image block header GIF_EOF = 0x3B, // end-of-file GIF_BLK_EXT = 0x21, // extension block header GIF_EXT_TXT = 0x01, // extension: plain text (long lost feature nobody used) GIF_EXT_GCT = 0xF9, // extension: graphic control GIF_EXT_COM = 0xFE, // extension: comment GIF_EXT_APP = 0xFF // extension: application }; int frames = 0; int time = 0; int chrome = 0; int ie = 0; int new_delay = 0; int new_delay_browsers = 0; while (1) { read_if_within_or_die(x, cur, data, data_length); ++cur; if (x == GIF_BLK_IMG) { cur += 8; read_if_within_or_die(x, cur, data, data_length); if (x & 0x80) cur += 6 << (x & 0x07); cur += 2; // leaving the cursor on a series of sub-blocks, to be skipped later // And now for the whole purpose of this routine, to add up // the delay values from the graphic control extensions time += new_delay; chrome += (new_delay_browsers < 2) ? 10 : new_delay_browsers; ie += (new_delay_browsers < 6) ? 10 : new_delay_browsers; // for images without graphic control attached the specs says // "process without delays", I'll interpret that to mean instantly, // 0 seconds, which will mean 0.10 seconds in web browsers. new_delay = 0; new_delay_browsers = 0; ++frames; } else if (x == GIF_BLK_EXT) { read_if_within_or_die(x, cur, data, data_length); ++cur; if (x == GIF_EXT_GCT) { // specs says at most one graphic control per image block but we'll // ignore that, and just have the new control override the old. // The spec also recommends that a 0 delay with a "user input flag" // should wait indefinitely. I'm ignoring that too. read_if_within_or_die(x, cur+3, data, data_length); new_delay = ((int)(data[cur+3] & 0xff) << 8) | (int)(data[cur+2] & 0xff); new_delay_browsers = new_delay; // this info was in a sub-block, so leave the cursor alone to let it be skipped } else if (x == GIF_EXT_TXT) { // this counts as a graphical block, thus timing delays applied for this, // but browsers don't understand the "plain text" block // so we do not clear new_delay_browsers time += new_delay; new_delay = 0; } } else if (x == GIF_EOF) { break; } else { // byte did not lead into any recognized block type. // There's no other way to determine how many bytes are in the block return -(cur-1+1); } // for both the image block and extension block, the cursor is left // on top of a series of data sub-blocks. Skip over them. do { read_if_within_or_die(x, cur, data, data_length); ++cur; cur += x; } while (x); } // Web browsers operate completely different with a single image GIF. if (frames <= 1) { chrome = 0; ie = 0; } if (chrome_time) *chrome_time = chrome; if (ie_time) *ie_time = ie; return time; } #undef read_if_within_or_die #endif // JRR_GIF_PLAYTIME_IMPLEMENTATION #ifdef USE_MAIN_CLI_APP // Standard headers that do not require the C runtime #include #include #include #include #include // C99 #include // C99 #include // FILE*, printf #include // malloc const char* PROGRAM_NAME = "gif-playtime"; const char* USAGE_TEXT = "gif-playtime - Prints the playtime duration of each .gif file\n" "\n" "Example Usage:\n" "$ gif-playtime *.gif\n" "clear.gif: 0.00 s\n" "dancing-tux.gif: 0.60 s (IE: 3.00 s)\n" "BOB_89A.gif: 48.00 s\n" "Rotating_earth_(large).gif: 3.96 s\n" "Newtons_cradle_animation_book.gif: 0.72 s (IE: 3.40 s)\n" ; // Reads files without fseek and ftell capabilities (stdin, named pipes) // this does so by using realloc a bunch of times. // returns 0 and does *not* set data_ptr if error or zero sized file. static size_t read_whole_file_without_fseek(uint8_t** data_ptr, FILE* f) { uint8_t* data = NULL; size_t length = 0; uint8_t* realloc_data = NULL; // I want a slower exponential growth then something like a simple // "capacity *= 2", so I choose the fibonacci sequence (growth rate of about 1.6). // 4KiB (common memory page size) times the 6th fibonacci number is 32kiB. size_t capacity = 32768; size_t previous_capacity = 20480; if (!data_ptr || !f) return 0; data = malloc(capacity); if (!data) return 0; while (!feof(f)) { length += fread(data + length, sizeof(uint8_t), capacity - length, f); if (ferror(f)) goto free_and_return_empty; if (length == capacity) { size_t n = capacity; capacity = capacity + previous_capacity; previous_capacity = n; realloc_data = realloc(data, capacity); if (!realloc_data) goto free_and_return_empty; data = realloc_data; } } if (!length) goto free_and_return_empty; // shrink buffer to fit the final size realloc_data = realloc(data, length); if (!realloc_data) goto free_and_return_empty; data = realloc_data; *data_ptr = data; return length; free_and_return_empty: free(data); return 0; } // returns 0 and does *not* set data_ptr if error or zero sized file. static size_t read_whole_file(uint8_t** data_ptr, FILE* f) { if (!data_ptr || !f) return 0; if (fseek(f, 0, SEEK_END) != 0) { // If fseek fails the file cursor is still at the beginning // so no need for rewind(f) return read_whole_file_without_fseek(data_ptr, f); } // fseek works, so now do it the simple way size_t length = ftell(f); if (length <= 0) return 0; uint8_t* data = malloc(length); if (!data) return 0; rewind(f); size_t read_length = fread(data, sizeof(uint8_t), length, f); if (read_length != length) { free(data); return 0; } *data_ptr = data; return length; } int main (int argc, char* argv[]) { FILE* input_file = NULL; size_t input_data_length = 0; uint8_t* input_data = NULL; if (argc < 2) { printf(USAGE_TEXT); return 0; } int number_of_failed_files = 0; int gif_std_time; int chrome_time; int ie_time; for (int i = 1; i < argc; ++i) { if (argc > 2) printf("%s: ", argv[i]); input_file = fopen(argv[i], "rb"); if (!input_file) { printf("Error opening file\n"); ++number_of_failed_files; continue; } input_data_length = read_whole_file(&input_data, input_file); fclose(input_file); if (!input_data_length) { printf("Empty file\n"); ++number_of_failed_files; continue; } gif_std_time = jrr_gif_playtime(input_data, input_data_length, &chrome_time, &ie_time); free(input_data); if (gif_std_time < 0) { if (-(gif_std_time+1) < 6) { printf("Not a GIF file\n"); } else { printf("Malformed GIF file at byte %d\n", -(gif_std_time+1)); } ++number_of_failed_files; continue; } // It is impossible to get a case where only Chrome differs // Because to have a delay < 2 it'll necessarily be < 6. if (!ie_time || ie_time == gif_std_time) { printf("%d.%02d s\n", gif_std_time/100, gif_std_time%100); } else if (chrome_time == gif_std_time) { printf("%d.%02d s (IE: %d.%02d s)\n", gif_std_time/100, gif_std_time%100, ie_time/100, ie_time%100); } else if (chrome_time == ie_time) { printf("%d.%02d s (Chrome/IE: %d.%02d s)\n", gif_std_time/100, gif_std_time%100, ie_time/100, ie_time%100); } else { printf("%d.%02d s (Chrome: %d.%02d s, IE: %d.%02d s)\n", gif_std_time/100, gif_std_time%100, chrome_time/100, chrome_time%100, ie_time/100, ie_time%100); } } return number_of_failed_files; } #endif // USE_MAIN_CLI_APP /* This is free and unencumbered software released into the public domain. Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. For more information, please refer to */