Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- --------------------------------------------------------------------------------------------------------------------------
- -- Decoder of GIF-files
- --------------------------------------------------------------------------------------------------------------------------
- -- This module extracts images from GIF-files.
- -- Written in pure Lua.
- -- Compatible with Lua 5.1, 5.2, 5.3, LuaJIT.
- -- Version 1 (2017-05-15)
- -- require('gif')(filename) opens .gif-file for read-only and returns "GifObject" having the following functions:
- -- read_matrix(x, y, width, height)
- -- returns current image (one animated frame) as 2D-matrix of colors (as nested Lua tables)
- -- by default whole non-clipped picture is returned
- -- pixels are numbers: (-1) for transparent color, 0..0xFFFFFF for 0xRRGGBB color
- -- get_width_height()
- -- returns two integers (width and height are the properties of the whole file)
- -- get_file_parameters()
- -- returns table with the following fields (these are the properties of the whole file)
- -- comment -- text coment inside gif-file
- -- looped -- boolean
- -- number_of_images -- == 1 for non-animated gifs, > 1 for animated gifs
- -- get_image_parameters()
- -- returns table with fields image_no and delay_in_ms (these are properties of the current animation frame)
- -- next_image(looping_mode)
- -- switches to next frame, returns false if failed to switch
- -- looping_mode = 'never' (default) - never wrap from the last frame to the first
- -- 'always' - always wrap from the last frame to the first
- -- 'play' - depends on whether or not .gif-file is marked as looped gif
- -- close()
- --------------------------------------------------------------------------------------------------------------------------
- --[[
- script source: https://github.com/Egor-Skriptunoff/pure_lua_GIF
- credit: Egor-Skriptunoff
- modified to work with CE stream data;
- supply a stream with GIF initialized
- --]]
- function btToString(t)
- for k,v in pairs(t) do
- t[k] = string.char(v);
- end
- return table.concat(t);
- end
- function open_gif(input)
- local gif = {}
- assert(({GIF87a=0,GIF89a=0})[btToString(input.read(6))], 'wrong file format')
- local gif_width, gif_height = input.readWord(),input.readWord();
- assert(gif_width ~= 0 and gif_height ~= 0, 'wrong file format')
- function gif.get_width_height()
- return gif_width, gif_height
- end
- local global_flags = input.readByte();
- input.read(2);
- local global_palette -- 0-based global palette array (or nil)
- if global_flags >= 0x80 then
- global_palette = {}
- for color_index = 0, 2^(global_flags % 8 + 1) - 1 do
- local R, G, B = unpack(input.read(3));
- global_palette[color_index] = R * 2^16 + G * 2^8 + B
- end
- end
- local first_frame_offset = input.position;
- local file_parameters -- initially nil, filled after finishing first pass
- local fp_comment, fp_looped_animation -- for storing parameters before first pass completed
- local fp_number_of_frames = 0
- local fp_last_processed_offset = 0
- local function fp_first_pass()
- if not file_parameters then
- local current_offset = input.position;
- if current_offset > fp_last_processed_offset then
- fp_last_processed_offset = current_offset
- return true
- end
- end
- end
- local function skip_to_end_of_block()
- repeat
- local size = input.readByte()
- input.read(size)
- until size == 0
- end
- local function skip_2C()
- input.read(8);
- local local_flags = input.readByte()
- if local_flags >= 0x80 then
- input.read(3 * 2^(local_flags % 8 + 1))
- end
- input.readByte();
- skip_to_end_of_block()
- end
- local function process_blocks(callback_2C, callback_21_F9)
- -- processing blocks of GIF-file
- if (input.size == input.position) then -- end of stream
- return 'EOF';
- end
- local exit_reason
- repeat
- local starter = input.readByte()
- if starter == 0x3B then -- EOF marker
- if fp_first_pass() then
- file_parameters = {comment = fp_comment, looped = fp_looped_animation, number_of_images = fp_number_of_frames}
- end
- exit_reason = 'EOF'
- elseif starter == 0x2C then -- image marker
- if fp_first_pass() then
- fp_number_of_frames = fp_number_of_frames + 1
- end
- exit_reason = (callback_2C or skip_2C)()
- elseif starter == 0x21 then
- local fn_no = input.readByte()
- if fn_no == 0xF9 then
- (callback_21_F9 or skip_to_end_of_block)()
- elseif fn_no == 0xFE and not fp_comment then
- fp_comment = {}
- repeat
- local size = input.readByte()
- table.insert(fp_comment, btToString(input.read(size)))
- until size == 0
- fp_comment = table.concat(fp_comment)
- elseif fn_no == 0xFF and btToString(input.read(input.readByte())) == 'NETSCAPE2.0' then
- fp_looped_animation = true
- skip_to_end_of_block()
- else
- skip_to_end_of_block()
- end
- else
- error'wrong file format'
- end
- until exit_reason
- return exit_reason
- end
- function gif.get_file_parameters()
- if not file_parameters then
- local saved_offset = input.position;
- process_blocks()
- input.position = saved_offset;
- end
- return file_parameters
- end
- local loaded_frame_no = 0 --\ frame parameters (frame_no = 1, 2, 3,...)
- local loaded_frame_delay --/
- local loaded_frame_action_on_background
- local loaded_frame_transparent_color_index
- local loaded_frame_matrix -- picture of the frame \ may be two pointers to the same matrix
- local background_matrix_after_loaded_frame -- background for next picture /
- local background_rectangle_to_erase -- nil or {left, top, width, height}
- function gif.read_matrix(x, y, width, height)
- -- by default whole picture rectangle
- x, y = x or 0, y or 0
- width, height = width or gif_width - x, height or gif_height - y
- assert(x >= 0 and y >= 0 and width >= 1 and height >= 1 and x + width <= gif_width and y + height <= gif_height,
- 'attempt to read pixels out of the picture boundary')
- local matrix = {}
- for row_no = 1, height do
- matrix[row_no] = {unpack(loaded_frame_matrix[row_no + y], x + 1, x + width)}
- end
- return matrix
- end
- function gif.close()
- loaded_frame_matrix = nil
- background_matrix_after_loaded_frame = nil
- end
- function gif.get_image_parameters()
- return {image_no = loaded_frame_no, delay_in_ms = loaded_frame_delay}
- end
- local function callback_2C()
- if background_rectangle_to_erase then
- local left, top, width, height = unpack(background_rectangle_to_erase)
- background_rectangle_to_erase = nil
- for row = top + 1, top + height do
- local line = background_matrix_after_loaded_frame[row]
- for col = left + 1, left + width do
- line[col] = -1
- end
- end
- end
- loaded_frame_action_on_background = loaded_frame_action_on_background or 'combine'
- local left, top, width, height = input.readWord(),input.readWord(),input.readWord(),input.readWord();
- assert(width ~= 0 and height ~= 0 and left + width <= gif_width and top + height <= gif_height, 'wrong file format')
- local local_flags = input.readByte()
- local interlaced = local_flags % 0x80 >= 0x40
- local palette = global_palette -- 0-based palette array
- if local_flags >= 0x80 then
- palette = {}
- for color_index = 0, 2^(local_flags % 8 + 1) - 1 do
- local R, G, B = unpack(input.read(3))
- palette[color_index] = R * 2^16 + G * 2^8 + B
- end
- end
- assert(palette, 'wrong file format')
- local bits_in_color = input.readByte() -- number of colors in LZW voc
- local bytes_in_current_part_of_stream = 0
- local function read_byte_from_stream() -- returns next byte or false
- if bytes_in_current_part_of_stream > 0 then
- bytes_in_current_part_of_stream = bytes_in_current_part_of_stream - 1
- return input.readByte()
- else
- bytes_in_current_part_of_stream = input.readByte() - 1
- return bytes_in_current_part_of_stream >= 0 and input.readByte()
- end
- end
- local CLEAR_VOC = 2^bits_in_color
- local END_OF_STREAM = CLEAR_VOC + 1
- local LZW_voc -- [code] = {prefix_code, color_index}
- local bits_in_code = bits_in_color + 1
- local next_power_of_two = 2^bits_in_code
- local first_undefined_code, need_completion
- local stream_bit_buffer = 0
- local bits_in_buffer = 0
- local function read_code_from_stream()
- while bits_in_buffer < bits_in_code do
- stream_bit_buffer = stream_bit_buffer + assert(read_byte_from_stream(), 'wrong file format') * 2^bits_in_buffer
- bits_in_buffer = bits_in_buffer + 8
- end
- local code = stream_bit_buffer % next_power_of_two
- stream_bit_buffer = (stream_bit_buffer - code) / next_power_of_two
- bits_in_buffer = bits_in_buffer - bits_in_code
- return code
- end
- assert(read_code_from_stream() == CLEAR_VOC, 'wrong file format')
- local function clear_LZW_voc()
- LZW_voc = {}
- bits_in_code = bits_in_color + 1
- next_power_of_two = 2^bits_in_code
- first_undefined_code = CLEAR_VOC + 2
- need_completion = nil
- end
- clear_LZW_voc()
- -- Copy matrix background_matrix_after_loaded_frame to loaded_frame_matrix
- if loaded_frame_action_on_background == 'combine' or loaded_frame_action_on_background == 'erase' then
- loaded_frame_matrix = background_matrix_after_loaded_frame
- else -- 'undo'
- loaded_frame_matrix = {}
- for row = 1, gif_height do
- loaded_frame_matrix[row] = {unpack(background_matrix_after_loaded_frame[row])}
- end
- end
- -- Decode and apply image delta (window: left, top, width, height) on the matrix loaded_frame_matrix
- local pixels_remained = width * height
- local x_inside_window, y_inside_window -- coordinates inside window
- local function pixel_from_stream(color_index)
- pixels_remained = pixels_remained - 1
- assert(pixels_remained >= 0, 'wrong file format')
- if x_inside_window then
- x_inside_window = x_inside_window + 1
- if x_inside_window == width then
- x_inside_window = 0
- if interlaced then
- repeat
- if y_inside_window % 8 == 0 then
- y_inside_window = y_inside_window < height and y_inside_window + 8 or 4
- elseif y_inside_window % 4 == 0 then
- y_inside_window = y_inside_window < height and y_inside_window + 8 or 2
- elseif y_inside_window % 2 == 0 then
- y_inside_window = y_inside_window < height and y_inside_window + 4 or 1
- else
- y_inside_window = y_inside_window + 2
- end
- until y_inside_window < height
- else
- y_inside_window = y_inside_window + 1
- end
- end
- else
- x_inside_window, y_inside_window = 0, 0
- end
- if color_index ~= loaded_frame_transparent_color_index then
- loaded_frame_matrix[top + y_inside_window + 1][left + x_inside_window + 1]
- = assert(palette[color_index], 'wrong file format')
- end
- end
- repeat
- -- LZW_voc: [code] = {prefix_code, color_index}
- -- all the codes (CLEAR_VOC+2)...(first_undefined_code-2) are defined completely
- -- the code (first_undefined_code-1) has defined only its first component
- local code = read_code_from_stream()
- if code == CLEAR_VOC then
- clear_LZW_voc()
- elseif code ~= END_OF_STREAM then
- assert(code < first_undefined_code, 'wrong file format')
- local stack_of_pixels = {}
- local pos = 1
- local first_pixel = code
- while first_pixel >= CLEAR_VOC do
- first_pixel, stack_of_pixels[pos] = unpack(LZW_voc[first_pixel])
- pos = pos + 1
- end
- stack_of_pixels[pos] = first_pixel
- if need_completion then
- need_completion = nil
- LZW_voc[first_undefined_code - 1][2] = first_pixel
- if code == first_undefined_code - 1 then
- stack_of_pixels[1] = first_pixel
- end
- end
- -- send pixels for phrase "code" to result matrix
- for pos = pos, 1, -1 do
- pixel_from_stream(stack_of_pixels[pos])
- end
- if first_undefined_code < 0x1000 then
- -- create new code
- LZW_voc[first_undefined_code] = {code}
- need_completion = true
- if first_undefined_code == next_power_of_two then
- bits_in_code = bits_in_code + 1
- next_power_of_two = 2^bits_in_code
- end
- first_undefined_code = first_undefined_code + 1
- end
- end
- until code == END_OF_STREAM
- assert(pixels_remained == 0 and stream_bit_buffer == 0, 'wrong file format')
- local extra_byte = read_byte_from_stream()
- assert(not extra_byte or extra_byte == 0 and not read_byte_from_stream(), 'wrong file format')
- -- Modify the matrix background_matrix_after_loaded_frame
- if loaded_frame_action_on_background == 'combine' then
- background_matrix_after_loaded_frame = loaded_frame_matrix
- elseif loaded_frame_action_on_background == 'erase' then
- background_matrix_after_loaded_frame = loaded_frame_matrix
- background_rectangle_to_erase = {left, top, width, height}
- end
- loaded_frame_no = loaded_frame_no + 1
- return 'OK'
- end
- local function callback_21_F9()
- local len, flags = unpack(input.read(2))
- local delay = input.readWord()
- local transparent, terminator = unpack(input.read(2))
- assert(len == 4 and terminator == 0, 'wrong file format')
- loaded_frame_delay = delay * 10
- if flags % 2 == 1 then
- loaded_frame_transparent_color_index = transparent
- end
- local method = flags // 4 % 8
- if method == 2 then
- loaded_frame_action_on_background = 'erase'
- elseif method == 3 then
- loaded_frame_action_on_background = 'undo'
- end
- end
- local function load_next_frame()
- -- returns true if next frame was loaded (of false if there is no next frame)
- if loaded_frame_no == 0 then
- background_matrix_after_loaded_frame = {}
- for y = 1, gif_height do
- background_matrix_after_loaded_frame[y] = {}
- end
- background_rectangle_to_erase = {0, 0, gif_width, gif_height}
- input.position = first_frame_offset
- end
- loaded_frame_action_on_background = nil
- loaded_frame_transparent_color_index = nil
- return process_blocks(callback_2C, callback_21_F9) ~= 'EOF'
- end
- assert(load_next_frame(), 'wrong file format')
- function gif.next_image(looping_mode)
- if load_next_frame() then
- return true
- else
- return false
- end
- end
- return gif
- end
- --[[
- my script starts here;
- fetches frame using the GIF decoder;
- encodes as a bitmap streams into a table
- source: https://en.wikipedia.org/wiki/BMP_file_format;
- --]]
- local char = string.char;
- local insert = table.insert
- local unpack = unpack;
- local function encodeInteger(n)
- return char(n&0xFF,(n>>8)&0xFF,(n>>16)&0xFF,(n>>24)&0xFF);
- end
- local function encodePixel(p,...)
- if(not p)then return;end;return char(p&0xFF,p>>8&0xFF,p>>16&0xFF),encodePixel(...);
- end
- local function table_insert(t,i,...)
- if(i)then insert(t,i);return table_insert(t,...);end;
- end
- local function encodeBitmap(pd)
- assert(type(pd)=='table'and type(pd[1])=='table','error invalid pixel data');
- local h,w = #pd,#pd[1]; -- height,width of image
- local rowWidth = w*3;
- local rowSize = math.ceil((rowWidth)/4)*4
- local pad = ('\0'):rep(rowSize-rowWidth);
- local bytes = h*rowSize;
- local _3n,_4n = ("\0"):rep(3),("\0"):rep(4);
- local bmp = {'BM',encodeInteger(54+bytes),_4n,"\54",_3n,"\40",_3n,encodeInteger(w),encodeInteger(h),"\1\0\24\0",_4n,encodeInteger(bytes),_4n:rep(4)};
- -- for y,row in pairs(pd) do
- for i=#pd,1,-1 do -- reversed;
- table_insert(bmp,encodePixel(unpack(pd[i])));
- table_insert(bmp,pad);
- end
- bmp = table.concat(bmp);
- return bmp -- returns bitmap string; save it; or load into a stream;
- end
- function extractGIF(gifString)
- if(type(gifString)=='string')then gifString=createStringStream(gifString);end;
- local gif = open_gif(gifString);
- local currentFrame = gif.read_matrix();
- local bitmaps = {};
- local id = 1;
- while (true) do
- local bitmap = encodeBitmap(currentFrame);
- local bitmapStream = createStringStream(bitmap); -- creates a bitmap stream, so we could load using
- table.insert(bitmaps,bitmapStream);
- if (not gif.next_image()) then -- will try to load next frame;
- break;
- end
- id = id + 1;
- currentFrame = gif.read_matrix(); -- fetch next frame matrix;
- end
- return bitmaps,gif; -- table with bitmap streams; gif object -> use this to obtain information (such as delay and etc.);
- end
- -- EXAMPLE HERE
- local _internet
- function getImageString(url)
- _internet = (_internet or getInternet());
- return _internet.getURL(url);
- end
- local f = createForm();
- -- get gif information and extract it;
- local gifString = getImageString('https://media.tenor.com/images/e885b897e6928712233f6e9ce7b8ff67/tenor.gif')
- -- local gifString = getImageString('https://media.tenor.com/images/dfb11ec7ee94e551ceeac653ce636d21/tenor.gif')
- bitmapStreams,gif = extractGIF(gifString);
- local i = createImage(f);
- i.width,i.height = gif.get_width_height(); -- set width && height
- f.autosize = true; -- auto size to image size
- -- animate the image!
- t = createTimer(f);
- t.interval = gif.get_image_parameters().delay_in_ms; -- based on first frame delay
- local id = 1;
- t.onTimer = function(sender)
- id = (id > #bitmapStreams and 1 or id);
- local stream = bitmapStreams[id];
- stream.position = 0;
- i.Picture.loadFromStream(stream);
- id = id + 1;
- end
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement