---------------------------------------------------------------------------------- -- -- LibCompress.lua -- -- Authors: jjsheets and Galmok of European Stormrage (Horde) -- Email : sheets.jeff@gmail.com and galmok@gmail.com -- Licence: GPL version 2 (General Public License) ---------------------------------------------------------------------------------- local MAJOR, MINOR = "LibCompress", 3 local LibCompress,oldminor = LibStub:NewLibrary(MAJOR, MINOR) if not LibCompress then return end -- list of codecs in this file: -- \000 - Never used -- \001 - Uncompressed -- \002 - LZW -- \003 - Huffman -- local is faster than global local table_insert = table.insert local table_remove = table.remove local table_concat = table.concat local string_char = string.char local string_byte = string.byte local string_len = string.len local string_sub = string.sub local unpack = unpack local pairs = pairs local math_modf = math.modf local bit_band = bit.band local bit_bor = bit.bor local bit_lshift = bit.lshift local bit_rshift = bit.rshift local tostring = tostring local next = next local strlen = strlen local strsplit = string.split local strfind = string.find local strgfind = string.gfind local tonumber = tonumber -------------------------------------------------------------------------------- -- Cleanup local tables = {} -- tables that may be cleaned have to be kept here local tables_to_clean = {} -- list of tables by name (string) that may be reset to {} after a timeout local timeout = -1 -- tables that may be erased local function cleanup() for k,v in pairs(tables_to_clean) do tables[k]={} tables_to_clean[k]=nil end end local function onUpdate(frame, elapsed) timeout = timeout - arg1 if timeout < 0 then this:Hide() cleanup() end end LibCompress.frame = LibCompress.frame or CreateFrame("frame", nil, UIParent) -- reuse the old frame LibCompress.frame:SetScript("OnUpdate", onUpdate) LibCompress.frame:Hide() local function setCleanupTables(s1, s2) timeout = 15 -- empty tables after 15 seconds if not LibCompress.frame:IsShown() then LibCompress.frame:Show() end if tables_to_clean[s1] then tables_to_clean[s1] = true end if tables_to_clean[s2] then tables_to_clean[s2] = true end end -------------------------------------------------------------------------------- -- LZW codec -- implemented by sheets.jeff@gmail.com -- encode is used to uniquely encode a number into a sequence of bytes that can be decoded using decode() -- the bytes returned by this do not contain "\000" local bytes = {} local function encode(x) local math_modf = math.modf for k = 1, getn(bytes) do bytes[k] = nil end local xmod x, xmod = math_modf(x/255.0) xmod = xmod * 255 bytes[getn(bytes) + 1] = xmod while x > 0 do x, xmod = math_modf(x/255.0) xmod = xmod * 255.0 bytes[getn(bytes) + 1] = xmod end if getn(bytes) == 1 and bytes[1] > 0 and bytes[1] < 250 then return string_char(bytes[1]) else for i = 1, getn(bytes) do bytes[i] = bytes[i] + 1 end return string_char(256 - getn(bytes), unpack(bytes)) end end --decode converts a unique character sequence into its equivalent number, from ss, beginning at the ith char. -- returns the decoded number and the count of characters used in the decode process. local function decode(ss,i) i = i or 1 local a = string_byte(ss,i,i) if a > 249 then local r = 0 a = 256 - a for n = i+a, i+1, -1 do r = r * 255 + string_byte(ss,n,n) - 1 end return r, a + 1 else return a, 1 end end -- -- Added by Shino -- function LibCompress:TableToString(t) if t == nil then return nil end if type(t)=="string" then return t end local cache={} local function sub_mod(t) if (type(t)=="table") then for pos,val in pairs(t) do if (type(val)=="table") then table_insert(cache, pos..">".."{") sub_mod(val) table_insert(cache, "}") elseif (type(val)=="string") then table_insert(cache, pos..'>"'..val..'"') else table_insert(cache, pos..">"..tostring(val)) end end else table_insert(cache, tostring(t)) end end sub_mod(t) return table_concat(cache, ",") -- ? maybe use a different delimiter end -- Adding a costum string.split function cause this one has a problem with too big tables -- Credits: https://gist.github.com/jaredallard/ddb152179831dd23b230 function LibCompress:StringSplit(delimiter, str) local result = { } local from = 1 local delim_from, delim_to = strfind( str, delimiter, from ) while delim_from do table_insert( result, strsub( str, from , delim_from-1 ) ) from = delim_to + 1 delim_from, delim_to = strfind( str, delimiter, from ) end table.insert( result, strsub( str, from ) ) return result end function LibCompress:StringToTable(t) if t=="" or t==nil then return nil end if type(t)=="table" then return t end local table = self:StringSplit(",",t) local function fillSubTable(start) local subT = {} local cont = true local po = nil for k, v in pairs(table) do if k>start then cont = true po = nil for pos in strgfind(v, "(.+)>{") do -- opening sub table po = tonumber(pos) if po then subT[po], start = fillSubTable(k) else subT[pos], start = fillSubTable(k) end end if k>start then -- cause it could have changed if a new table was opened for pos, val in strgfind(v, '(.+)%>"(.+)"') do po = tonumber(pos) if po then subT[po] = ""..val else subT[pos] = ""..val end cont = false end if cont then for pos, val in strgfind(v, "(.+)%>(.+)") do -- Do floats work too? po = tonumber(pos) if val == "false" then if po then subT[po] = false else subT[pos] = false end elseif val == "true" then if po then subT[po] = true else subT[pos] = true end else if po then subT[po] = tonumber(val) else subT[pos] = tonumber(val) end end end if v=="}" then -- closing sub table return subT,k -- Returning the end end end end end end return subT, 0 end return fillSubTable(0) end -- Shino end -- -- Compresses the given uncompressed string. -- Unless the uncompressed string starts with "\002", this is guaranteed to return a string equal to or smaller than -- the passed string. -- the returned string will only contain "\000" characters in rare circumstances, and will contain none if the -- source string has none. local dict = {} function LibCompress:CompressLZW(uncompressed) if type(uncompressed) == "string" then local dict_size = 256 for k, _ in pairs(dict) do dict[k] = nil end local result = {"\002"} local w = '' local ressize = 1 for i = 0, 255 do dict[string_char(i)] = i end for i = 1, strlen(uncompressed) do local c = string_sub(uncompressed, i,i) local wc = w..c if dict[wc] then w = wc else dict[wc] = dict_size dict_size = dict_size +1 local r = encode(dict[w]) ressize = ressize + strlen(r) result[getn(result) + 1] = r w = c end end if w then local r = encode(dict[w]) ressize = ressize + strlen(r) result[getn(result) + 1] = r end if (strlen(uncompressed)+1) > ressize then return table_concat(result, "") else return "\002"..uncompressed end else return nil, "Can only compress strings" end end -- if the passed string is a compressed string, this will decompress it and return the decompressed string. -- Otherwise it return an error message -- compressed strings are marked by beginning with "\002" function LibCompress:DecompressLZW(compressed) if type(compressed) == "string" then if string_sub(compressed,1,1) ~= "\002" then return nil, "Can only decompress LZW compressed data ("..tostring(string_sub(compressed,1,1))..")" end compressed = string_sub(compressed,2)-- ? local dict_size = 256 for k, _ in pairs(dict) do dict[k] = nil end for i = 0, 255 do dict[i] = string_char(i) end local result = {} local t = 1 local delta, k k, delta = decode(compressed,t) t = t + delta result[getn(result)+1] = dict[k] local w = dict[k] local entry while t <= strlen(compressed) do k, delta = decode(compressed,t) t = t + delta entry = dict[k] or (w..string_sub(w,1,1)) result[getn(result)+1] = entry dict[dict_size] = w..string_sub(entry,1,1) dict_size = dict_size + 1 w = entry end return table_concat(result) else return nil, "Can only uncompress strings" end end -------------------------------------------------------------------------------- -- Huffman codec -- implemented by Galmok of European Stormrage (Horde), galmok@gmail.com local function addCode(tree, bcode,len) if tree then tree.bcode = bcode; tree.blength = len; if tree.c1 then addCode(tree.c1, bit_bor(bcode, bit_lshift(1,len)), len+1) end if tree.c2 then addCode(tree.c2, bcode, len+1) end end end local function escape_code(code, len) local escaped_code = 0; local b; local l = 0; for i = len-1, 0,- 1 do b = bit_band( code, bit_lshift(1,i))==0 and 0 or 1 escaped_code = bit_lshift(escaped_code,1+b) + b l = l + b; end return escaped_code, len+l end tables.Huffman_compressed = {} tables.Huffman_large_compressed = {} local compressed_size = 0 local remainder; local remainder_length; local function addBits(tbl, code, len) remainder = remainder + bit_lshift(code, remainder_length) remainder_length = len + remainder_length if remainder_length > 32 then return true -- Bits lost due to too long code-words. end while remainder_length>=8 do compressed_size = compressed_size + 1 tbl[compressed_size] = string_char(bit_band(remainder, 255)) remainder = bit_rshift(remainder, 8) remainder_length = remainder_length -8 end end -- word size for this huffman algorithm is 8 bits (1 byte). This means the best compression is representing 1 byte with 1 bit, i.e. compress to 0.125 of original size. function LibCompress:CompressHuffman(uncompressed) if not type(uncompressed)=="string" then return nil, "Can only compress strings" end -- make histogram local hist = {} local n = 0 -- dont have to use all datat to make the histogram local uncompressed_size = string_len(uncompressed) local c; for i = 1, uncompressed_size do c = string_byte(uncompressed, i) hist[c] = (hist[c] or 0) + 1 end --Start with as many leaves as there are symbols. local leafs = {} local leaf; local symbols = {} for symbol, weight in pairs(hist) do leaf = { symbol=string_char(symbol), weight=weight }; symbols[symbol] = leaf; table_insert(leafs, leaf) end --Enqueue all leaf nodes into the first queue (by probability in increasing order so that the least likely item is in the head of the queue). sort(leafs, function(a,b) if a.weightb.weight then return false else return nil end end) local nLeafs = getn(leafs) -- create tree local huff = {} --While there is more than one node in the queues: local l,h, li, hi, leaf1, leaf2 local newNode; while (getn(leafs)+getn(huff) > 1) do -- Dequeue the two nodes with the lowest weight. -- Dequeue first if not next(huff) then li, leaf1 = next(leafs) table_remove(leafs, li) elseif not next(leafs) then hi, leaf1 = next(huff) table_remove(huff, hi) else li, l = next(leafs); hi, h = next(huff); if l.weight<=h.weight then leaf1 = l; table_remove(leafs, li) else leaf1 = h; table_remove(huff, hi) end end -- Dequeue second if not next(huff) then li, leaf2 = next(leafs) table_remove(leafs, li) elseif not next(leafs) then hi, leaf2 = next(huff) table_remove(huff, hi) else li, l = next(leafs); hi, h = next(huff); if l.weight<=h.weight then leaf2 = l; table_remove(leafs, li) else leaf2 = h; table_remove(huff, hi) end end --Create a new internal node, with the two just-removed nodes as children (either node can be either child) and the sum of their weights as the new weight. newNode = { c1 = leaf1, c2 = leaf2, weight = leaf1.weight+leaf2.weight } table_insert(huff,newNode) end if getn(leafs)>0 then li, l = next(leafs) table_insert(huff, l) table_remove(leafs, li) end huff = huff[1]; -- assign codes to each symbol -- c1 = "0", c2 = "1" -- As a common convention, bit '0' represents following the left child and bit '1' represents following the right child. -- c1 = left, c2 = right addCode(huff,0,0); if huff then huff.bcode = 0 huff.blength = 1 end -- READING -- bitfield = 0 -- bitfield_len = 0 -- read byte1 -- bitfield = bitfield + bit_lshift(byte1, bitfield_len) -- bitfield_len = bitfield_len + 8 -- read byte2 -- bitfield = bitfield + bit_lshift(byte2, bitfield_len) -- bitfield_len = bitfield_len + 8 -- (use 5 bits) -- word = bit_band( bitfield, bit_lshift(1,5)-1) -- bitfield = bit_rshift( bitfield, 5) -- bitfield_len = bitfield_len - 5 -- read byte3 -- bitfield = bitfield + bit_lshift(byte3, bitfield_len) -- bitfield_len = bitfield_len + 8 -- WRITING remainder = 0; remainder_length = 0; local compressed = tables.Huffman_compressed --compressed_size = 0 -- first byte is version info. 0 = uncompressed, 1 = 8-bit word huffman compressed compressed[1] = "\003" -- Header: byte 0=#leafs, byte 1-3=size of uncompressed data -- max 2^24 bytes local l = string_len(uncompressed) compressed[2] = string_char(bit_band(nLeafs-1, 255)) -- number of leafs compressed[3] = string_char(bit_band(l, 255)) -- bit 0-7 compressed[4] = string_char(bit_band(bit_rshift(l, 8), 255)) -- bit 8-15 compressed[5] = string_char(bit_band(bit_rshift(l, 16), 255)) -- bit 16-23 compressed_size = 5 -- create symbol/code map for symbol, leaf in pairs(symbols) do addBits(compressed, symbol, 8); if addBits(compressed, escape_code(leaf.bcode, leaf.blength)) then -- code word too long. Needs new revision to be able to handle more than 32 bits return string_char(0)..uncompressed end addBits(compressed, 3, 2); end -- create huffman code local large_compressed = tables.Huffman_large_compressed local large_compressed_size = 0 local ulimit for i = 1, l, 200 do ulimit = l<(i+199) and l or (i+199) for sub_i = i, ulimit do c = string_byte(uncompressed, sub_i) addBits(compressed, symbols[c].bcode, symbols[c].blength) end large_compressed_size = large_compressed_size + 1 large_compressed[large_compressed_size] = table_concat(compressed, "", 1, compressed_size) compressed_size = 0 end -- add remainding bits (if any) if remainder_length>0 then large_compressed_size = large_compressed_size + 1 large_compressed[large_compressed_size] = string_char(remainder) end local compressed_string = table_concat(large_compressed, "", 1, large_compressed_size) -- is compression worth it? If not, return uncompressed data. if (strlen(uncompressed)+1) <= strlen(compressed_string) then return "\001"..uncompressed end setCleanupTables("Huffman_compressed", "Huffman_large_compressed") return compressed_string end -- lookuptable (cached between calls) local lshiftMask = {} setmetatable(lshiftMask, { __index = function (t, k) local v = bit_lshift(1, k) rawset(t, k, v) return v end }) -- lookuptable (cached between calls) local lshiftMinusOneMask = {} setmetatable(lshiftMinusOneMask, { __index = function (t, k) local v = bit_lshift(1, k)-1 rawset(t, k, v) return v end }) local function getCode(bitfield, field_len) if field_len>=2 then local b; local p = 0; for i = 0, field_len-1 do b = bit_band(bitfield, lshiftMask[i]) if not (p==0) and not (b == 0) then -- found 2 bits set right after each other (stop bits) return bit_band( bitfield, lshiftMinusOneMask[i-1]), i-1, bit_rshift(bitfield, i+1), field_len-i-1 end p = b end end return nil end local function unescape_code(code, code_len) local unescaped_code=0; local b; local l = 0; local i = 0 while i < code_len do b = bit_band( code, lshiftMask[i]) if not (b==0) then unescaped_code = bit_bor(unescaped_code, lshiftMask[l]) i = i + 1 end i = i + 1 l = l + 1 end return unescaped_code, l end tables.Huffman_uncompressed = {} tables.Huffman_large_uncompressed = {} -- will always be as big as the larges string ever decompressed. Bad, but clearing i every timetakes precious time. function LibCompress:DecompressHuffman(compressed) if type(compressed)~="string" or compressed == nil then if type(compressed)=="table" then return compressed end return nil end local compressed_size = strlen(compressed) --decode header local info_byte = string_byte(compressed) -- is data compressed if info_byte==1 then return string_sub(compressed,2) --return uncompressed data end if not (info_byte==3) then return nil, "Can only decompress Huffman compressed data ("..tostring(info_byte)..")" end local num_symbols = string_byte(string_sub(compressed, 2, 2)) + 1 local c0 = string_byte(string_sub(compressed, 3, 3)) local c1 = string_byte(string_sub(compressed, 4, 4)) local c2 = string_byte(string_sub(compressed, 5, 5)) local orig_size = c2*65536 + c1*256 + c0 if orig_size==0 then return ""; end -- decode code->symbal map local bitfield = 0; local bitfield_len = 0; local map = {} -- only table not reused in Huffman decode. setmetatable(map, { __index = function (t, k) local v = {} rawset(t, k, v) return v end }) local i = 6; -- byte 1-5 are header bytes local c, cl; local minCodeLen = 1000; local maxCodeLen = 0; local symbol, code, code_len, _bitfield, _bitfield_len; local n = 0; local state = 0; -- 0 = get symbol (8 bits), 1 = get code (varying bits, ends with 2 bits set) while ncompressed_size then return nil, "Cannot decode map" end c = string_byte(compressed, i) bitfield = bit_bor(bitfield, bit_lshift(c, bitfield_len)) bitfield_len = bitfield_len + 8 if state == 0 then symbol = bit_band(bitfield, 255) bitfield = bit_rshift(bitfield, 8) bitfield_len = bitfield_len -8 state = 1 -- search for code now else code, code_len, _bitfield, _bitfield_len = getCode(bitfield, bitfield_len) if code then bitfield, bitfield_len = _bitfield, _bitfield_len c, cl = unescape_code(code, code_len) map[cl][c]=string_char(symbol) minCodeLen = clmaxCodeLen and cl or maxCodeLen --print("symbol: "..string_char(symbol).." code: "..tobinary(c, cl)) n = n + 1 state = 0 -- search for next symbol (if any) end end i=i+1 end -- dont create new subtables for entries not in the map. Waste of space. -- But do return an empty table to prevent runtime errors. (instead of returning nil) local mt = {} setmetatable(map, { __index = function (t, k) return mt end }) local uncompressed = tables.Huffman_uncompressed local large_uncompressed = tables.Huffman_large_uncompressed local uncompressed_size = 0 local large_uncompressed_size = 0 local test_code local test_code_len = minCodeLen; local symbol; local dec_size = 0; compressed_size = compressed_size + 1 local temp_limit = 200; -- first limit of uncompressed data. large_uncompressed will hold strings of length 200 while true do if test_code_len<=bitfield_len then test_code=bit_band( bitfield, lshiftMinusOneMask[test_code_len]) symbol = map[test_code_len][test_code] if symbol then uncompressed_size = uncompressed_size + 1 uncompressed[uncompressed_size]=symbol dec_size = dec_size + 1 if dec_size>=orig_size then -- checked here for speed reasons break; end -- process compressed bytes in smaller chunks large_uncompressed_size = large_uncompressed_size + 1 large_uncompressed[large_uncompressed_size] = table_concat(uncompressed, "", 1, uncompressed_size) uncompressed_size = 0 temp_limit = temp_limit + 200 -- repeated chunk size is 200 uncompressed bytes temp_limit = temp_limit > orig_size and orig_size or temp_limit bitfield = bit_rshift(bitfield, test_code_len) bitfield_len = bitfield_len - test_code_len test_code_len = minCodeLen else test_code_len = test_code_len + 1 if test_code_len>maxCodeLen then return nil, "Decompression error at "..tostring(i).."/"..tostring(strlen(compressed)) end end else c = string_byte(compressed, i) bitfield = bitfield + bit_lshift(c or 0, bitfield_len) bitfield_len = bitfield_len + 8 i = i + 1 if i > temp_limit then if i > compressed_size then break; end end end end setCleanupTables("Huffman_uncompressed", "Huffman_large_uncompressed") return table_concat(large_uncompressed, "", 1, large_uncompressed_size)..table_concat(uncompressed, "", 1, uncompressed_size) end -------------------------------------------------------------------------------- -- Generic codec interface function LibCompress:DecompressUncompressed(data) if type(data)~="string" then return nil, "Can only handle strings" end if string.byte(data) ~= 1 then return nil, "Can only handle uncompressed data" end return string_sub(data, 2) end local compression_methods = { [2] = LibCompress.CompressLZW, [3] = LibCompress.CompressHuffman } local decompression_methods = { [1] = LibCompress.DecompressUncompressed, [2] = LibCompress.DecompressLZW, [3] = LibCompress.DecompressHuffman } -- try all compression codecs and return best result function LibCompress:Compress(data) local method = next(compression_methods) local result = compression_methods[method](self, data) local nTable = {} local rTable = {} local n method = next(compression_methods, method) while method do n = compression_methods[method](self, data) nTable = {} for i = 1, strlen(n) do nTable[i] = string.sub(n, i, i) end for i = 1, strlen(result) do rTable[i] = string.sub(result, i, i) end if getn(nTable) < getn(rTable) then result = nTable end method = next(compression_methods, method) end return result end function LibCompress:Decompress(data) local header_info = string.byte(data) if decompression_methods[header_info] then return decompression_methods[header_info](self, data) else return nil, "Unknown compression method ("..tostring(header_info)..")" end end