Advertisement
BombBloke

GIF API (ComputerCraft)

Apr 26th, 2015
8,694
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 28.14 KB | None | 0 0
  1. -- +---------------------+-----------+---------------------+
  2. -- |                     |           |                     |
  3. -- |                     |    GIF    |                     |
  4. -- |                     |           |                     |
  5. -- +---------------------+-----------+---------------------+
  6.  
  7. local version = "Version 1.3.0pr1"
  8.  
  9. -- Loads and renders GIF files in ComputerCraft.
  10. -- Capable of handling animations.
  11. -- http://www.computercraft.info/forums2/index.php?/topic/23056-gif-renderer
  12.  
  13. ---------------------------------------------
  14. ------------Variable Declarations------------
  15. ---------------------------------------------
  16.  
  17. if not bbpack then os.loadAPI("bbpack") end
  18.  
  19. local CCPal = io.lines and  -- CC 1.55 adds this function, and also changes the colours, so...
  20.         {[0] = {240,240,240},{242,178, 51},{229,127,216},{153,178,242},
  21.         {222,222,108},{127,204, 25},{242,178,204},{ 76, 76, 76},
  22.         {153,153,153},{ 76,153,178},{178,102,229},{ 51,102,204},
  23.         {127,102, 76},{ 87,166, 78},{204, 76, 76},{ 25, 25, 25}}
  24.     or
  25.         {[0] = {240,240,240},{235,136, 68},{195, 84,205},{102,137,211},
  26.         {222,222,108},{65,205, 52},{216,129,152},{ 67, 67, 67},
  27.         {153,153,153},{ 40,118,151},{123, 47,190},{ 37, 49,146},
  28.         { 81, 48, 26},{ 59, 81, 26},{179, 49, 44},{  0,  0,  0}}
  29.  
  30. local GIF87a, GIF89a, colourNum = {71,73,70,56,55,97}, {71,73,70,56,57,97}, {}
  31.  
  32. do
  33.     local hex = "0123456789abcdef"
  34.     for i = 1, 16 do colourNum[i - 1] = hex:sub(i, i) end
  35.     for i = 1, 16 do colourNum[hex:sub(i, i)] = i - 1 end
  36. end
  37.  
  38. ---------------------------------------------
  39. ------------Function Declarations------------
  40. ---------------------------------------------
  41.  
  42. local function snooze()
  43.     local myEvent = tostring({})
  44.     os.queueEvent(myEvent)
  45.     os.pullEvent(myEvent)
  46. end
  47.  
  48. local function simpleWindow(terminal, xPos, yPos, width, height)
  49.     local results, curX, curY = {}, 1, 1
  50.    
  51.     results.clear = function(colour)
  52.         local text = string.rep(colour, width)
  53.         for y = 1, height do results[y] = text end
  54.     end
  55.    
  56.     results.draw = function(x1, y1, x2, y2)
  57.         if x1 then
  58.             local text = string.rep(" ", x2 - x1 + 1)
  59.             for y = y1, y2 do
  60.                 local snip = results[y]:sub(x1, x2)
  61.                 terminal.setCursorPos(xPos + x1 - 1, yPos + y - 1)
  62.                 terminal.blit(text, snip, snip)
  63.             end
  64.         else
  65.             local text = string.rep(" ", width)
  66.             for y = 1, height do
  67.                 terminal.setCursorPos(xPos, yPos + y - 1)
  68.                 terminal.blit(text, results[y], results[y])
  69.             end
  70.         end
  71.     end
  72.    
  73.     results.setCursorPos = function(newX, newY)
  74.         curX, curY = newX, newY
  75.     end
  76.    
  77.     results.blit = function(_, _, colString)
  78.         if curY < 1 or curY > height or curX > width or curX + #colString < 2 then return end
  79.        
  80.         local ending = (curX + #colString - 1 > width) and (width - curX + 1) or #colString
  81.         if curX < 1 then
  82.             colString = colString:sub(2 - curX, ending)
  83.             curX = 1
  84.         elseif ending ~= #colString then colString = colString:sub(1, ending) end
  85.        
  86.         if curX > 1 then
  87.             if curX + #colString - 1 < width then
  88.                 results[curY] = results[curY]:sub(1, curX - 1) .. colString .. results[curY]:sub(curX + #colString)
  89.             else
  90.                 results[curY] = results[curY]:sub(1, curX - 1) .. colString
  91.             end
  92.         elseif #colString < width then
  93.             results[curY] = colString .. results[curY]:sub(curX + #colString)
  94.         else results[curY] = colString end
  95.     end
  96.    
  97.     return results
  98. end
  99.  
  100. -- ComputerCraft builds prior to 1.74 lack term.blit(). This function implements a substitute:
  101. local function checkTerm(terminal)
  102.     if not terminal.blit then terminal.blit = function(_, _, backCol)
  103.         local counter, lastChar = 1, backCol:sub(1, 1)
  104.  
  105.         for i = 2, #backCol do if backCol:sub(i, i) ~= lastChar then
  106.             terminal.setBackgroundColour(bit.blshift(1, colourNum[lastChar]))
  107.             terminal.write(string.rep(" ", counter))
  108.             counter, lastChar = 1, backCol:sub(i, i)
  109.         else counter = counter + 1 end end
  110.  
  111.         terminal.setBackgroundColour(bit.blshift(1, colourNum[lastChar]))
  112.         terminal.write(string.rep(" ", counter))
  113.        
  114.         snooze()
  115.     end end
  116. end
  117.  
  118. if term.setPaletteColour then
  119.     function applyPalette(image, terminal)
  120.         terminal = terminal or term
  121.  
  122.         local col, pal = 1, image.pal
  123.  
  124.         for i = 0, #pal do
  125.             local thisCol = pal[i]
  126.             terminal.setPaletteColour(col, thisCol[1] / 255, thisCol[2] / 255, thisCol[3] / 255)
  127.             col = col * 2
  128.         end
  129.     end
  130. end
  131.  
  132. function drawGIF(image, xPos, yPos, terminal)
  133.     xPos, yPos, terminal = xPos or 1, yPos or 1, terminal or term
  134.     if type(image[1][1]) == "table" then image = image[1] end
  135.     checkTerm(terminal)
  136.    
  137.     for y = 1, image.yend do
  138.         local x, ystart, imageY = image.xstart + 1, image.ystart, image[y]
  139.         for i = 1, #imageY do
  140.             local imageYI = imageY[i]
  141.             if type(imageYI) == "number" then
  142.                 x = x + imageYI
  143.             else
  144.                 terminal.setCursorPos(x + xPos - 1, y + ystart + yPos - 1)
  145.                 terminal.blit(string.rep(" ", #imageYI), imageYI, imageYI)
  146.                 x = x + #imageYI
  147.             end
  148.         end
  149.     end
  150. end
  151.  
  152. function animateGIF(image, xPos, yPos, terminal)
  153.     if terminal == term or not terminal then terminal = term.current() end
  154.     checkTerm(terminal)
  155.     local buffer = simpleWindow(terminal, xPos, yPos, image.width, image.height)
  156.        
  157.     while true do for i = 1, #image do
  158.         local imageI, imageIneg1 = image[i], image[i - 1]
  159.         if i == 1 then buffer.clear(image.backgroundCol2) end
  160.        
  161.         drawGIF(imageI, 1, 1, buffer)
  162.        
  163.         if i == 1 then
  164.             buffer.draw()
  165.         elseif imageIneg1.disposal == 2 then
  166.             buffer.draw(math.min(imageIneg1.xstart, imageI.xstart) + 1, math.min(imageIneg1.ystart, imageI.ystart) + 1, math.max(imageIneg1.xstart + imageIneg1.xend, imageI.xstart + imageI.xend), math.max(imageIneg1.ystart + imageIneg1.yend, imageI.ystart + imageI.yend))
  167.         else
  168.             buffer.draw(imageI.xstart + 1, imageI.ystart + 1, imageI.xstart + imageI.xend, imageI.ystart + imageI.yend)
  169.         end
  170.        
  171.         sleep(imageI.delay)
  172.        
  173.         if imageI.disposal == 2 then
  174.             local blit = string.rep(image.backgroundCol2, imageI.xend)
  175.             for y = 1, imageI.yend do
  176.                 buffer.setCursorPos(imageI.xstart + 1, y + imageI.ystart)
  177.                 buffer.blit(nil, nil, blit)
  178.             end
  179.         end
  180.     end end
  181. end
  182.  
  183. local function rasteriseGIF(image)
  184.     local results, buffer = {["width"] = image.width, ["height"] = image.height, ["backgroundCol"] = image.backgroundCol, ["backgroundCol2"] = image.backgroundCol2, ["pal"] = image.pal}, {}
  185.    
  186.     for i = 1, image.height do
  187.         local temp = {}
  188.         for j = 1, image.width do temp[j] = " " end
  189.         buffer[i] = temp
  190.     end
  191.    
  192.     for i = 1, #image do
  193.         local thisImg = image[i]
  194.         local xStart, yStart = thisImg.xstart + 1, thisImg.ystart
  195.        
  196.         for y = 1, thisImg.yend do
  197.             local x, thisImgY, buffY = xStart, thisImg[y], buffer[y + yStart]
  198.             for i = 1, #thisImgY do
  199.                 local thisImgYI = thisImgY[i]
  200.                 if type(thisImgYI) == "number" then
  201.                     x = x + thisImgYI
  202.                 elseif type(thisImgYI) == "string" then
  203.                     for j = 1, #thisImgYI do buffY[x + j - 1] = colourNum[thisImgYI:sub(j, j)] end
  204.                     x = x + #thisImgYI
  205.                 else
  206.                     for j = 1, #thisImgYI do buffY[x + j - 1] = thisImgYI[j] end
  207.                     x = x + #thisImgYI
  208.                 end
  209.             end
  210.         end
  211.  
  212.         local resultsI = {["delay"] = thisImg.delay or 0.1}
  213.         for j = 1, #buffer do
  214.             local newRow, buffRow = {}, buffer[j]
  215.             for k = 1, #buffRow do newRow[k] = buffRow[k] end
  216.             resultsI[j] = newRow
  217.         end
  218.         results[i] = resultsI
  219.  
  220.         if thisImg.disposal == 2 then
  221.             local xEnd = thisImg.xstart + thisImg.xend
  222.             for y = yStart + 1, yStart + thisImg.yend do
  223.                 local bufferY = buffer[y]
  224.                 for x = xStart, xEnd do bufferY[x] = " " end
  225.             end
  226.         end
  227.  
  228.         snooze()
  229.     end
  230.  
  231.     return results 
  232. end
  233.  
  234. function flattenGIF(image, optimise, prerastered)
  235.     if not prerastered then image = rasteriseGIF(image) end
  236.    
  237.     local width, bump, cacheImg = image.width, 0
  238.     for i = 1, #image do
  239.         local prevImg, thisImg, nextImg, same = image[i - 1 - bump], image[i - bump], image[i + 1 - bump], true
  240.         if not thisImg then break end
  241.         local txStart, txEnd, tyStart, tyEnd, cxStart, cxEnd, cyStart, cyEnd = image.width, 1, image.height, 1
  242.         if cacheImg then cxStart, cxEnd, cyStart, cyEnd = cacheImg.xstart, cacheImg.xend, cacheImg.ystart, cacheImg.yend end
  243.        
  244.         if optimise and i > 1 then
  245.             local prevDisp = prevImg.disposal
  246.             for y = 1, #thisImg do
  247.                 local thisImgY, cacheImgY = thisImg[y], cacheImg[y]
  248.                 for x = 1, width do if thisImgY[x] ~= ((prevDisp == 2 and not (x < cxStart + 1 or y < cyStart + 1 or x > cxStart + cxEnd or y > cyStart + cyEnd)) and " " or cacheImgY[x]) then
  249.                     same = false
  250.                     if y < tyStart + 1 then tyStart = y - 1 end
  251.                     if y > tyEnd then tyEnd = y end
  252.                     if x < txStart + 1 then txStart = x - 1 end
  253.                     if x > txEnd then txEnd = x end
  254.                     snooze()
  255.                 end end
  256.             end
  257.         elseif optimise == false then
  258.             same = false
  259.             for y = 1, #thisImg do
  260.                 local thisImgY = thisImg[y]
  261.                 for x = 1, width do if thisImgY[x] ~= " " then
  262.                     if y < tyStart + 1 then tyStart = y - 1 end
  263.                     if y > tyEnd then tyEnd = y end
  264.                     if x < txStart + 1 then txStart = x - 1 end
  265.                     if x > txEnd then txEnd = x end
  266.                 end end
  267.                 snooze()
  268.             end
  269.         else
  270.             same, txStart, txEnd, tyStart, tyEnd = false, 0, width, 0, #thisImg
  271.         end
  272.        
  273.         if same and i > 1 then
  274.             image[i - 1 - bump].delay = prevImg.delay + thisImg.delay
  275.             table.remove(image, i - bump)
  276.             bump = bump + 1
  277.             snooze()
  278.         else
  279.             if i - bump ~= #image and optimise ~= nil then
  280.                 local obscured = true
  281.                 for y = 1, #thisImg do
  282.                     local thisImgY, nextImgY = thisImg[y], nextImg[y]
  283.                     for x = 1, width do if thisImgY[x] ~= " " and nextImgY[x] == " " then
  284.                         obscured = false
  285.                         if y < tyStart + 1 then tyStart = y - 1 end
  286.                         if y > tyEnd then tyEnd = y end
  287.                         if x < txStart + 1 then txStart = x - 1 end
  288.                         if x > txEnd then txEnd = x end
  289.                     end end
  290.                     snooze()
  291.                 end
  292.                 thisImg.disposal = obscured and 1 or 2
  293.             else thisImg.disposal = 1 end
  294.  
  295.             if txStart > txEnd then txStart, txEnd, tyStart, tyEnd = 0, 1, 0, 1 end
  296.            
  297.             local nxStart, nxEnd, nyStart, nyEnd, cacheDisp = txStart, txEnd - txStart, tyStart, tyEnd - tyStart, cacheImg and cacheImg.disposal
  298.             newImg = {["delay"] = thisImg.delay, ["disposal"] = thisImg.disposal, ["xstart"] = nxStart, ["xend"] = nxEnd, ["ystart"] = nyStart, ["yend"] = nyEnd}
  299.            
  300.             for y = 1, nyEnd do
  301.                 local skip, chars, temp, thisImgY, cacheImgY = 0, {}, {}, thisImg[y + nyStart], cacheImg and cacheImg[y + nyStart]
  302.                 for x = nxStart + 1, nxStart + nxEnd do
  303.                     local thisVal = thisImgY[x]
  304.                     if optimise ~= nil and cacheImg and thisVal == cacheImgY[x] and (cacheDisp == 1 or (cacheDisp == 2 and (x < cxStart + 1 or y + nyStart < cyStart + 1 or x > cxStart + cxEnd or y + nyStart > cyStart + cyEnd))) then thisVal = " " end
  305.                    
  306.                     if thisVal == " " then
  307.                         skip = skip + 1
  308.                         if #chars > 0 then
  309.                             temp[#temp + 1] = (image.pal and image.pal[1][4]) and chars or table.concat(chars)
  310.                             chars = {}
  311.                         end
  312.                     else
  313.                         chars[#chars + 1] = (image.pal and image.pal[1][4]) and thisVal or colourNum[thisVal]
  314.                         if skip > 0 then
  315.                             temp[#temp + 1] = skip
  316.                             skip = 0
  317.                         end
  318.                     end
  319.                 end
  320.                 if skip == 0 then temp[#temp + 1] = (image.pal and image.pal[1][4]) and chars or table.concat(chars) end
  321.                 newImg[y] = temp
  322.                 snooze()
  323.             end
  324.             thisImg.xstart, thisImg.xend, thisImg.ystart, thisImg.yend = txStart, txEnd, tyStart, tyEnd
  325.             cacheImg = thisImg
  326.             image[i - bump] = newImg
  327.         end
  328.     end
  329.    
  330.     return image
  331. end
  332.  
  333. function resizeGIF(image, width, height)
  334.     if not width then width = math.floor(height / image.height * image.width) end
  335.     if not height then height = math.floor(width / image.width * image.height) end
  336.     local xInc, yInc, pal = image.width / width, image.height / height, image.pal or CCPal
  337.     image = rasteriseGIF(image)
  338.    
  339.     for i = 1, #image do
  340.         local oldFrame, yPos2 = image[i], 1
  341.         local newFrame = {["delay"] = oldFrame.delay or 0.1}
  342.         for y = 1, height do
  343.             local thisLine, xPos2 = {}, 1
  344.             for x = 1, width do
  345.                 local remY, R, G, B, totalWeight, yPos = yInc, 0, 0, 0, 0, yPos2
  346.                 while remY > 0 do
  347.                     local remX, maxInc, oldFrameY, xPos, yWeight = xInc, (math.floor(yPos) == yPos) and 1 or (math.ceil(yPos) - yPos), oldFrame[math.min(math.floor(yPos), image.height)], xPos2
  348.                     if remY < maxInc then yWeight = remY remY = 0 else yWeight = maxInc remY = remY - maxInc end
  349.                     while remX > 0 do
  350.                         local maxInc, thisChar, xWeight = (math.floor(xPos) == xPos) and 1 or (math.ceil(xPos) - xPos), oldFrameY[math.min(math.floor(xPos), image.width)]
  351.                         if remX < maxInc then xWeight = remX remX = 0 else xWeight = maxInc remX = remX - maxInc end
  352.  
  353.                         if thisChar ~= " " then
  354.                             local thisWeight = xWeight * yWeight
  355.                             totalWeight, thisChar = totalWeight + thisWeight, pal[thisChar]
  356.                             R, G, B = R + thisChar[1] * thisWeight, G + thisChar[2] * thisWeight, B + thisChar[3] * thisWeight
  357.                         end
  358.                        
  359.                         xPos = (maxInc < 1 and xWeight == maxInc) and math.ceil(xPos) or (xPos + xWeight)
  360.                     end
  361.                     yPos = (maxInc < 1 and yWeight == maxInc) and math.ceil(yPos) or (yPos + yWeight)
  362.                 end
  363.                
  364.                 if totalWeight > (xInc * yInc) / 2 then
  365.                     local closest, difference = 0, 10000
  366.                     R, G, B = R / totalWeight, G / totalWeight, B / totalWeight
  367.                     for j = 0, #pal do
  368.                         local thisDiff = math.abs(pal[j][1] - R) + math.abs(pal[j][2] - G) + math.abs(pal[j][3] - B)
  369.                         if thisDiff < difference then
  370.                             difference = thisDiff
  371.                             closest = j
  372.                             if difference == 0 then break end
  373.                         end
  374.                     end
  375.                     thisLine[#thisLine + 1] = closest
  376.                 else thisLine[#thisLine + 1] = " " end
  377.                 xPos2 = xPos2 + xInc
  378.             end
  379.             yPos2 = yPos2 + yInc
  380.             newFrame[y] = thisLine
  381.             snooze()
  382.         end
  383.         image[i] = newFrame
  384.     end
  385.    
  386.     image.width, image.height = width, height
  387.    
  388.     return flattenGIF(image, false, true)
  389. end
  390.  
  391. function loadGIF(targetfile, pal)
  392.     local results, fileread = {["backgroundCol"] = 0}, fs.open(targetfile, "rb")
  393.     if not fileread then error("GIF.loadGIF(): Unable to open " .. targetfile .. " for input.", 2) end
  394.    
  395.     if type(pal) == "string" then
  396.         local file = fs.open(pal, "r")
  397.         pal = textutils.unserialize(file.readAll())
  398.         file.close()
  399.         for i = 1, #pal do
  400.             local palI = pal[i]
  401.             if not palI[5] then palI[5] = 0 end
  402.         end
  403.         if not pal[0] then pal[0] = {1000, 1000, 1000, "minecraft:air", 0} end
  404.         results.pal = pal
  405.     elseif pal then
  406.         -- Construct a reduced palette from the GIF's own.
  407.         if not fileread.readAll then error("Installed version of CC does not support palette alterations.") end
  408.  
  409.         pal = {}
  410.        
  411.         fileread.read(10)
  412.         local temp, counter = fileread.read(), 0
  413.         fileread.read(2)
  414.  
  415.         -- Read in global palette:
  416.         if bit.band(temp, 128) == 128 then for i = 1, 2 ^ (bit.band(temp, 7) + 1) do
  417.             local thisCol, found = fileread.read(3), false
  418.  
  419.             for i = 1, counter do if pal[i] == thisCol then
  420.                 found = true
  421.                 break
  422.             end end
  423.  
  424.             if not found then
  425.                 counter = counter + 1
  426.                 pal[counter] = thisCol
  427.             end
  428.         end end
  429.        
  430.         -- Read in any additional palettes:
  431.         while true do
  432.             local record = fileread.read()
  433.            
  434.             if record == 33 then
  435.                 fileread.read()
  436.                
  437.                 while true do
  438.                     record = fileread.read()
  439.                     if record == 0 then break end
  440.                     fileread.read(record)
  441.                     snooze()
  442.                 end
  443.             elseif record == 44 then
  444.                 fileread.read(8)
  445.                
  446.                 record = fileread.read()
  447.  
  448.                 if bit.band(record, 128) == 128 then for i = 1, 2 ^ (bit.band(record, 7) + 1) do
  449.                     local thisCol, found = fileread.read(3), false
  450.  
  451.                     for i = 1, counter do if pal[i] == thisCol then
  452.                         found = true
  453.                         break
  454.                     end end
  455.  
  456.                     if not found then
  457.                         counter = counter + 1
  458.                         pal[counter] = thisCol
  459.                     end
  460.                 end end
  461.                
  462.                 fileread.read()
  463.                
  464.                 while true do
  465.                     record = fileread.read()
  466.                     if record == 0 then break end
  467.                     fileread.read(record)
  468.                     snooze()
  469.                 end
  470.             elseif record == 59 then
  471.                 fileread.close()
  472.                 break
  473.             else
  474.                 error("Malformed record")
  475.             end
  476.         end
  477.  
  478.         -- Condense palette:
  479.         for i = 1, counter do pal[i] = {pal[i]:byte(1, 3)} end
  480.        
  481.         while #pal > 16 do
  482.             local matches, tally, bestDiff, curLen = {}, tally, 1000, #pal
  483.            
  484.             for i = 1, curLen - 1 do
  485.                 local palI, found = pal[i], false
  486.                
  487.                 for j = i + 1, curLen do
  488.                     local palJ = pal[j]
  489.                     local thisDiff = math.abs(palJ[1] - palI[1]) + math.abs(palJ[2] - palI[2]) + math.abs(palJ[3] - palI[3])
  490.                    
  491.                     if thisDiff < bestDiff then
  492.                         matches, tally, bestDiff, found = {[j] = i}, 1, thisDiff, true
  493.                     elseif thisDiff == bestDiff and not matches[j] and not matches[i] and not found then
  494.                         matches[j], tally, found = i, tally + 1, true
  495.                     end
  496.                 end
  497.             end
  498.            
  499.             local counter, max = 1, (curLen - tally > 15) and tally or (curLen - 16)
  500.             for i, j in pairs(matches) do
  501.                 local palI, palJ = pal[i], pal[j]
  502.                 --print(#pal,i,j,type(palI),type(palJ))
  503.                 pal[i] = {math.floor((palI[1] + palJ[1]) / 2), math.floor((palI[2] + palJ[2]) / 2), math.floor((palI[3] + palJ[3]) / 2)}
  504.                 pal[j], counter = "remove", counter + 1
  505.                 if counter > max then break end
  506.             end
  507.            
  508.             for i = curLen, 1, -1 do if pal[i] == "remove" then table.remove(pal, i) end end
  509.         end
  510.        
  511.         pal[0] = pal[#pal]
  512.         pal[#pal] = nil
  513.         results.pal = pal
  514.        
  515.         fileread = fs.open(targetfile, "rb")
  516.     else pal = CCPal end
  517.    
  518.     local readByte = fileread.read
  519.    
  520.     local readBytes = fileread.readAll and
  521.         function(num)
  522.             local output = readByte(num)
  523.             return {output:byte(1, #output)}
  524.         end
  525.     or
  526.         function(num)
  527.             local output = {}
  528.             for i = 1, num do output[#output + 1] = readByte() end
  529.             return output
  530.         end
  531.    
  532.     local function readInt()
  533.         local results = readByte()
  534.         return results + readByte() * 256
  535.     end
  536.  
  537.     -- GIF signature:
  538.     local temp = readBytes(6)
  539.     for i = 1, 6 do if temp[i] ~= GIF87a[i] and temp[i] ~= GIF89a[i] then
  540.         fileread.close()
  541.         error("GIF.loadGIF(): " .. targetfile .. " does not appear to be a GIF.", 2)
  542.     end end
  543.  
  544.     -- Screen discriptor:
  545.     local width = readInt()
  546.     local height = readInt()
  547.     results.width, results.height = width, height
  548.    
  549.     local cacheFrame, cacheBackup = {}
  550.     for y = 1, height do
  551.         local cacheFrameY = {}
  552.         for x = 1, width do cacheFrameY[x] = " " end
  553.         cacheFrame[y] = cacheFrameY
  554.     end
  555.        
  556.     temp = readByte()
  557.     local globalColourCount = 2 ^ (bit.band(temp, 7) + 1)
  558.     readInt()  -- This is the background colour, which we'll ignore, and instead pick something that contrasts the image later.
  559.     snooze()
  560.  
  561.     -- Palette:
  562.     local globalPal = bit.band(temp, 128) == 128
  563.     if globalPal then
  564.         globalPal = {}
  565.         for i = 0, globalColourCount - 1 do globalPal[i] = readBytes(3) end
  566.     end
  567.    
  568.     -- Read image records:
  569.     while true do
  570.         local thisImg, interlace, thisPal, transparent = {}
  571.        
  572.         -- Read image headers:
  573.         while true do
  574.             snooze()
  575.             local record = readByte()
  576.  
  577.             if record == 33 then
  578.                 record = readByte()
  579.  
  580.                 -- Gif extensions:
  581.                 if record == 249 then
  582.                     -- Graphics control extension:
  583.                     readByte()
  584.                     local flags = readByte()
  585.                     local isTransparent = bit.band(flags, 1) == 1
  586.                     thisImg.inputExpected = bit.band(flags, 2) == 2
  587.                     thisImg.disposal = bit.brshift(bit.band(flags, 28), 2)
  588.                     thisImg.delay = readInt()
  589.                     thisImg.delay = (thisImg.delay > 0) and (thisImg.delay / 100) or nil
  590.                     transparent = readByte()
  591.                     if not isTransparent then transparent = nil end
  592.                     readByte()
  593.                 elseif record == 254 then
  594.                     -- File comment:
  595.                     local fullComment = {}
  596.                    
  597.                     repeat
  598.                         local length = readByte()
  599.                         if length > 0 then fullComment[#fullComment + 1] = string.char(unpack(readBytes(length))) end
  600.                     until length == 0
  601.                            
  602.                     results.comment = table.concat(fullComment)
  603.                 elseif record == 1 then
  604.                     -- Plain text extension:
  605.                     readBytes(readByte())
  606.                    
  607.                     local fullText = {}
  608.                    
  609.                     repeat
  610.                         local length = readByte()
  611.                         if length > 0 then fullText[#fullText + 1] = string.char(unpack(readBytes(length))) end
  612.                     until length == 0
  613.                            
  614.                     results.text = table.concat(fullText)
  615.                 else
  616.                     -- All other extensions we skip past:
  617.                     repeat
  618.                         local length = readByte()
  619.                         if length > 0 then readBytes(length) end
  620.                     until length == 0
  621.                 end
  622.             elseif record == 44 then
  623.                 -- Image descriptor:
  624.                 thisImg.xstart = readInt()
  625.                 thisImg.ystart = readInt()
  626.                 thisImg.xend = readInt()
  627.                 thisImg.yend = readInt()
  628.  
  629.                 record = readByte()
  630.                 interlace = bit.band(record, 64) == 64
  631.  
  632.                 if bit.band(record, 128) == 128 then
  633.                     -- Local colour map:
  634.                     thisPal = {[0] = readBytes(3)}
  635.                     for i = 2, 2 ^ (bit.band(record, 7) + 1) do thisPal[#thisPal + 1] = readBytes(3) end
  636.                 else
  637.                     -- Global colour map:
  638.                     thisPal = textutils.unserialize(textutils.serialize(globalPal))
  639.                 end
  640.                
  641.                 local used = {}
  642.                 for i = 0, #thisPal do
  643.                     local closest, difference, thisPalI = 0, 10000, thisPal[i]
  644.  
  645.                     for j = 0, #pal do
  646.                         local palJ = pal[j]
  647.                         local thisDiff = math.abs(palJ[1] - thisPalI[1]) + math.abs(palJ[2] - thisPalI[2]) + math.abs(palJ[3] - thisPalI[3])
  648.  
  649.                         if thisDiff < difference then
  650.                             difference = thisDiff
  651.                             closest = j
  652.                             if difference == 0 then break end
  653.                         end
  654.                     end
  655.  
  656.                     thisPal[i] = closest
  657.                     if i ~= transparent then used[closest + 1] = true end
  658.                 end
  659.                
  660.                 local bgCol, bgCol2  = 2 ^ math.min(#used, 15), colourNum[math.min(#used, 15)]
  661.                 if bgCol > results.backgroundCol then
  662.                     results.backgroundCol = bgCol
  663.                     results.backgroundCol2 = bgCol2
  664.                 end
  665.                
  666.                 -- Image data follows, go read it:
  667.                 break
  668.             elseif record == 59 then
  669.                 -- End of file.
  670.                 fileread.close()
  671.                 return flattenGIF(results, false, true)
  672.             end
  673.         end
  674.        
  675.         local xStart, yStart, xEnd, yEnd = thisImg.xstart, thisImg.ystart, thisImg.xend, thisImg.yend
  676.        
  677.         if thisImg.disposal == 3 then
  678.             cacheBackup = {}
  679.             for y = 1, math.min(height - yStart, yEnd) do
  680.                 local cacheBackupY, cacheFrameY = {}, cacheFrame[y + yStart]
  681.                 for x = 1, math.min(width - xStart, xEnd) do cacheBackupY[x]  = cacheFrameY[x + xStart] end
  682.                 cacheBackup[y] = cacheBackupY
  683.             end
  684.         end
  685.        
  686.         -- Read image body:
  687.        
  688.         fileread = bbpack.open(fileread, "rb", 2 ^ fileread.read())
  689.         readByte = fileread.read
  690.  
  691.         local y, passnum, stepsize = 1, 8, 8
  692.         while y < yEnd + 1 do
  693.             local cacheFrameY = cacheFrame[y + yStart]
  694.             if y <= height then
  695.                 for x = 1, xEnd do
  696.                     local thisVal = readByte()
  697.                     if thisVal ~= transparent and x + xStart <= width then cacheFrameY[x + xStart] = thisPal[thisVal] end
  698.                 end
  699.             else for x = 1, xEnd do readByte() end end
  700.            
  701.             if interlace then
  702.                 y = y + stepsize
  703.  
  704.                 while y > yEnd and passnum > 1 do
  705.                     stepsize = passnum
  706.                     passnum = passnum / 2
  707.                     y = passnum + 1
  708.                 end
  709.             else y = y + 1 end
  710.            
  711.             snooze()
  712.         end
  713.        
  714.         for y = 1, height do
  715.             local thisImgY, cacheFrameY = {}, cacheFrame[y]
  716.             for x = 1, width do thisImgY[x] = cacheFrameY[x] end
  717.             thisImg[y] = thisImgY
  718.         end
  719.         if not thisImg.delay then thisImg.delay = 0.1 end
  720.        
  721.         if thisImg.disposal == 2 then
  722.             for y = 1, math.min(height - yStart, yEnd) do
  723.                 local cacheFrameY = cacheFrame[y + yStart]
  724.                 for x = 1, math.min(width - xStart, xEnd) do cacheFrameY[x + xStart] = " " end
  725.             end
  726.         elseif thisImg.disposal == 3 then
  727.             for y = 1, math.min(height - yStart, yEnd) do
  728.                 local cacheFrameY, cacheBackupY = cacheFrame[y + yStart], cacheBackup[y]
  729.                 for x = 1, math.min(width - xStart, xEnd) do cacheFrameY[x + xStart] = cacheBackupY[x] end
  730.             end
  731.             cacheBackup = nil
  732.         end
  733.        
  734.         fileread = fileread.extractHandle()
  735.         readByte = fileread.read
  736.        
  737.         results[#results + 1] = thisImg
  738.     end
  739. end
  740.  
  741. function setBackgroundColour(image, colour)
  742.     if type(colour) ~= "number" and not colourNum[colour] then error("GIF.setBackgroundColour: Bad colour.", 2) end
  743.    
  744.     if type(colour) == "number" then
  745.         local counter = 0
  746.         while colour > 1 do
  747.             colour = bit.brshift(colour, 1)
  748.             counter = counter + 1
  749.         end
  750.         image.backgroundCol = 2 ^ counter
  751.         image.backgroundCol2 = colourNum[counter]
  752.     else
  753.         image.backgroundCol = 2 ^ colourNum[colour]
  754.         image.backgroundCol2 = colour
  755.     end
  756. end
  757. setBackgroundColor = setBackgroundColour
  758.  
  759. function toPaintutils(image)
  760.     if type(image[1][1]) == "table" then image = image[1] end
  761.    
  762.     local results = {}
  763.    
  764.     if not image.yend then error("wtf",2) end
  765.     for y = 1, image.yend do
  766.         local resultsY, imageY = {}, image[y]
  767.         local x = image.xstart + 1
  768.         for i = 1, #imageY do
  769.             local imageYI = imageY[i]
  770.             if type(imageYI) == "number" then
  771.                 for j = 1, imageYI do resultsY[#resultsY + 1] = 0 end
  772.             else
  773.                 for j = 1, #imageYI do resultsY[#resultsY + 1] = 2 ^ colourNum[imageYI:sub(j, j)] end
  774.             end
  775.         end
  776.         results[y] = resultsY
  777.     end
  778.    
  779.     for i = #results[1] + 1, image.xstart + image.xend do results[1][i] = 0 end
  780.    
  781.     return results
  782. end
  783.  
  784. function buildGIF(...)
  785.     local colourChar, hex, width, height, used = {[0] = " "}, "0123456789abcdef", 0, 0, 0
  786.     for i = 1, 16 do colourChar[2 ^ (i - 1)] = hex:sub(i, i) end
  787.    
  788.     for i = 1, #arg do
  789.         local argI = arg[i]
  790.         if #argI > height then height = #argI end
  791.         for y = 1, #argI do if #argI[y] > width then width = #argI[y] end end
  792.     end
  793.    
  794.     for i = 1, #arg do
  795.         local thisImage, argI = {["delay"] = 0.1}, arg[i]
  796.        
  797.         for y = 1, #argI do
  798.             local thisLine, argIY = {}, argI[y]
  799.            
  800.             for x = 1, width do
  801.                 local argIYX = argIY[x] or 0
  802.                 if argIYX > used then used = argIYX end
  803.                 thisLine[x] = colourNum[colourChar[argIYX]] or " "
  804.             end
  805.                
  806.             thisImage[y] = thisLine
  807.         end
  808.        
  809.         for y = #argI + 1, height do
  810.             local thisLine = {}
  811.             for x = 1, width do thisLine[x] = " " end
  812.             thisImage[y] = thisLine
  813.         end
  814.        
  815.         arg[i] = thisImage
  816.     end
  817.    
  818.     arg.width, arg.height, arg.backgroundCol, arg.backgroundCol2 = width, height, used, colourChar[used]
  819.    
  820.     return flattenGIF(arg, true, true)
  821. end
  822.  
  823. function saveGIF(image, file)
  824.     if not (image.width or image.xstart) then
  825.         -- "paintutils" image.
  826.         image = buildGIF(image)
  827.     elseif not image.width then
  828.         -- Single frame of an image.
  829.         image = {width = image.xstart + image.xend, height = image.ystart + image.yend, image}
  830.     end
  831.    
  832.     local file, power, transCol = fs.open(file, "wb"), 5
  833.     if not file then error("Unable to open file for output.", 2) end
  834.     local writeByte = file.write
  835.    
  836.     local function writeBytes(bytes)
  837.         for i = 1, #bytes do writeByte(bytes[i]) end
  838.     end
  839.    
  840.     local function writeInt(num)
  841.         writeByte(bit.band(num, 255))
  842.         writeByte(bit.brshift(num, 8))
  843.     end
  844.  
  845.     -- GIF signature:
  846.     writeBytes(GIF89a)
  847.    
  848.     -- Screen discriptor:
  849.     writeInt(image.width)
  850.     writeInt(image.height)
  851.    
  852.     if image.pal then
  853.         local pal = image.pal
  854.         power = 1
  855.         while 2 ^ power < (#pal + 1) do power = power + 1 end
  856.         writeByte(127 + power)
  857.         transCol = 0
  858.         writeInt(transCol)
  859.  
  860.         -- Palette:
  861.         for i = 0, #pal do
  862.             local palI = pal[i]
  863.             for j = 1, 3 do writeByte(palI[j]) end
  864.         end
  865.         for i = #pal + 2, 2 ^ power do writeBytes({127, 127, 127}) end
  866.     else
  867.         writeByte(132)
  868.         transCol = #CCPal + 1
  869.         writeInt(transCol)
  870.  
  871.         -- Palette:
  872.         for i = 0, 15 do writeBytes(CCPal[i]) end
  873.         for i = 0, 15 do writeBytes({127, 127, 127}) end
  874.     end
  875.    
  876.     -- Loop extension:
  877.     if #image > 1 then
  878.         writeBytes({33, 255, 11})
  879.         for i = 1, 11 do writeByte(string.byte("NETSCAPE2.0", i)) end
  880.         writeBytes({3, 1, 0, 0, 0})
  881.     end
  882.    
  883.     -- Write image records:
  884.     for i = 1, #image do
  885.         local image = image[i]
  886.        
  887.         writeBytes({33, 249, 4})
  888.         writeByte(image.disposal and (1 + bit.blshift(image.disposal, 2)) or 1)
  889.         writeInt((not image.delay) and 0 or math.floor(image.delay * 100))
  890.         writeInt(transCol)
  891.        
  892.         writeByte(44)
  893.         writeInt(image.xstart)
  894.         writeInt(image.ystart)
  895.         writeInt(image.xend)
  896.         writeInt(image.yend)
  897.         writeByte(0)
  898.         writeByte(power)
  899.  
  900.         file = bbpack.open(file, "wb", 2 ^ power)
  901.         writeByte = file.write
  902.        
  903.         for y = 1, image.yend do
  904.             local rowLen, imageY = 0, image[y]
  905.             for i = 1, #imageY do
  906.                 local imageYI = imageY[i]
  907.                 if type(imageYI) == "number" then
  908.                     for j = 1, imageYI do writeByte(transCol) end
  909.                     rowLen = rowLen + imageYI
  910.                 elseif type(imageYI) == "string" then
  911.                     for j = 1, #imageYI do writeByte(colourNum[imageYI:sub(j, j)]) end
  912.                     rowLen = rowLen + #imageYI
  913.                 else
  914.                     for j = 1, #imageYI do writeByte(imageYI[j]) end
  915.                     rowLen = rowLen + #imageYI
  916.                 end
  917.             end
  918.             for i = 1, image.xend - rowLen do writeByte(transCol) end
  919.             snooze()
  920.         end
  921.        
  922.         file = file.extractHandle()
  923.         writeByte = file.write
  924.     end
  925.    
  926.     writeByte(59)
  927.     file.close()
  928. end
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement