nonogamer9

OC-WEB 3.0: A Web Browser For OpenComputers!

Sep 30th, 2026 (edited)
85
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 89.00 KB | Source Code | 0 0
  1. local component = require("component")
  2. local event = require("event")
  3. local internet = require("internet")
  4. local term = require("term")
  5. local unicode = require("unicode")
  6. local computer = require("computer")
  7. local filesystem = require("filesystem")
  8.  
  9. local collectgarbage = collectgarbage or function() end
  10.  
  11. local okBit, bit32lib = pcall(require, "bit32")
  12. local bit32 = okBit and bit32lib or _G.bit32
  13. local band, lshift, rshift = bit32.band, bit32.lshift, bit32.rshift
  14.  
  15. local gpu = component.gpu
  16. local depth = gpu.getDepth()
  17.  
  18. local renderBuffer = nil
  19. local renderW, renderH = 0, 0
  20.  
  21. local function ensureRenderBuffer(w, h)
  22.   if renderBuffer then
  23.     local ok, bw, bh = pcall(gpu.getBufferSize, renderBuffer)
  24.     if ok and bw == w and bh == h then
  25.       return true
  26.     end
  27.     pcall(gpu.setActiveBuffer, 0)
  28.     pcall(gpu.freeBuffer, renderBuffer)
  29.     renderBuffer = nil
  30.   end
  31.  
  32.   local ok, buf = pcall(gpu.allocateBuffer, w, h)
  33.   if not ok or not buf then
  34.     return false
  35.   end
  36.   renderBuffer = buf
  37.   renderW, renderH = w, h
  38.   return true
  39. end
  40.  
  41. local function freeRenderBuffer()
  42.   if renderBuffer then
  43.     pcall(gpu.setActiveBuffer, 0)
  44.     pcall(gpu.freeBuffer, renderBuffer)
  45.     renderBuffer = nil
  46.     renderW, renderH = 0, 0
  47.   end
  48. end
  49.  
  50. local function beginBufferRender(w, h)
  51.   if not ensureRenderBuffer(w, h) then
  52.     pcall(gpu.setActiveBuffer, 0)
  53.     return false
  54.   end
  55.   local ok = pcall(gpu.setActiveBuffer, renderBuffer)
  56.   if not ok then
  57.     renderBuffer = nil
  58.     return false
  59.   end
  60.   return true
  61. end
  62.  
  63. local function presentBuffer(w, h)
  64.   pcall(gpu.setActiveBuffer, 0)
  65.   pcall(gpu.bitblt, 0, 1, 1, w, h, renderBuffer, 1, 1)
  66. end
  67.  
  68. local realCollect = rawget(_G, "collectgarbage")
  69. local function gc()
  70.   if realCollect then
  71.     pcall(realCollect)
  72.     pcall(realCollect)
  73.     return
  74.   end
  75.   local okMem, free = pcall(computer.freeMemory)
  76.   if not okMem or type(free) ~= "number" then free = 65536 end
  77.   local count = math.max(200, math.min(3000, math.floor(free / 8 / 64)))
  78.   for _ = 1, 3 do
  79.     local junk = {}
  80.     for i = 1, count do junk[i] = {} end
  81.     junk = nil
  82.     os.sleep(0)
  83.   end
  84. end
  85.  
  86. local LOAD_IMAGES = true
  87.  
  88. local MAX_IMAGE_PIXELS = 900000
  89. local MAX_IMAGE_BYTES = 1000000
  90. local MAX_IMAGE_ROWS = 80
  91.  
  92. local IMAGE_MODE = "half"
  93.  
  94. local MAX_PAGE_BYTES = 1000000
  95.  
  96. local LINK_MARKERS = true
  97. local invertImages = false
  98.  
  99. local ENABLE_FORMS = true
  100. local TAB_FOCUS = true
  101.  
  102. local floor, ceil, abs, min, max = math.floor, math.ceil, math.abs, math.min, math.max
  103. local byte, char = string.byte, string.char
  104. local unpack = table.unpack or unpack
  105.  
  106. local function trim(s)
  107.   return (s:match("^%s*(.-)%s*$"))
  108. end
  109.  
  110. local function set(list)
  111.   local t = {}
  112.   for _, v in ipairs(list) do t[v] = true end
  113.   return t
  114. end
  115.  
  116. local function copy(t)
  117.   local n = {}
  118.   for k, v in pairs(t) do n[k] = v end
  119.   return n
  120. end
  121.  
  122. local function lum(c)
  123.   return 0.299 * (floor(c / 65536) % 256) + 0.587 * (floor(c / 256) % 256) + 0.114 * (c % 256)
  124. end
  125.  
  126. local function emphasize(fg, bg)
  127.   local target = lum(fg) >= lum(bg) and 255 or 0
  128.   local r, g, b = floor(fg / 65536) % 256, floor(fg / 256) % 256, fg % 256
  129.   r = floor(r + (target - r) * 0.4)
  130.   g = floor(g + (target - g) * 0.4)
  131.   b = floor(b + (target - b) * 0.4)
  132.   return r * 65536 + g * 256 + b
  133. end
  134.  
  135. local function q(c)
  136.   if depth == 1 then return lum(c) > 127 and 0xFFFFFF or 0x000000 end
  137.   return c
  138. end
  139.  
  140. local function status(msg)
  141.   local w, h = gpu.getResolution()
  142.   gpu.setBackground(q(0x333333))
  143.   gpu.setForeground(q(0xFFFFFF))
  144.   gpu.fill(1, h, w, 1, " ")
  145.   gpu.set(1, h, unicode.sub(msg, 1, w))
  146. end
  147.  
  148. local ENT = {
  149.   lt = "<", gt = ">", amp = "&", quot = '"', apos = "'", nbsp = " ",
  150.   mdash = "—", ndash = "–", hellip = "…", copy = "©", reg = "®", trade = "™",
  151.   laquo = "«", raquo = "»", bull = "•", middot = "·", euro = "€", pound = "£",
  152.   larr = "←", rarr = "→", uarr = "↑", darr = "↓", times = "×", deg = "°",
  153. }
  154.  
  155. local function decodeEntities(t)
  156.   return (t:gsub("&(#?%w+);", function(e)
  157.     if e:sub(1, 1) == "#" then
  158.       local n
  159.       if e:match("^#[xX]%x+$") then n = tonumber(e:sub(3), 16) else n = tonumber(e:sub(2)) end
  160.       if n == 160 then return " " end
  161.       if n and n > 31 and n < 0x110000 then return unicode.char(n) end
  162.       return nil
  163.     end
  164.     return ENT[e]
  165.   end))
  166. end
  167.  
  168. local NAMED = {
  169.   black = 0x000000, white = 0xFFFFFF, red = 0xFF0000, green = 0x008000, blue = 0x0000FF,
  170.   yellow = 0xFFFF00, cyan = 0x00FFFF, aqua = 0x00FFFF, magenta = 0xFF00FF, fuchsia = 0xFF00FF,
  171.   gray = 0x808080, grey = 0x808080, silver = 0xC0C0C0, maroon = 0x800000, olive = 0x808000,
  172.   lime = 0x00FF00, navy = 0x000080, teal = 0x008080, purple = 0x800080, orange = 0xFFA500,
  173.   pink = 0xFFC0CB, brown = 0xA52A2A, gold = 0xFFD700, darkgray = 0xA9A9A9, darkgrey = 0xA9A9A9,
  174.   lightgray = 0xD3D3D3, lightgrey = 0xD3D3D3, darkred = 0x8B0000, darkgreen = 0x006400,
  175.   darkblue = 0x00008B, lightblue = 0xADD8E6, lightgreen = 0x90EE90, skyblue = 0x87CEEB,
  176.   coral = 0xFF7F50, crimson = 0xDC143C, indigo = 0x4B0082, violet = 0xEE82EE, beige = 0xF5F5DC,
  177.   ivory = 0xFFFFF0, khaki = 0xF0E68C, tan = 0xD2B48C, salmon = 0xFA8072, turquoise = 0x40E0D0,
  178.   whitesmoke = 0xF5F5F5, dimgray = 0x696969, dimgrey = 0x696969, steelblue = 0x4682B4,
  179.   royalblue = 0x4169E1, orangered = 0xFF4500, tomato = 0xFF6347, chocolate = 0xD2691E,
  180.   firebrick = 0xB22222, forestgreen = 0x228B22, seagreen = 0x2E8B57, slategray = 0x708090,
  181.   midnightblue = 0x191970, hotpink = 0xFF69B4, lavender = 0xE6E6FA, wheat = 0xF5DEB3,
  182. }
  183.  
  184. local function parseColor(v, bare)
  185.   if not v then return nil end
  186.   v = trim(v:lower())
  187.   if NAMED[v] then return NAMED[v] end
  188.   local hex = v:match("^#(%x+)$") or (bare and v:match("^(%x%x%x%x%x%x)$"))
  189.   if hex then
  190.     if #hex == 3 or #hex == 4 then
  191.       local r, g, b = hex:sub(1, 1), hex:sub(2, 2), hex:sub(3, 3)
  192.       return tonumber(r .. r .. g .. g .. b .. b, 16)
  193.     elseif #hex == 6 or #hex == 8 then
  194.       return tonumber(hex:sub(1, 6), 16)
  195.     end
  196.     return nil
  197.   end
  198.   local inner = v:match("^rgba?%((.-)%)$")
  199.   if inner then
  200.     local c = {}
  201.     for num, pct in inner:gmatch("([%d%.]+)(%%?)") do
  202.       local x = tonumber(num) or 0
  203.       if pct == "%" then x = x * 2.55 end
  204.       c[#c + 1] = max(0, min(255, floor(x + 0.5)))
  205.       if #c == 3 then break end
  206.     end
  207.     if #c == 3 then return c[1] * 65536 + c[2] * 256 + c[3] end
  208.   end
  209.   return nil
  210. end
  211.  
  212. local function findColor(v)
  213.   v = v:gsub("url%b()", "")
  214.   local f = v:match("rgba?%b()") or v:match("#%x+")
  215.   if f then return parseColor(f) end
  216.   for word in v:gmatch("%a+") do
  217.     local c = NAMED[word]
  218.     if c then return c end
  219.   end
  220.   return nil
  221. end
  222.  
  223. local function resolveURL(base, rel)
  224.   if not rel then return nil end
  225.   rel = trim(rel)
  226.   if rel == "" then return nil end
  227.   if rel:match("^https?://") then return rel end
  228.   if rel:match("^[%a][%w%+%.%-]*:") or rel:sub(1, 1) == "#" then return nil end
  229.   if not base then return nil end
  230.   local scheme, host, path = base:match("^(https?)://([^/?#]+)(.*)$")
  231.   if not scheme then return nil end
  232.   if rel:sub(1, 2) == "//" then return scheme .. ":" .. rel end
  233.   path = path:gsub("[#?].*$", "")
  234.   if rel:sub(1, 1) == "?" then return scheme .. "://" .. host .. path .. rel end
  235.   rel = rel:gsub("#.*$", "")
  236.   local query = rel:match("(%?.*)$") or ""
  237.   rel = rel:gsub("%?.*$", "")
  238.   local full
  239.   if rel:sub(1, 1) == "/" then
  240.     full = rel
  241.   else
  242.     full = (path:match("^(.*/)") or "/") .. rel
  243.   end
  244.   local out = {}
  245.   for seg in full:gmatch("([^/]+)") do
  246.     if seg == ".." then out[#out] = nil elseif seg ~= "." then out[#out + 1] = seg end
  247.   end
  248.   local res = "/" .. table.concat(out, "/")
  249.   if full:sub(-1) == "/" and #out > 0 then res = res .. "/" end
  250.   return scheme .. "://" .. host .. res .. query
  251. end
  252.  
  253. local PAGE_HEADERS = {
  254.   ["User-Agent"] = "OpenComputers/3.0 (Compatible; OCWeb/3.0)",
  255.   ["Accept"] = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
  256. }
  257. local IMG_HEADERS = {
  258.   ["User-Agent"] = "OpenComputers/3.0 (Compatible; OCWeb/3.0)",
  259.   ["Accept"] = "image/png,image/jpeg,image/bmp,image/gif,image/*;q=0.5,*/*;q=0.1",
  260. }
  261.  
  262. local BROWSER_HEADERS = {
  263.   ["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0",
  264.   ["Accept"] = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
  265.   ["Accept-Language"] = "en-US,en;q=0.5",
  266. }
  267.  
  268. local function header(h, name)
  269.   if not h then return nil end
  270.   for k, v in pairs(h) do
  271.     if tostring(k):lower() == name then
  272.       if type(v) == "table" then return v[1] end
  273.       return v
  274.     end
  275.   end
  276.   return nil
  277. end
  278.  
  279. local function httpRequest(url, headers, limit)
  280.   os.sleep(0)
  281.   local ok, handle = pcall(internet.request, url, nil, headers)
  282.   if not ok or not handle then
  283.     return nil, "Failed to connect to " .. url .. ": " .. tostring(handle)
  284.   end
  285.   local parts, total, code, rh = {}, 0, nil, nil
  286.   local ok2, err = pcall(function()
  287.     for chunk in handle do
  288.       if not code then
  289.         local c, _, h = handle.response()
  290.         code, rh = c, h
  291.       end
  292.       if chunk then
  293.         parts[#parts + 1] = chunk
  294.         total = total + #chunk
  295.         if limit and total > limit then error("response too large") end
  296.       end
  297.       os.sleep(0)
  298.     end
  299.   end)
  300.   if not ok2 then
  301.     if handle.close then pcall(handle.close) end
  302.     return nil, tostring(err), true
  303.   end
  304.   if not code then
  305.     local ok3, c, _, h = pcall(handle.response)
  306.     if ok3 then code, rh = c, h end
  307.   end
  308.   return table.concat(parts), code, rh
  309. end
  310.  
  311. local function badRedirectTarget(msg)
  312.   msg = tostring(msg or "")
  313.   local u = msg:match("(https?://[^\r\n]+)")
  314.   if not u then return nil end
  315.   u = trim(u)
  316.   u = u:gsub("#.*$", "")
  317.   u = u:gsub("[%c %z\128-\255]", function(c)
  318.     return string.format("%%%02X", byte(c))
  319.   end)
  320.   return u
  321. end
  322.  
  323. local function httpFollow(url, headers, limit)
  324.   local seen = {}
  325.   local sawDead = false
  326.   for _ = 1, 8 do
  327.     if seen[url] then return nil, "Redirect loop detected" end
  328.     seen[url] = true
  329.     local data, code, rh = httpRequest(url, headers, limit)
  330.     if not data then
  331.       local dead = rh == true and badRedirectTarget(code) or nil
  332.       if dead then sawDead = true end
  333.       if dead and not seen[dead] then
  334.         url = dead
  335.       elseif dead then
  336.         return nil, "Server redirected to a broken address: " .. tostring(code):gsub("^[^\n:]*:%d+: ", ""), true
  337.       else
  338.         return nil, (tostring(code):gsub("^[^\n:]*:%d+: ", "")), sawDead
  339.       end
  340.     elseif code and code >= 300 and code < 400 then
  341.       local loc = header(rh, "location")
  342.       if loc then
  343.         local nu = resolveURL(url, loc)
  344.         if not nu then return nil, "Invalid redirect URL: " .. loc end
  345.         url = (nu:gsub(" ", "%%20"))
  346.       else
  347.         return data, url
  348.       end
  349.     elseif code and code >= 400 then
  350.       return nil, "HTTP Error: " .. code
  351.     else
  352.       return data, url
  353.     end
  354.   end
  355.   return nil, "Too many redirects"
  356. end
  357.  
  358. local function clientRedirect(data, url)
  359.   local function usable(u)
  360.     if not u then return nil end
  361.     u = trim(u)
  362.     if u == "" or u:find("[%+]%s") or u:find("%s[%+]") or u:find("[\"'<>]") then return nil end
  363.     local t = resolveURL(url, u)
  364.     if not t then return nil end
  365.     return (t:gsub(" ", "%%20"))
  366.   end
  367.   local target
  368.   for tag in data:gmatch("<[mM][eE][tT][aA][^>]*>") do
  369.     if tag:lower():find("refresh", 1, true) then
  370.       local u = tag:match("[uU][rR][lL]%s*=%s*['\"]?([^'\">]+)")
  371.       target = usable(u) or target
  372.     end
  373.   end
  374.   if not target and #data < 1500 then
  375.     local js = data:match("window%.location[%.%a]*%s*=%s*['\"](.-)['\"]")
  376.     target = usable(js)
  377.   end
  378.   return target
  379. end
  380.  
  381. local function loadPage(url)
  382.   local data, finalUrl, dead = httpFollow(url, PAGE_HEADERS, MAX_PAGE_BYTES)
  383.   if not data and dead then
  384.     local d2, f2 = httpFollow(url, BROWSER_HEADERS, MAX_PAGE_BYTES)
  385.     if d2 then return d2, f2 end
  386.   end
  387.   return data, finalUrl
  388. end
  389.  
  390. local function fetchPage(url)
  391.   local data, finalUrl = loadPage(url)
  392.   if not data then return false, finalUrl end
  393.   url = finalUrl
  394.   local seen = {}
  395.   for _ = 1, 6 do
  396.     seen[url] = true
  397.     local target = clientRedirect(data, url)
  398.     if not target or seen[target] then break end
  399.     local nd, nu = loadPage(target)
  400.     if not nd then break end
  401.     data, url = nd, nu
  402.   end
  403.   return true, data, url
  404. end
  405.  
  406. local LBASE = {3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258}
  407. local LEXT = {0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0}
  408. local DBASE = {1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577}
  409. local DEXT = {0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13}
  410. local CLORDER = {16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15}
  411.  
  412. local function inflate(data, pos, sink)
  413.   local bitbuf, bitcnt = 0, 0
  414.   local pend, pendn, hist, hlen = {}, 0, "", 0
  415.   local ops = 0
  416.   local fixedLit, fixedDist
  417.  
  418.   local function bits(need)
  419.     while bitcnt < need do
  420.       local b = byte(data, pos)
  421.       if not b then error("inflate: unexpected end of data") end
  422.       pos = pos + 1
  423.       bitbuf = bitbuf + lshift(b, bitcnt)
  424.       bitcnt = bitcnt + 8
  425.     end
  426.     local v = band(bitbuf, lshift(1, need) - 1)
  427.     bitbuf = rshift(bitbuf, need)
  428.     bitcnt = bitcnt - need
  429.     return v
  430.   end
  431.  
  432.   local function flush()
  433.     if pendn > 0 then
  434.       local s = char(unpack(pend, 1, pendn))
  435.       sink(s)
  436.       hist = (hist .. s):sub(-32768)
  437.       hlen = #hist
  438.       pendn = 0
  439.     end
  440.   end
  441.  
  442.   local function put(b)
  443.     pendn = pendn + 1
  444.     pend[pendn] = b
  445.     if pendn >= 2048 then flush() end
  446.   end
  447.  
  448.   local function back(dist)
  449.     local idx = pendn - dist + 1
  450.     if idx >= 1 then return pend[idx] end
  451.     local h = hlen + idx
  452.     if h < 1 then error("inflate: bad distance") end
  453.     return byte(hist, h)
  454.   end
  455.  
  456.   local function build(lens, n)
  457.     local count, offs, symbol = {}, {}, {}
  458.     for i = 0, 15 do count[i] = 0 end
  459.     for i = 0, n - 1 do count[lens[i]] = count[lens[i]] + 1 end
  460.     offs[1] = 0
  461.     for i = 1, 14 do offs[i + 1] = offs[i] + count[i] end
  462.     for i = 0, n - 1 do
  463.       local l = lens[i]
  464.       if l ~= 0 then
  465.         symbol[offs[l]] = i
  466.         offs[l] = offs[l] + 1
  467.       end
  468.     end
  469.     return { count = count, symbol = symbol }
  470.   end
  471.  
  472.   local function decode(h)
  473.     local code, first, index = 0, 0, 0
  474.     for len = 1, 15 do
  475.       code = code + bits(1)
  476.       local cnt = h.count[len]
  477.       if code - cnt < first then return h.symbol[index + (code - first)] end
  478.       index = index + cnt
  479.       first = (first + cnt) * 2
  480.       code = code * 2
  481.     end
  482.     error("inflate: bad huffman code")
  483.   end
  484.  
  485.   local function codes(lit, dst)
  486.     while true do
  487.       local sym = decode(lit)
  488.       if sym == nil then error("inflate: bad symbol") end
  489.       if sym < 256 then
  490.         put(sym)
  491.       elseif sym == 256 then
  492.         return
  493.       else
  494.         sym = sym - 256
  495.         if sym > 29 then error("inflate: bad length symbol") end
  496.         local len = LBASE[sym]
  497.         if LEXT[sym] > 0 then len = len + bits(LEXT[sym]) end
  498.         local ds = decode(dst)
  499.         if ds == nil or ds > 29 then error("inflate: bad distance symbol") end
  500.         ds = ds + 1
  501.         local dist = DBASE[ds]
  502.         if DEXT[ds] > 0 then dist = dist + bits(DEXT[ds]) end
  503.         for _ = 1, len do put(back(dist)) end
  504.       end
  505.       ops = ops + 1
  506.       if ops % 3000 == 0 then os.sleep(0) end
  507.     end
  508.   end
  509.  
  510.   local final
  511.   repeat
  512.     final = bits(1)
  513.     local typ = bits(2)
  514.     if typ == 0 then
  515.       bitbuf, bitcnt = 0, 0
  516.       local len = byte(data, pos) + byte(data, pos + 1) * 256
  517.       pos = pos + 4
  518.       for i = 0, len - 1 do put(byte(data, pos + i)) end
  519.       pos = pos + len
  520.     elseif typ == 1 then
  521.       if not fixedLit then
  522.         local l = {}
  523.         for i = 0, 143 do l[i] = 8 end
  524.         for i = 144, 255 do l[i] = 9 end
  525.         for i = 256, 279 do l[i] = 7 end
  526.         for i = 280, 287 do l[i] = 8 end
  527.         fixedLit = build(l, 288)
  528.         local d = {}
  529.         for i = 0, 29 do d[i] = 5 end
  530.         fixedDist = build(d, 30)
  531.       end
  532.       codes(fixedLit, fixedDist)
  533.     elseif typ == 2 then
  534.       local nlen = bits(5) + 257
  535.       local ndist = bits(5) + 1
  536.       local ncode = bits(4) + 4
  537.       local lens = {}
  538.       for i = 0, 18 do lens[i] = 0 end
  539.       for i = 1, ncode do lens[CLORDER[i]] = bits(3) end
  540.       local lencode = build(lens, 19)
  541.       local all, idx = {}, 0
  542.       while idx < nlen + ndist do
  543.         local sym = decode(lencode)
  544.         if sym < 16 then
  545.           all[idx] = sym
  546.           idx = idx + 1
  547.         else
  548.           local prev, rep = 0, 0
  549.           if sym == 16 then
  550.             prev = all[idx - 1] or 0
  551.             rep = 3 + bits(2)
  552.           elseif sym == 17 then
  553.             rep = 3 + bits(3)
  554.           else
  555.             rep = 11 + bits(7)
  556.           end
  557.           for _ = 1, rep do
  558.             all[idx] = prev
  559.             idx = idx + 1
  560.           end
  561.         end
  562.       end
  563.       local ll, dl = {}, {}
  564.       for i = 0, nlen - 1 do ll[i] = all[i] end
  565.       for i = 0, ndist - 1 do dl[i] = all[nlen + i] end
  566.       codes(build(ll, nlen), build(dl, ndist))
  567.     else
  568.       error("inflate: bad block type")
  569.     end
  570.   until final == 1
  571.   flush()
  572. end
  573.  
  574. local function be32(s, p)
  575.   local a, b, c, d = byte(s, p, p + 3)
  576.   return ((a * 256 + b) * 256 + c) * 256 + d
  577. end
  578. local function le16(s, p)
  579.   local a, b = byte(s, p, p + 1)
  580.   return a + b * 256
  581. end
  582. local function le32(s, p)
  583.   local a, b, c, d = byte(s, p, p + 3)
  584.   return a + b * 256 + c * 65536 + d * 16777216
  585. end
  586.  
  587. local function newGrid(W, H, cols, pixPerCell)
  588.   pixPerCell = pixPerCell or 4
  589.   cols = max(1, min(cols, floor(W / 2)))
  590.   local pw = cols * 2
  591.   local ph = max(4, ceil(pw * H / W / pixPerCell) * pixPerCell)
  592.   if ph > MAX_IMAGE_ROWS * pixPerCell then
  593.     ph = MAX_IMAGE_ROWS * pixPerCell
  594.     pw = max(2, 2 * floor(ph * W / H / 2 + 0.5))
  595.   end
  596.  
  597.   local g = { W = W, H = H, pw = pw, ph = ph, acc = {}, xmap = {}, xcnt = {}, ycnt = {} }
  598.   local xmap, xcnt, ycnt = g.xmap, g.xcnt, g.ycnt
  599.   for i = 1, pw do xcnt[i] = 0 end
  600.   for i = 1, ph do ycnt[i] = 0 end
  601.   for x = 1, W do
  602.     local c = floor((x - 1) * pw / W) + 1
  603.     xmap[x] = c
  604.     xcnt[c] = xcnt[c] + 1
  605.   end
  606.   local xmax = 0
  607.   for i = 1, pw do if xcnt[i] > xmax then xmax = xcnt[i] end end
  608.   local bound = xmax * ceil(H / ph)
  609.   local sh = 0
  610.   while bound * ((255 + ((1 << sh) >> 1)) >> sh) >= 2097152 do sh = sh + 1 end
  611.   g.sh = sh
  612.   g.hf = (1 << sh) >> 1
  613.   local acc = g.acc
  614.   for i = 1, pw * ph do acc[i] = 0 end
  615.   return g
  616. end
  617.  
  618. local function gridRow(g, y, rr, gg, bb)
  619.   local ry = floor(y * g.ph / g.H)
  620.   local ycnt = g.ycnt
  621.   ycnt[ry + 1] = ycnt[ry + 1] + 1
  622.   local base = ry * g.pw
  623.   local acc, xm, sh, hf = g.acc, g.xmap, g.sh, g.hf
  624.   for x = 1, g.W do
  625.     local k = base + xm[x]
  626.     local r = (floor(rr[x] + 0.5) + hf) >> sh
  627.     local gv = (floor(gg[x] + 0.5) + hf) >> sh
  628.     local b = (floor(bb[x] + 0.5) + hf) >> sh
  629.     acc[k] = acc[k] + (r * 4398046511104 + gv * 2097152 + b)
  630.   end
  631. end
  632.  
  633. local function finishGrid(g)
  634.   local pw, ph = g.pw, g.ph
  635.   local acc, xcnt, ycnt = g.acc, g.xcnt, g.ycnt
  636.   local scale = 1 << g.sh
  637.   g.acc, g.xmap = nil, nil
  638.   local M = 2097151
  639.  
  640.   local minr, ming, minb, maxr, maxg, maxb = 255, 255, 255, 0, 0, 0
  641.   local i = 0
  642.   for y = 1, ph do
  643.     local cy = ycnt[y]
  644.     for x = 1, pw do
  645.       i = i + 1
  646.       local c = cy * xcnt[x]
  647.       local r, gv, b = 255, 255, 255
  648.       if c > 0 then
  649.         local a = acc[i]
  650.         r = (a >> 42) * scale / c
  651.         gv = ((a >> 21) & M) * scale / c
  652.         b = (a & M) * scale / c
  653.       end
  654.       if r < minr then minr = r end
  655.       if gv < ming then ming = gv end
  656.       if b < minb then minb = b end
  657.       if r > maxr then maxr = r end
  658.       if gv > maxg then maxg = gv end
  659.       if b > maxb then maxb = b end
  660.     end
  661.   end
  662.   local dr, dg, db = maxr - minr, maxg - ming, maxb - minb
  663.  
  664.   local parts, pn = {}, 0
  665.   local chunk, cn = {}, 0
  666.   i = 0
  667.   for y = 1, ph do
  668.     local cy = ycnt[y]
  669.     for x = 1, pw do
  670.       i = i + 1
  671.       local c = cy * xcnt[x]
  672.       local r, gv, b = 255, 255, 255
  673.       if c > 0 then
  674.         local a = acc[i]
  675.         r = (a >> 42) * scale / c
  676.         gv = ((a >> 21) & M) * scale / c
  677.         b = (a & M) * scale / c
  678.       end
  679.       if dr > 8 then r = (r - minr) * 255 / dr end
  680.       if dg > 8 then gv = (gv - ming) * 255 / dg end
  681.       if db > 8 then b = (b - minb) * 255 / db end
  682.       r = floor(r + 0.5); if r < 0 then r = 0 elseif r > 255 then r = 255 end
  683.       gv = floor(gv + 0.5); if gv < 0 then gv = 0 elseif gv > 255 then gv = 255 end
  684.       b = floor(b + 0.5); if b < 0 then b = 0 elseif b > 255 then b = 255 end
  685.       cn = cn + 1
  686.       chunk[cn] = char(r, gv, b)
  687.       if cn >= 1000 then
  688.         pn = pn + 1
  689.         parts[pn] = table.concat(chunk, "", 1, cn)
  690.         cn = 0
  691.       end
  692.     end
  693.     if y % 32 == 0 then os.sleep(0) end
  694.   end
  695.   if cn > 0 then pn = pn + 1; parts[pn] = table.concat(chunk, "", 1, cn) end
  696.   return { pw = pw, ph = ph, rgb = table.concat(parts) }
  697. end
  698.  
  699. local function decodePNG(box, cols, pixPerCell)
  700.   local data = box[1]; box[1] = nil
  701.   if data:sub(1, 8) ~= "\137PNG\r\n\26\n" then return nil, "bad PNG" end
  702.   local pos = 9
  703.   local W, H, bd, ct, il
  704.   local pal = {}
  705.   local idat = {}
  706.   while pos <= #data - 7 do
  707.     local len = be32(data, pos)
  708.     local typ = data:sub(pos + 4, pos + 7)
  709.     local body = pos + 8
  710.     if typ == "IHDR" then
  711.       W, H = be32(data, body), be32(data, body + 4)
  712.       bd, ct, il = byte(data, body + 8), byte(data, body + 9), byte(data, body + 12)
  713.     elseif typ == "PLTE" then
  714.       for i = 0, floor(len / 3) - 1 do
  715.         local r, g, b = byte(data, body + i * 3, body + i * 3 + 2)
  716.         pal[i] = { r, g, b }
  717.       end
  718.     elseif typ == "IDAT" then
  719.       idat[#idat + 1] = data:sub(body, body + len - 1)
  720.     elseif typ == "IEND" then
  721.       break
  722.     end
  723.     pos = body + len + 4
  724.   end
  725.   data = nil
  726.   if not W or W < 1 or H < 1 then return nil, "bad PNG" end
  727.   if il ~= 0 then return nil, "interlaced PNG" end
  728.   if W * H > MAX_IMAGE_PIXELS then return nil, "image too large" end
  729.   local spp = ({ [0] = 1, [2] = 3, [3] = 1, [4] = 2, [6] = 4 })[ct]
  730.   if not spp then return nil, "bad PNG color type" end
  731.  
  732.   local bppBits = spp * bd
  733.   local rowbytes = ceil(W * bppBits / 8)
  734.   local fbpp = max(1, ceil(bppBits / 8))
  735.   local g = newGrid(W, H, cols, pixPerCell)
  736.   local prev = {}
  737.   for i = 1, rowbytes do prev[i] = 0 end
  738.   local rr, gg, bb = {}, {}, {}
  739.   local scale = bd < 8 and 255 / (2 ^ bd - 1) or 1
  740.  
  741.   local function sample(cur, s)
  742.     if bd == 8 then return cur[s + 1] end
  743.     if bd == 16 then return cur[2 * s + 1] end
  744.     local per = 8 / bd
  745.     local b = cur[floor(s / per) + 1]
  746.     local sh = 8 - bd * (s % per + 1)
  747.     return floor(b / 2 ^ sh) % (2 ^ bd)
  748.   end
  749.  
  750.   local function rowRGB(cur)
  751.     if bd == 8 then
  752.       if ct == 2 then
  753.         for x = 1, W do
  754.           local i = x * 3 - 2
  755.           rr[x], gg[x], bb[x] = cur[i], cur[i + 1], cur[i + 2]
  756.         end
  757.       elseif ct == 6 then
  758.         for x = 1, W do
  759.           local i = x * 4 - 3
  760.           local a = cur[i + 3] / 255
  761.           rr[x] = cur[i] * a + 255 * (1 - a)
  762.           gg[x] = cur[i + 1] * a + 255 * (1 - a)
  763.           bb[x] = cur[i + 2] * a + 255 * (1 - a)
  764.         end
  765.       elseif ct == 0 then
  766.         for x = 1, W do
  767.           local v = cur[x]
  768.           rr[x], gg[x], bb[x] = v, v, v
  769.         end
  770.       elseif ct == 4 then
  771.         for x = 1, W do
  772.           local a = cur[2 * x] / 255
  773.           local v = cur[2 * x - 1] * a + 255 * (1 - a)
  774.           rr[x], gg[x], bb[x] = v, v, v
  775.         end
  776.       else
  777.         for x = 1, W do
  778.           local p = pal[cur[x]]
  779.           if p then rr[x], gg[x], bb[x] = p[1], p[2], p[3] end
  780.         end
  781.       end
  782.       return
  783.     end
  784.     for x = 0, W - 1 do
  785.       local r, gc, b
  786.       if ct == 0 then
  787.         local v = sample(cur, x) * scale
  788.         r, gc, b = v, v, v
  789.       elseif ct == 3 then
  790.         local p = pal[sample(cur, x)]
  791.         if p then r, gc, b = p[1], p[2], p[3] else r, gc, b = 0, 0, 0 end
  792.       elseif ct == 2 then
  793.         r = sample(cur, 3 * x) * scale
  794.         gc = sample(cur, 3 * x + 1) * scale
  795.         b = sample(cur, 3 * x + 2) * scale
  796.       elseif ct == 4 then
  797.         local a = sample(cur, 2 * x + 1) / 255
  798.         local v = sample(cur, 2 * x) * scale * a + 255 * (1 - a)
  799.         r, gc, b = v, v, v
  800.       else
  801.         local a = sample(cur, 4 * x + 3) / 255
  802.         r = sample(cur, 4 * x) * scale * a + 255 * (1 - a)
  803.         gc = sample(cur, 4 * x + 1) * scale * a + 255 * (1 - a)
  804.         b = sample(cur, 4 * x + 2) * scale * a + 255 * (1 - a)
  805.       end
  806.       rr[x + 1], gg[x + 1], bb[x + 1] = r, gc, b
  807.     end
  808.   end
  809.  
  810.   local y, ops, buf = 0, 0, ""
  811.  
  812.   local function processRow(rs)
  813.     local ft = byte(rs, 1)
  814.     local cur = {}
  815.     for i = 2, rowbytes + 1, 200 do
  816.       local t = { byte(rs, i, min(i + 199, rowbytes + 1)) }
  817.       for j = 1, #t do cur[i + j - 2] = t[j] end
  818.     end
  819.     if ft == 1 then
  820.       for i = fbpp + 1, rowbytes do cur[i] = (cur[i] + cur[i - fbpp]) % 256 end
  821.     elseif ft == 2 then
  822.       for i = 1, rowbytes do cur[i] = (cur[i] + prev[i]) % 256 end
  823.     elseif ft == 3 then
  824.       for i = 1, rowbytes do
  825.         local a = i > fbpp and cur[i - fbpp] or 0
  826.         cur[i] = (cur[i] + floor((a + prev[i]) / 2)) % 256
  827.       end
  828.     elseif ft == 4 then
  829.       for i = 1, rowbytes do
  830.         local a = i > fbpp and cur[i - fbpp] or 0
  831.         local b = prev[i]
  832.         local c = i > fbpp and prev[i - fbpp] or 0
  833.         local p = a + b - c
  834.         local pa, pb, pc = abs(p - a), abs(p - b), abs(p - c)
  835.         local pr
  836.         if pa <= pb and pa <= pc then pr = a elseif pb <= pc then pr = b else pr = c end
  837.         cur[i] = (cur[i] + pr) % 256
  838.       end
  839.     end
  840.     rowRGB(cur)
  841.     gridRow(g, y, rr, gg, bb)
  842.     prev = cur
  843.     y = y + 1
  844.     ops = ops + W
  845.     if ops > 20000 then ops = 0; os.sleep(0) end
  846.   end
  847.  
  848.   local function sink(chunk)
  849.     buf = buf .. chunk
  850.     while y < H and #buf >= rowbytes + 1 do
  851.       processRow(buf:sub(1, rowbytes + 1))
  852.       buf = buf:sub(rowbytes + 2)
  853.     end
  854.   end
  855.  
  856.   local z = table.concat(idat)
  857.   idat = nil
  858.   inflate(z, 3, sink)
  859.   return finishGrid(g)
  860. end
  861.  
  862. local function decodeBMP(box, cols, pixPerCell)
  863.   local data = box[1]; box[1] = nil
  864.   if #data < 54 then return nil, "bad BMP" end
  865.   local off, hsz = le32(data, 11), le32(data, 15)
  866.   local W, H = le32(data, 19), le32(data, 23)
  867.   local bpp, comp = le16(data, 29), le32(data, 31)
  868.   if H >= 2 ^ 31 then H = H - 2 ^ 32 end
  869.   local top = H < 0
  870.   H = abs(H)
  871.   if comp ~= 0 then return nil, "compressed BMP" end
  872.   if not (bpp == 1 or bpp == 4 or bpp == 8 or bpp == 24 or bpp == 32) then return nil, "BMP depth " .. bpp end
  873.   if W < 1 or H < 1 then return nil, "bad BMP" end
  874.   if W * H > MAX_IMAGE_PIXELS then return nil, "image too large" end
  875.   local pal = {}
  876.   if bpp <= 8 then
  877.     local n = le32(data, 47)
  878.     if n == 0 then n = 2 ^ bpp end
  879.     for i = 0, n - 1 do
  880.       local b, g, r = byte(data, 15 + hsz + i * 4, 15 + hsz + i * 4 + 2)
  881.       pal[i] = { r, g, b }
  882.     end
  883.   end
  884.   local rowsize = floor((bpp * W + 31) / 32) * 4
  885.   local g = newGrid(W, H, cols, pixPerCell)
  886.   local rr, gg, bb = {}, {}, {}
  887.   local ops = 0
  888.   for y = 0, H - 1 do
  889.     local sy = top and y or (H - 1 - y)
  890.     local rp = off + sy * rowsize + 1
  891.     if bpp == 24 or bpp == 32 then
  892.       local step = bpp / 8
  893.       for x = 0, W - 1 do
  894.         local b, gg2, r = byte(data, rp + x * step, rp + x * step + 2)
  895.         rr[x + 1], gg[x + 1], bb[x + 1] = r, gg2, b
  896.       end
  897.     elseif bpp == 8 then
  898.       for x = 0, W - 1 do
  899.         local p = pal[byte(data, rp + x)]
  900.         if p then rr[x + 1], gg[x + 1], bb[x + 1] = p[1], p[2], p[3] end
  901.       end
  902.     else
  903.       local per = 8 / bpp
  904.       for x = 0, W - 1 do
  905.         local bt = byte(data, rp + floor(x / per))
  906.         local sh = 8 - bpp * (x % per + 1)
  907.         local p = pal[floor(bt / 2 ^ sh) % (2 ^ bpp)]
  908.         if p then rr[x + 1], gg[x + 1], bb[x + 1] = p[1], p[2], p[3] end
  909.       end
  910.     end
  911.     gridRow(g, y, rr, gg, bb)
  912.     ops = ops + W
  913.     if ops > 20000 then ops = 0; os.sleep(0) end
  914.   end
  915.   return finishGrid(g)
  916. end
  917.  
  918. local ZZ = {[0]=0,1,8,16,9,2,3,10,17,24,32,25,18,11,4,5,12,19,26,33,40,48,41,34,27,20,13,6,7,14,21,28,
  919.   35,42,49,56,57,50,43,36,29,22,15,23,30,37,44,51,58,59,52,45,38,31,39,46,53,60,61,54,47,55,62,63}
  920. local IDCT = {}
  921. for x = 0, 7 do
  922.   IDCT[x] = {}
  923.   for u = 0, 7 do
  924.     IDCT[x][u] = (u == 0 and math.sqrt(0.5) or 1) * 0.5 * math.cos((2 * x + 1) * u * math.pi / 16)
  925.   end
  926. end
  927.  
  928. local function idct(coef)
  929.   local tmp, out = {}, {}
  930.   for v = 0, 7 do
  931.     for x = 0, 7 do
  932.       local sum = 0
  933.       for u = 0, 7 do
  934.         local c = coef[v * 8 + u]
  935.         if c ~= 0 then sum = sum + IDCT[x][u] * c end
  936.       end
  937.       tmp[v * 8 + x] = sum
  938.     end
  939.   end
  940.   for x = 0, 7 do
  941.     for y = 0, 7 do
  942.       local sum = 0
  943.       for v = 0, 7 do sum = sum + IDCT[y][v] * tmp[v * 8 + x] end
  944.       out[y * 8 + x] = sum + 128
  945.     end
  946.   end
  947.   return out
  948. end
  949.  
  950. local function decodeJPEG(box, cols, pixPerCell)
  951.   local data = box[1]; box[1] = nil
  952.   if data:sub(1, 2) ~= "\255\216" then return nil, "bad JPEG" end
  953.   local pos = 3
  954.   local qt, hdc, hac = {}, {}, {}
  955.   local W, H, comps, ri = nil, nil, {}, 0
  956.   local scanStart, sosComps
  957.   while pos < #data do
  958.     if byte(data, pos) ~= 255 then
  959.       pos = pos + 1
  960.     else
  961.       local m = byte(data, pos + 1)
  962.       if m == 255 or m == 0 then
  963.         pos = pos + 1
  964.       elseif m == 0xD8 or m == 1 or (m >= 0xD0 and m <= 0xD7) then
  965.         pos = pos + 2
  966.       elseif m == 0xD9 then
  967.         break
  968.       else
  969.         local len = byte(data, pos + 2) * 256 + byte(data, pos + 3)
  970.         local s, e = pos + 4, pos + 1 + len
  971.         if m == 0xDB then
  972.           local p = s
  973.           while p < e do
  974.             local pq = byte(data, p)
  975.             local prec, id = floor(pq / 16), pq % 16
  976.             p = p + 1
  977.             local t = {}
  978.             for k = 0, 63 do
  979.               if prec == 0 then t[k] = byte(data, p + k)
  980.               else t[k] = byte(data, p + k) * 256 + byte(data, p + k + 1) end
  981.             end
  982.             p = p + (prec == 0 and 64 or 128)
  983.             qt[id] = t
  984.           end
  985.         elseif m == 0xC4 then
  986.           local p = s
  987.           while p < e do
  988.             local tc = byte(data, p)
  989.             local cls, id = floor(tc / 16), tc % 16
  990.             p = p + 1
  991.             local counts, total = {}, 0
  992.             for i = 1, 16 do counts[i] = byte(data, p + i - 1); total = total + counts[i] end
  993.             p = p + 16
  994.             local sym = {}
  995.             for i = 0, total - 1 do sym[i] = byte(data, p + i) end
  996.             p = p + total
  997.             local h = { count = counts, symbol = sym }
  998.             if cls == 0 then hdc[id] = h else hac[id] = h end
  999.           end
  1000.         elseif m == 0xC0 or m == 0xC1 then
  1001.           if byte(data, s) ~= 8 then return nil, "JPEG precision" end
  1002.           H = byte(data, s + 1) * 256 + byte(data, s + 2)
  1003.           W = byte(data, s + 3) * 256 + byte(data, s + 4)
  1004.           for i = 0, byte(data, s + 5) - 1 do
  1005.             local id, hv, tq = byte(data, s + 6 + i * 3, s + 8 + i * 3)
  1006.             comps[#comps + 1] = { id = id, h = floor(hv / 16), v = hv % 16, tq = tq, pred = 0 }
  1007.           end
  1008.         elseif m == 0xC2 or (m >= 0xC3 and m <= 0xCF and m ~= 0xC4 and m ~= 0xC8 and m ~= 0xCC) then
  1009.           return nil, "progressive JPEG"
  1010.         elseif m == 0xDD then
  1011.           ri = byte(data, s) * 256 + byte(data, s + 1)
  1012.         elseif m == 0xDA then
  1013.           sosComps = {}
  1014.           for i = 0, byte(data, s) - 1 do
  1015.             local cid, tt = byte(data, s + 1 + i * 2, s + 2 + i * 2)
  1016.             sosComps[#sosComps + 1] = { id = cid, dc = floor(tt / 16), ac = tt % 16 }
  1017.           end
  1018.           scanStart = e + 1
  1019.           break
  1020.         end
  1021.         pos = e + 1
  1022.       end
  1023.     end
  1024.   end
  1025.   local nc = #comps
  1026.   if not (W and scanStart and (nc == 1 or nc == 3)) or #sosComps ~= nc then return nil, "unsupported JPEG" end
  1027.   if W * H > 4000000 then return nil, "image too large" end
  1028.   local Hmax, Vmax = 1, 1
  1029.   for _, c in ipairs(comps) do Hmax = max(Hmax, c.h); Vmax = max(Vmax, c.v) end
  1030.   if nc == 1 then comps[1].h, comps[1].v, Hmax, Vmax = 1, 1, 1, 1 end
  1031.   local mx, my = ceil(W / (8 * Hmax)), ceil(H / (8 * Vmax))
  1032.   local yc = comps[1]
  1033.   local blocksY_X = mx * yc.h
  1034.   local blocksY_Y = my * yc.v
  1035.   local full = (W * H <= 40000)
  1036.  
  1037.   local plane = {}
  1038.   for ci = 1, nc do
  1039.     local c = comps[ci]
  1040.     local bx = mx * c.h
  1041.     local by = my * c.v
  1042.     local rows = full and (by * 8) or by
  1043.     local p = {}
  1044.     for r = 1, rows do p[r] = {} end
  1045.     c.plane = p
  1046.   end
  1047.  
  1048.   local segs = {}
  1049.   do
  1050.     local p, cur = scanStart, {}
  1051.     while true do
  1052.       local f = data:find("\255", p, true)
  1053.       if not f then cur[#cur + 1] = data:sub(p); break end
  1054.       cur[#cur + 1] = data:sub(p, f - 1)
  1055.       local c = byte(data, f + 1)
  1056.       if c == 0 then
  1057.         cur[#cur + 1] = "\255"; p = f + 2
  1058.       elseif c and c >= 0xD0 and c <= 0xD7 then
  1059.         segs[#segs + 1] = table.concat(cur); cur = {}; p = f + 2
  1060.       else
  1061.         break
  1062.       end
  1063.     end
  1064.     segs[#segs + 1] = table.concat(cur)
  1065.   end
  1066.   data = nil
  1067.  
  1068.   local sd, sp, bitcnt, curb = "", 1, 0, 0
  1069.   local function getbit()
  1070.     if bitcnt == 0 then curb = byte(sd, sp) or 0; sp = sp + 1; bitcnt = 8 end
  1071.     bitcnt = bitcnt - 1
  1072.     return band(rshift(curb, bitcnt), 1)
  1073.   end
  1074.   local function receive(n)
  1075.     local v = 0
  1076.     for _ = 1, n do v = v * 2 + getbit() end
  1077.     return v
  1078.   end
  1079.   local function extend(v, n)
  1080.     if v < lshift(1, n - 1) then return v - lshift(1, n) + 1 end
  1081.     return v
  1082.   end
  1083.   local function decodeSym(h)
  1084.     local code, first, index = 0, 0, 0
  1085.     for len = 1, 16 do
  1086.       code = code + getbit()
  1087.       local cnt = h.count[len]
  1088.       if code - cnt < first then return h.symbol[index + (code - first)] end
  1089.       index = index + cnt
  1090.       first = (first + cnt) * 2
  1091.       code = code * 2
  1092.     end
  1093.     error("bad JPEG huffman code")
  1094.   end
  1095.  
  1096.   local total = mx * my
  1097.   local per = ri > 0 and ri or total
  1098.   local mcu = 0
  1099.   for si = 1, #segs do
  1100.     sd, sp, bitcnt = segs[si], 1, 0
  1101.     for _, c in ipairs(comps) do c.pred = 0 end
  1102.     for _ = 1, per do
  1103.       if mcu >= total then break end
  1104.       local mxi, myi = mcu % mx, floor(mcu / mx)
  1105.       for ci, c in ipairs(comps) do
  1106.         local dcT, acT = hdc[sosComps[ci].dc], hac[sosComps[ci].ac]
  1107.         local q = qt[c.tq]
  1108.         for by = 0, c.v - 1 do
  1109.           for bx = 0, c.h - 1 do
  1110.             local t = decodeSym(dcT)
  1111.             if t > 0 then c.pred = c.pred + extend(receive(t), t) end
  1112.             local coef
  1113.             if full then
  1114.               coef = {}
  1115.               for i = 0, 63 do coef[i] = 0 end
  1116.               coef[0] = c.pred * q[0]
  1117.             end
  1118.             local k = 1
  1119.             while k < 64 do
  1120.               local rs = decodeSym(acT)
  1121.               local r, sz = floor(rs / 16), rs % 16
  1122.               if sz == 0 then
  1123.                 if r == 15 then k = k + 16 else break end
  1124.               else
  1125.                 k = k + r
  1126.                 local v = extend(receive(sz), sz)
  1127.                 if coef and k < 64 then coef[ZZ[k]] = v * q[k] end
  1128.                 k = k + 1
  1129.               end
  1130.             end
  1131.             local gx, gy = mxi * c.h + bx, myi * c.v + by
  1132.             if full then
  1133.               local o = idct(coef)
  1134.               for yy = 0, 7 do
  1135.                 local row = c.plane[gy * 8 + yy + 1]
  1136.                 for xx = 0, 7 do row[gx * 8 + xx + 1] = o[yy * 8 + xx] end
  1137.               end
  1138.             else
  1139.               c.plane[gy + 1][gx + 1] = c.pred * q[0] / 8 + 128
  1140.             end
  1141.           end
  1142.         end
  1143.       end
  1144.       mcu = mcu + 1
  1145.       if mcu % 64 == 0 then os.sleep(0) end
  1146.     end
  1147.   end
  1148.  
  1149.   local Ws, Hs = W, H
  1150.   local fullW, fullH
  1151.   if full then
  1152.     fullW = mx * yc.h * 8
  1153.     fullH = my * yc.v * 8
  1154.   else
  1155.     Ws, Hs = ceil(W / 8), ceil(H / 8)
  1156.     fullW = mx * yc.h
  1157.     fullH = my * yc.v
  1158.   end
  1159.  
  1160.   local g = newGrid(Ws, Hs, cols, pixPerCell)
  1161.   local rr, gg, bb = {}, {}, {}
  1162.  
  1163.   local function samplePlane(c, x, y, planeW, planeH)
  1164.     local sx = floor(x * c.h * (full and 8 or 1) / (yc.h * (full and 8 or 1)))
  1165.     local sy = floor(y * c.v * (full and 8 or 1) / (yc.v * (full and 8 or 1)))
  1166.     if sx < 0 then sx = 0 elseif sx >= planeW then sx = planeW - 1 end
  1167.     if sy < 0 then sy = 0 elseif sy >= planeH then sy = planeH - 1 end
  1168.     local row = c.plane[sy + 1]
  1169.     if not row then return 128 end
  1170.     local v = row[sx + 1]
  1171.     if v == nil then return 128 end
  1172.     return v
  1173.   end
  1174.  
  1175.   local yPlaneW = full and (mx * yc.h * 8) or (mx * yc.h)
  1176.   local yPlaneH = full and (my * yc.v * 8) or (my * yc.v)
  1177.   local cbPlaneW, cbPlaneH, crPlaneW, crPlaneH
  1178.   if nc == 3 then
  1179.     local cb = comps[2]
  1180.     local cr = comps[3]
  1181.     cbPlaneW = full and (mx * cb.h * 8) or (mx * cb.h)
  1182.     cbPlaneH = full and (my * cb.v * 8) or (my * cb.v)
  1183.     crPlaneW = full and (mx * cr.h * 8) or (mx * cr.h)
  1184.     crPlaneH = full and (my * cr.v * 8) or (my * cr.v)
  1185.   end
  1186.  
  1187.   for y = 0, Hs - 1 do
  1188.     for x = 0, Ws - 1 do
  1189.       local Y, Cb, Cr
  1190.       if full then
  1191.         local yrow = yc.plane[y + 1]
  1192.         Y = yrow and yrow[x + 1] or 128
  1193.       else
  1194.         local yrow = yc.plane[y + 1]
  1195.         Y = yrow and yrow[x + 1] or 128
  1196.       end
  1197.       if nc == 3 then
  1198.         Cb = samplePlane(comps[2], x, y, cbPlaneW, cbPlaneH)
  1199.         Cr = samplePlane(comps[3], x, y, crPlaneW, crPlaneH)
  1200.       else
  1201.         Cb, Cr = 128, 128
  1202.       end
  1203.       local r = Y + 1.402 * (Cr - 128)
  1204.       local gc = Y - 0.344136 * (Cb - 128) - 0.714136 * (Cr - 128)
  1205.       local b = Y + 1.772 * (Cb - 128)
  1206.       if r < 0 then r = 0 elseif r > 255 then r = 255 end
  1207.       if gc < 0 then gc = 0 elseif gc > 255 then gc = 255 end
  1208.       if b < 0 then b = 0 elseif b > 255 then b = 255 end
  1209.       rr[x + 1], gg[x + 1], bb[x + 1] = r, gc, b
  1210.     end
  1211.     gridRow(g, y, rr, gg, bb)
  1212.     if y % 32 == 31 then os.sleep(0) end
  1213.   end
  1214.  
  1215.   for ci = 1, nc do comps[ci].plane = nil end
  1216.   return finishGrid(g)
  1217. end
  1218.  
  1219. local function gifReadPalette(data, pos, count)
  1220.   local pal = {}
  1221.   for i = 0, count - 1 do
  1222.     local r, g, b = byte(data, pos, pos + 2)
  1223.     if not r or not g or not b then
  1224.       return nil, pos, "truncated GIF color table"
  1225.     end
  1226.     pal[i] = { r, g, b }
  1227.     pos = pos + 3
  1228.   end
  1229.   return pal, pos
  1230. end
  1231.  
  1232. local function gifSkipSubBlocks(data, pos)
  1233.   while true do
  1234.     local n = byte(data, pos)
  1235.     if not n then return nil, "truncated GIF sub-blocks" end
  1236.     pos = pos + 1
  1237.     if n == 0 then return pos end
  1238.     if pos + n - 1 > #data then
  1239.       return nil, "truncated GIF sub-block"
  1240.     end
  1241.     pos = pos + n
  1242.   end
  1243. end
  1244.  
  1245. local function gifReadSubBlocks(data, pos)
  1246.   local chunks = {}
  1247.   while true do
  1248.     local n = byte(data, pos)
  1249.     if not n then return nil, "truncated GIF sub-blocks" end
  1250.     pos = pos + 1
  1251.     if n == 0 then break end
  1252.     if pos + n - 1 > #data then
  1253.       return nil, "truncated GIF sub-block"
  1254.     end
  1255.     chunks[#chunks + 1] = data:sub(pos, pos + n - 1)
  1256.     pos = pos + n
  1257.   end
  1258.   return table.concat(chunks), pos
  1259. end
  1260.  
  1261. local function gifLZW(data, minCodeSize, expected)
  1262.   if minCodeSize < 2 or minCodeSize > 11 then
  1263.     return nil, "unsupported GIF LZW code size"
  1264.   end
  1265.  
  1266.   local clear = 1 << minCodeSize
  1267.   local eoi = clear + 1
  1268.   local nextCode = clear + 2
  1269.   local codeSize = minCodeSize + 1
  1270.  
  1271.   local prefix, suffix = {}, {}
  1272.   for i = 0, clear - 1 do
  1273.     prefix[i] = -1
  1274.     suffix[i] = i
  1275.   end
  1276.  
  1277.   local chunks, buf, bn, outn = {}, {}, 0, 0
  1278.   local stack = {}
  1279.   local bitPos = 0
  1280.   local totalBits = #data * 8
  1281.  
  1282.   local function readCode()
  1283.     if bitPos + codeSize > totalBits then return nil end
  1284.     local value = 0
  1285.     for i = 0, codeSize - 1 do
  1286.       local p = bitPos + i
  1287.       local b = byte(data, floor(p / 8) + 1)
  1288.       local bit = band(rshift(b, p % 8), 1)
  1289.       value = value + lshift(bit, i)
  1290.     end
  1291.     bitPos = bitPos + codeSize
  1292.     return value
  1293.   end
  1294.  
  1295.   local function put(c)
  1296.     bn = bn + 1
  1297.     buf[bn] = c
  1298.     outn = outn + 1
  1299.     if bn >= 2048 then
  1300.       chunks[#chunks + 1] = char(unpack(buf, 1, bn))
  1301.       bn = 0
  1302.     end
  1303.   end
  1304.  
  1305.   local function emitString(code)
  1306.     local n = 0
  1307.     while code >= clear do
  1308.       local p = prefix[code]
  1309.       if p == nil then return nil end
  1310.       n = n + 1
  1311.       stack[n] = suffix[code]
  1312.       code = p
  1313.     end
  1314.     n = n + 1
  1315.     stack[n] = code
  1316.     if outn + n > expected then return nil end
  1317.     for i = n, 1, -1 do put(stack[i]) end
  1318.     return code
  1319.   end
  1320.  
  1321.   local prev = nil
  1322.  
  1323.   while outn < expected do
  1324.     local code = readCode()
  1325.     if code == nil then break end
  1326.  
  1327.     if code == clear then
  1328.       nextCode = clear + 2
  1329.       codeSize = minCodeSize + 1
  1330.       prev = nil
  1331.  
  1332.     elseif code == eoi then
  1333.       break
  1334.  
  1335.     else
  1336.       if code > nextCode then
  1337.         return nil, "bad GIF LZW code"
  1338.       end
  1339.  
  1340.       if code == nextCode then
  1341.         if prev == nil then
  1342.           return nil, "bad GIF LZW first code"
  1343.         end
  1344.         local first = emitString(prev)
  1345.         if not first then
  1346.           return nil, "GIF image data too large"
  1347.         end
  1348.         if outn + 1 > expected then
  1349.           return nil, "GIF image data too large"
  1350.         end
  1351.         put(first)
  1352.  
  1353.         if nextCode < 4096 then
  1354.           prefix[nextCode] = prev
  1355.           suffix[nextCode] = first
  1356.           nextCode = nextCode + 1
  1357.           if nextCode == (1 << codeSize) and codeSize < 12 then
  1358.             codeSize = codeSize + 1
  1359.           end
  1360.         end
  1361.  
  1362.       else
  1363.         local first = emitString(code)
  1364.         if not first then
  1365.           return nil, "GIF image data too large"
  1366.         end
  1367.  
  1368.         if prev ~= nil and nextCode < 4096 then
  1369.           prefix[nextCode] = prev
  1370.           suffix[nextCode] = first
  1371.           nextCode = nextCode + 1
  1372.           if nextCode == (1 << codeSize) and codeSize < 12 then
  1373.             codeSize = codeSize + 1
  1374.           end
  1375.         end
  1376.       end
  1377.  
  1378.       prev = code
  1379.     end
  1380.  
  1381.     if outn % 20000 == 0 then
  1382.       os.sleep(0)
  1383.     end
  1384.   end
  1385.  
  1386.   if bn > 0 then chunks[#chunks + 1] = char(unpack(buf, 1, bn)) end
  1387.   if outn < expected then
  1388.     return nil, "truncated GIF image data"
  1389.   end
  1390.   return table.concat(chunks)
  1391. end
  1392.  
  1393. local function decodeGIF(box, cols, pixPerCell)
  1394.   local data = box[1]; box[1] = nil
  1395.   local sig = data:sub(1, 6)
  1396.   if sig ~= "GIF87a" and sig ~= "GIF89a" then
  1397.     return nil, "bad GIF"
  1398.   end
  1399.   if #data < 13 then
  1400.     return nil, "truncated GIF"
  1401.   end
  1402.  
  1403.   local W = le16(data, 7)
  1404.   local H = le16(data, 9)
  1405.   if W < 1 or H < 1 then return nil, "bad GIF dimensions" end
  1406.   if W * H > MAX_IMAGE_PIXELS then return nil, "image too large" end
  1407.  
  1408.   local packed = byte(data, 11)
  1409.   local hasGlobalPalette = band(packed, 0x80) ~= 0
  1410.   local globalPaletteSize = 2 ^ (band(packed, 0x07) + 1)
  1411.  
  1412.   local pos = 14
  1413.   local globalPalette
  1414.   if hasGlobalPalette then
  1415.     globalPalette, pos = gifReadPalette(data, pos, globalPaletteSize)
  1416.     if not globalPalette then return nil, "bad GIF global palette" end
  1417.   end
  1418.  
  1419.   local transparentIndex = nil
  1420.  
  1421.   while pos <= #data do
  1422.     local block = byte(data, pos)
  1423.     pos = pos + 1
  1424.  
  1425.     if block == 0x3B then
  1426.       return nil, "GIF contains no image frame"
  1427.     end
  1428.  
  1429.     if block == 0x21 then
  1430.       local label = byte(data, pos)
  1431.       pos = pos + 1
  1432.  
  1433.       if label == 0xF9 then
  1434.         local size = byte(data, pos)
  1435.         pos = pos + 1
  1436.         if size ~= 4 or pos + 4 > #data then
  1437.           return nil, "bad GIF graphic control extension"
  1438.         end
  1439.         local gcePacked = byte(data, pos)
  1440.         transparentIndex = nil
  1441.         if band(gcePacked, 1) ~= 0 then
  1442.           transparentIndex = byte(data, pos + 3)
  1443.         end
  1444.         pos = pos + 4
  1445.         if byte(data, pos) ~= 0 then
  1446.           return nil, "bad GIF graphic control terminator"
  1447.         end
  1448.         pos = pos + 1
  1449.       else
  1450.         local newPos, err = gifSkipSubBlocks(data, pos)
  1451.         if not newPos then return nil, err end
  1452.         pos = newPos
  1453.       end
  1454.  
  1455.     elseif block == 0x2C then
  1456.       if pos + 9 > #data then
  1457.         return nil, "truncated GIF image descriptor"
  1458.       end
  1459.       local fw = le16(data, pos + 4)
  1460.       local fh = le16(data, pos + 6)
  1461.       local ipacked = byte(data, pos + 8)
  1462.       pos = pos + 9
  1463.  
  1464.       if fw < 1 or fh < 1 then return nil, "bad GIF frame dimensions" end
  1465.       if fw * fh > MAX_IMAGE_PIXELS then return nil, "GIF frame too large" end
  1466.  
  1467.       local hasLocalPalette = band(ipacked, 0x80) ~= 0
  1468.       local interlaced = band(ipacked, 0x40) ~= 0
  1469.       local localPaletteSize = 2 ^ (band(ipacked, 0x07) + 1)
  1470.  
  1471.       local palette = globalPalette
  1472.       if hasLocalPalette then
  1473.         palette, pos = gifReadPalette(data, pos, localPaletteSize)
  1474.         if not palette then return nil, "bad GIF local palette" end
  1475.       end
  1476.       if not palette then return nil, "GIF has no color palette" end
  1477.  
  1478.       local minCodeSize = byte(data, pos)
  1479.       pos = pos + 1
  1480.  
  1481.       local compressed, newPos = gifReadSubBlocks(data, pos)
  1482.       if not compressed then return nil, newPos end
  1483.       data = nil
  1484.  
  1485.       local expected = fw * fh
  1486.       local pixels, err = gifLZW(compressed, minCodeSize, expected)
  1487.       compressed = nil
  1488.       if not pixels then return nil, err end
  1489.  
  1490.       local g = newGrid(fw, fh, cols, pixPerCell)
  1491.       local rr, gg, bb = {}, {}, {}
  1492.  
  1493.       local function renderRow(srcY, pixelOffset)
  1494.         for x = 1, fw do
  1495.           local idx = byte(pixels, pixelOffset + x - 1)
  1496.           if idx == transparentIndex then
  1497.             rr[x], gg[x], bb[x] = 255, 255, 255
  1498.           else
  1499.             local p = palette[idx]
  1500.             if p then rr[x], gg[x], bb[x] = p[1], p[2], p[3] end
  1501.           end
  1502.         end
  1503.         gridRow(g, srcY, rr, gg, bb)
  1504.       end
  1505.  
  1506.       if interlaced then
  1507.         local passes = {
  1508.           { start = 0, step = 8 },
  1509.           { start = 4, step = 8 },
  1510.           { start = 2, step = 4 },
  1511.           { start = 1, step = 2 },
  1512.         }
  1513.         local sourceRow = 0
  1514.         for _, pass in ipairs(passes) do
  1515.           for y = pass.start, fh - 1, pass.step do
  1516.             renderRow(y, sourceRow * fw + 1)
  1517.             sourceRow = sourceRow + 1            if sourceRow % 32 == 0 then os.sleep(0) end
  1518.           end
  1519.         end
  1520.       else
  1521.         for y = 0, fh - 1 do
  1522.           renderRow(y, y * fw + 1)
  1523.           if y % 32 == 31 then os.sleep(0) end
  1524.         end
  1525.       end
  1526.  
  1527.       return finishGrid(g)
  1528.     end
  1529.   end
  1530.  
  1531.   return nil, "GIF contains no image frame"
  1532. end
  1533.  
  1534. local CACHE_DIRS = { "/home/.ocweb-cache", "/var/ocweb-cache", "/usr/.ocweb-cache" }
  1535. local MAX_CACHE_BYTES = 32 * 1024 * 1024
  1536. local MAX_MEM_CACHE_BYTES = 128 * 1024
  1537. local CACHE_FREE_MARGIN = 128 * 1024
  1538. local IO_CHUNK = 2048
  1539.  
  1540. local imgstore = {}
  1541. do
  1542.   local dir, proxy, dirTried = nil, nil, false
  1543.   local curFile, seq = nil, 0
  1544.   local cache, cacheMem = {}, 0
  1545.   local saved = nil
  1546.   local tmpAddr
  1547.   pcall(function() tmpAddr = computer.tmpAddress() end)
  1548.  
  1549.   local function tryDir(path)
  1550.     local ok, p = pcall(function()
  1551.       if not filesystem.exists(path) then
  1552.         filesystem.makeDirectory(path)
  1553.       end
  1554.       if not filesystem.isDirectory(path) then return nil end
  1555.       local fsp = filesystem.get(path)
  1556.       if not fsp or fsp.isReadOnly() then return nil end
  1557.       if tmpAddr and fsp.address == tmpAddr then return nil end
  1558.       return fsp
  1559.     end)
  1560.     if ok then return p end
  1561.   end
  1562.  
  1563.   local function ensureDir()
  1564.     if dir then return true end
  1565.     if dirTried then return false end
  1566.     dirTried = true
  1567.     for _, path in ipairs(CACHE_DIRS) do
  1568.       local p = tryDir(path)
  1569.       if p then
  1570.         dir, proxy = path, p
  1571.         pcall(function()
  1572.           for name in filesystem.list(dir) do
  1573.             if name:match("%.bin$") then filesystem.remove(dir .. "/" .. name) end
  1574.           end
  1575.         end)
  1576.         return true
  1577.       end
  1578.     end
  1579.     return false
  1580.   end
  1581.  
  1582.   local function freeSpace()
  1583.     if not proxy then return 0 end
  1584.     local ok, t, u = pcall(function() return proxy.spaceTotal(), proxy.spaceUsed() end)
  1585.     if ok and type(t) == "number" and type(u) == "number" then return t - u end
  1586.     return 0
  1587.   end
  1588.  
  1589.   local function newFile()
  1590.     if not ensureDir() then return nil end
  1591.     seq = seq + 1
  1592.     local path = dir .. "/c" .. seq .. ".bin"
  1593.     local ok, h = pcall(filesystem.open, path, "wb")
  1594.     if not ok or not h then return nil end
  1595.     h:close()
  1596.     return { path = path, size = 0 }
  1597.   end
  1598.  
  1599.   local function dropFile(f)
  1600.     if not f then return end
  1601.     if f.rh then pcall(f.rh.close, f.rh); f.rh = nil end
  1602.     pcall(filesystem.remove, f.path)
  1603.   end
  1604.  
  1605.   local function append(f, data)
  1606.     local ok, off = pcall(function()
  1607.       if freeSpace() < #data + CACHE_FREE_MARGIN then return nil end
  1608.       local off = filesystem.size(f.path)
  1609.       local h = filesystem.open(f.path, "ab")
  1610.       if not h then return nil end
  1611.       local good = true
  1612.       for i = 1, #data, 4096 do
  1613.         if not h:write(data:sub(i, i + 4095)) then good = false; break end
  1614.       end
  1615.       h:close()
  1616.       if not good then return nil end
  1617.       f.size = off + #data
  1618.       return off
  1619.     end)
  1620.     if ok then return off end
  1621.   end
  1622.  
  1623.   local function readFile(f, off, len)
  1624.     local ok, res = pcall(function()
  1625.       local h = f.rh
  1626.       if not h then
  1627.         h = filesystem.open(f.path, "rb")
  1628.         if not h then return nil end
  1629.         f.rh = h
  1630.       end
  1631.       if not h:seek("set", off) then return nil end
  1632.       local parts, n, got = {}, 0, 0
  1633.       while got < len do
  1634.         local d = h:read(min(len - got, IO_CHUNK))
  1635.         if not d or d == "" then break end
  1636.         n = n + 1
  1637.         parts[n] = d
  1638.         got = got + #d
  1639.       end
  1640.       return table.concat(parts)
  1641.     end)
  1642.     if ok then return res end
  1643.     if f.rh then pcall(f.rh.close, f.rh); f.rh = nil end
  1644.   end
  1645.  
  1646.   function imgstore.put(img)
  1647.     local rec = { pw = img.pw, ph = img.ph }
  1648.     local data = img.rgb
  1649.     if not curFile then curFile = newFile() end
  1650.     if curFile then
  1651.       local off = append(curFile, data)
  1652.       if off then
  1653.         rec.f, rec.a = curFile, off
  1654.         return rec
  1655.       end
  1656.     end
  1657.     rec.s = data
  1658.     cacheMem = cacheMem + #data
  1659.     return rec
  1660.   end
  1661.  
  1662.   function imgstore.read(rec, off, len)
  1663.     local s
  1664.     if rec.s then
  1665.       s = rec.s:sub(off + 1, off + len)
  1666.     else
  1667.       s = readFile(rec.f, rec.a + off, len)
  1668.     end
  1669.     s = s or ""
  1670.     if #s < len then s = s .. string.rep("\0", len - #s) end
  1671.     return s
  1672.   end
  1673.  
  1674.   function imgstore.get(key) return cache[key] end
  1675.   function imgstore.set(key, v) cache[key] = v end
  1676.  
  1677.   function imgstore.begin()
  1678.     saved = { cache = cache, mem = cacheMem, file = curFile }
  1679.     if (curFile and curFile.size > MAX_CACHE_BYTES) or cacheMem > MAX_MEM_CACHE_BYTES then
  1680.       cache, cacheMem, curFile = {}, 0, nil
  1681.     end
  1682.   end
  1683.  
  1684.   function imgstore.commit()
  1685.     if saved and saved.file and saved.file ~= curFile then dropFile(saved.file) end
  1686.     saved = nil
  1687.   end
  1688.  
  1689.   function imgstore.abort()
  1690.     if not saved then return end
  1691.     if curFile and curFile ~= saved.file then dropFile(curFile) end
  1692.     cache, cacheMem, curFile = saved.cache, saved.mem, saved.file
  1693.     saved = nil
  1694.   end
  1695.  
  1696.   function imgstore.shutdown()
  1697.     if saved and saved.file and saved.file ~= curFile then dropFile(saved.file) end
  1698.     dropFile(curFile)
  1699.     curFile, saved, cache, cacheMem = nil, nil, {}, 0
  1700.     if dir then pcall(filesystem.remove, dir) end
  1701.   end
  1702. end
  1703.  
  1704. local function loadImage(url, cols, pixPerCell)
  1705.   local key = url .. "|" .. cols .. "|" .. pixPerCell
  1706.   local hit = imgstore.get(key)
  1707.   if hit then
  1708.     if hit.err then return nil, hit.err end
  1709.     return hit.img
  1710.   end
  1711.   local function fail(msg)
  1712.     imgstore.set(key, { err = msg })
  1713.     return nil, msg
  1714.   end
  1715.   local data, err = httpFollow(url, IMG_HEADERS, MAX_IMAGE_BYTES)
  1716.   if not data then return fail(err) end
  1717.  
  1718.   local head8 = data:sub(1, 8)
  1719.   local head2 = head8:sub(1, 2)
  1720.   local head6 = head8:sub(1, 6)
  1721.   local decoder
  1722.   if head8 == "\137PNG\r\n\26\n" then
  1723.     decoder = decodePNG
  1724.   elseif head2 == "BM" then
  1725.     decoder = decodeBMP
  1726.   elseif head2 == "\255\216" then
  1727.     decoder = decodeJPEG
  1728.   elseif head6 == "GIF87a" or head6 == "GIF89a" then
  1729.     decoder = decodeGIF
  1730.   else
  1731.     return fail("unknown image format")
  1732.   end
  1733.  
  1734.   local box = { data }
  1735.   data = nil
  1736.   local ok, img, e = pcall(decoder, box, cols, pixPerCell)
  1737.   box = nil
  1738.   if not ok then return fail("decode error: " .. tostring(img)) end
  1739.   if not img then return fail(e or "decode failed") end
  1740.   local rec = imgstore.put(img)
  1741.   img = nil
  1742.   imgstore.set(key, { img = rec })
  1743.   return rec
  1744. end
  1745.  
  1746. local function parseCSS(css, rules)
  1747.   css = css:gsub("/%*.-%*/", ""):gsub("<!%-%-", ""):gsub("%-%->", "")
  1748.   css = css:gsub("@[^{;]*%b{}", ""):gsub("@[^;{]*;", "")
  1749.   for sels, body in css:gmatch("([^{}]+)%{([^{}]*)%}") do
  1750.     local props = {}
  1751.     for k, v in body:gmatch("([%w%-]+)%s*:%s*([^;]+)") do
  1752.       props[#props + 1] = { k:lower(), trim(v) }
  1753.     end
  1754.     for rawSel in sels:gmatch("[^,]+") do
  1755.       local sel = trim(rawSel)
  1756.       if sel ~= "" and sel:match("^[%w%.#_%-%*]+$") then
  1757.         local tag = (sel:match("^(%a[%w]*)") or "*"):lower()
  1758.         local id = sel:match("#([%w_%-]+)")
  1759.         local classes = {}
  1760.         for c in sel:gmatch("%.([%w_%-]+)") do classes[#classes + 1] = c end
  1761.         rules[#rules + 1] = {
  1762.           tag = tag, id = id, classes = classes, props = props, idx = #rules + 1,
  1763.           spec = (id and 100 or 0) + #classes * 10 + (tag ~= "*" and 1 or 0),
  1764.         }
  1765.       end
  1766.     end
  1767.   end
  1768. end
  1769.  
  1770. local function ruleMatches(r, name, attrs, cset)
  1771.   if r.tag ~= "*" and r.tag ~= name then return false end
  1772.   if r.id and r.id ~= attrs.id then return false end
  1773.   for _, c in ipairs(r.classes) do
  1774.     if not cset[c] then return false end
  1775.   end
  1776.   return true
  1777. end
  1778.  
  1779. local function applyDecl(s, k, v)
  1780.   v = v:gsub("%s*!important", "")
  1781.   local lv = v:lower()
  1782.   if k == "color" then
  1783.     local c = parseColor(lv)
  1784.     if c then s.fg = c; s.ownFg = true end
  1785.   elseif k == "background-color" or k == "background" then
  1786.     local c = findColor(lv)
  1787.     if c then s.bg = c; s.ownBg = true end
  1788.   elseif k == "text-align" then
  1789.     if lv:find("center") then s.align = "center"
  1790.     elseif lv:find("right") or lv:find("end") then s.align = "right"
  1791.     elseif lv:find("left") or lv:find("start") or lv:find("justify") then s.align = "left" end
  1792.   elseif k == "font-weight" then
  1793.     local num = tonumber(lv)
  1794.     if lv == "bold" or lv == "bolder" or (num and num >= 600) then s.bold = true
  1795.     elseif lv == "normal" or lv == "lighter" or num then s.bold = false end
  1796.   elseif k == "display" then
  1797.     if lv:find("none") then s.hidden = true end
  1798.   elseif k == "visibility" then
  1799.     if lv == "hidden" then s.hidden = true end
  1800.   elseif k == "text-transform" then
  1801.     s.upper = (lv == "uppercase")
  1802.   end
  1803. end
  1804.  
  1805. local function formEncode(s)
  1806.   s = tostring(s or "")
  1807.   s = s:gsub("\n", "\r\n")
  1808.   s = s:gsub("([^%w%-%._~ ])", function(c)
  1809.     return string.format("%%%02X", byte(c))
  1810.   end)
  1811.   return s:gsub(" ", "+")
  1812. end
  1813.  
  1814. local function controlValue(c)
  1815.   if c.type == "checkbox" or c.type == "radio" then
  1816.     return c.checked and (c.value ~= "" and c.value or "on") or "0"
  1817.   end
  1818.   return c.value or ""
  1819. end
  1820.  
  1821. local function serializeForm(controls)
  1822.   local out = {}
  1823.   for _, c in ipairs(controls) do
  1824.     if c.name and c.name ~= "" and not c.disabled then
  1825.       local skip = false
  1826.       local t = c.type
  1827.       if t == "button" or t == "reset" then
  1828.         skip = true
  1829.       elseif t == "radio" or t == "checkbox" then
  1830.         if not c.checked then skip = true end
  1831.       end
  1832.       if not skip then
  1833.         out[#out + 1] = formEncode(c.name) .. "=" .. formEncode(controlValue(c))
  1834.       end
  1835.     end
  1836.   end
  1837.   return table.concat(out, "&")
  1838. end
  1839.  
  1840. local function findControl(doc, id)
  1841.   for _, c in ipairs(doc.controls or {}) do
  1842.     if c.id == id then return c end
  1843.   end
  1844. end
  1845.  
  1846. local VOID = set { "br", "hr", "img", "meta", "link", "input", "area", "base", "col", "embed", "source", "wbr", "param", "track" }
  1847. local BLOCK = set { "html", "body", "div", "p", "h1", "h2", "h3", "h4", "h5", "h6", "ul", "ol", "li", "table", "tr",
  1848.   "blockquote", "pre", "center", "section", "article", "header", "footer", "nav", "main", "aside", "form",
  1849.   "fieldset", "dl", "dt", "dd", "address", "figure", "figcaption", "details", "summary", "caption", "thead",
  1850.   "tbody", "tfoot", "hgroup", "menu" }
  1851. local MARGIN = set { "p", "h1", "h2", "h3", "h4", "h5", "h6", "ul", "ol", "table", "blockquote", "pre", "dl", "form",
  1852.   "fieldset", "figure", "address" }
  1853. local LIST = set { "ul", "ol" }
  1854. local HEADING = set { "h1", "h2", "h3", "h4", "h5", "h6" }
  1855. local HIDE = set { "head", "title", "template", "svg", "noscript", "select", "canvas", "iframe", "object" }
  1856. local P_STOP = set { "td", "th", "li", "body", "html", "table", "div", "ul", "ol", "blockquote" }
  1857. local AUTO = {
  1858.   li = { targets = set { "li" }, stop = set { "ul", "ol" } },
  1859.   dt = { targets = set { "dt", "dd" }, stop = set { "dl" } },
  1860.   dd = { targets = set { "dt", "dd" }, stop = set { "dl" } },
  1861.   td = { targets = set { "td", "th" }, stop = set { "tr", "table" } },
  1862.   th = { targets = set { "td", "th" }, stop = set { "tr", "table" } },
  1863.   tr = { targets = set { "tr" }, stop = set { "table" } },
  1864. }
  1865.  
  1866. local function parseAttrs(s)
  1867.   local a = {}
  1868.   for k, _, v in s:gmatch("([%w%-_:]+)%s*=%s*([\"'])(.-)%2") do a[k:lower()] = v end
  1869.   s = s:gsub("([%w%-_:]+)%s*=%s*([\"'])(.-)%2", " ")
  1870.   for k, v in s:gmatch("([%w%-_:]+)%s*=%s*([^%s\"'>]+)") do a[k:lower()] = v end
  1871.   return a
  1872. end
  1873.  
  1874. local function findTagEnd(s, i)
  1875.   while true do
  1876.     local p = s:find("[>\"']", i)
  1877.     if not p then return nil end
  1878.     local ch = s:sub(p, p)
  1879.     if ch == ">" then return p end
  1880.     local qq = s:find(ch, p + 1, true)
  1881.     if not qq or qq - p > 2000 then return s:find(">", i, true) end
  1882.     i = qq + 1
  1883.   end
  1884. end
  1885.  
  1886. local function ciPattern(name)
  1887.   return "</" .. (name:gsub("%a", function(c) return "[" .. c:lower() .. c:upper() .. "]" end))
  1888. end
  1889. local CLOSE_PAT = { script = ciPattern("script"), style = ciPattern("style") }
  1890.  
  1891. local function layout(html, baseURL, w, opts)
  1892.   opts = opts or {}
  1893.   local rules = {}
  1894.   for css in html:gmatch("<[sS][tT][yY][lL][eE][^>]*>(.-)</[sS][tT][yY][lL][eE]>") do
  1895.     parseCSS(css, rules)
  1896.   end
  1897.   table.sort(rules, function(a, b)
  1898.     if a.spec ~= b.spec then return a.spec < b.spec end
  1899.     return a.idx < b.idx
  1900.   end)
  1901.  
  1902.   local lines, links = {}, {}
  1903.   local controls, controlSeq = {}, 0
  1904.   local forms, formSeq = {}, 0
  1905.   local title = ""
  1906.   local page = { bg = 0x000000 }
  1907.   local root = {
  1908.     fg = 0xFFFFFF, bg = 0x000000, fill = 0x000000, align = "left", indent = 0,
  1909.     bold = false, pre = false, hidden = false, upper = false, listDepth = 0,
  1910.   }
  1911.   local stack = { { name = "#root", st = root } }
  1912.   local cur, pending, imageCount = nil, false, 0
  1913.  
  1914.   local function st() return stack[#stack].st end
  1915.  
  1916.   local function ensureLine()
  1917.     if not cur then
  1918.       local s = st()
  1919.       cur = { segs = {}, len = 0, align = s.align, indent = s.indent, fill = s.fill }
  1920.     end
  1921.     return cur
  1922.   end
  1923.  
  1924.   local function endLine()
  1925.     if cur then
  1926.       lines[#lines + 1] = cur
  1927.       cur = nil
  1928.       pending = false
  1929.     end
  1930.   end
  1931.  
  1932.   local function newline()
  1933.     ensureLine()
  1934.     endLine()
  1935.   end
  1936.  
  1937.   local function blank()
  1938.     endLine()
  1939.     if #lines == 0 or lines[#lines].len == 0 then return end
  1940.     lines[#lines + 1] = { segs = {}, len = 0, align = "left", indent = 0, fill = st().fill }
  1941.   end
  1942.  
  1943.   local function newControl(c)
  1944.     controlSeq = controlSeq + 1
  1945.     c.id = controlSeq
  1946.     c.defaultValue = c.value or ""
  1947.     c.defaultChecked = c.checked or false
  1948.     controls[#controls + 1] = c
  1949.     if c.form then c.form.controls[#c.form.controls + 1] = c end
  1950.     return c
  1951.   end
  1952.  
  1953.   local function currentForm()
  1954.     for i = #stack, 2, -1 do
  1955.       if stack[i].name == "form" then return stack[i].form end
  1956.     end
  1957.     return nil
  1958.   end
  1959.  
  1960.   local function addSeg(text, s)
  1961.     local l = ensureLine()
  1962.     local fg = s.bold and emphasize(s.fg, s.bg) or s.fg
  1963.     local last = l.segs[#l.segs]
  1964.     if last and last.fg == fg and last.bg == s.bg and last.link == s.link then
  1965.       last.text = last.text .. text
  1966.     else
  1967.       l.segs[#l.segs + 1] = { text = text, fg = fg, bg = s.bg, link = s.link }
  1968.     end
  1969.     l.len = l.len + unicode.len(text)
  1970.   end
  1971.  
  1972.   local function addControlSeg(text, c, s)
  1973.     local l = ensureLine()
  1974.     l.segs[#l.segs + 1] = {
  1975.       text = text, fg = s.fg, bg = s.bg, link = s.link,
  1976.       control = c and c.id,
  1977.     }
  1978.     l.len = l.len + unicode.len(text)
  1979.     if c then c.rendered = true end
  1980.   end
  1981.  
  1982.   local function pushWord(word)
  1983.     local s = st()
  1984.     if s.upper then word = unicode.upper(word) end
  1985.     local wl = unicode.len(word)
  1986.     local l = cur
  1987.     local sp = (pending and l and l.len > 0) and 1 or 0
  1988.     if l and l.len > 0 and l.len + sp + wl > max(1, w - l.indent) then
  1989.       endLine()
  1990.       sp = 0
  1991.     end
  1992.     l = ensureLine()
  1993.     local avail = max(1, w - l.indent)
  1994.     if sp == 1 then addSeg(" ", s) end
  1995.     while wl > avail - l.len do
  1996.       local room = avail - l.len
  1997.       if room > 0 then
  1998.         addSeg(unicode.sub(word, 1, room), s)
  1999.         word = unicode.sub(word, room + 1)
  2000.         wl = wl - room
  2001.       end
  2002.       endLine()
  2003.       l = ensureLine()
  2004.     end
  2005.     addSeg(word, s)
  2006.     pending = false
  2007.     if s.link then links[s.link].hasText = true end
  2008.   end
  2009.  
  2010.   local function addText(text)
  2011.     local s = st()
  2012.  
  2013.     local top = stack[#stack]
  2014.     if top.name == "textarea" and top.control then
  2015.       top.control.value = (top.control.value or "") .. decodeEntities(text)
  2016.       return
  2017.     end
  2018.     if top.name == "option" and top.option then
  2019.       top.option.text = top.option.text .. decodeEntities(text)
  2020.       return
  2021.     end
  2022.     if top.name == "button" and top.control then
  2023.       top.control.text = top.control.text .. decodeEntities(text)
  2024.       return
  2025.     end
  2026.  
  2027.     if s.hidden then return end
  2028.     text = decodeEntities(text)
  2029.     if s.pre then
  2030.       text = text:gsub("\r", ""):gsub("\t", "    ")
  2031.       local i = 0
  2032.       for rawPart in (text .. "\n"):gmatch("(.-)\n") do
  2033.         local part = rawPart
  2034.         i = i + 1
  2035.         if i > 1 then newline() end
  2036.         if part ~= "" then
  2037.           if unicode.len(part) > w - s.indent then part = unicode.sub(part, 1, max(1, w - s.indent)) end
  2038.           addSeg(part, s)
  2039.         end
  2040.       end
  2041.       return
  2042.     end
  2043.     if text:match("^%s") then pending = true end
  2044.     for word in text:gmatch("%S+") do
  2045.       pushWord(word)
  2046.       pending = true
  2047.     end
  2048.     if not text:match("%s$") and text:find("%S") then pending = false end
  2049.   end
  2050.  
  2051.   local function emitImage(img, s)
  2052.     endLine()
  2053.     local rows = floor(img.ph / 2)
  2054.     local first = #lines + 1
  2055.     local proto = {
  2056.       img = img, first = first, rows = rows, len = img.pw, link = s.link,
  2057.       align = s.align, indent = s.indent, fill = s.fill,
  2058.     }
  2059.     for r = 0, rows - 1 do lines[first + r] = proto end
  2060.     if s.link then links[s.link].hasText = true end
  2061.   end
  2062.  
  2063.   local function doImage(attrs, s)
  2064.     if s.hidden then return end
  2065.     local alt = attrs.alt and trim(decodeEntities(attrs.alt)) or ""
  2066.     local wpx = tonumber((attrs.width or ""):match("^%d+"))
  2067.     local hpx = tonumber((attrs.height or ""):match("^%d+"))
  2068.     if (wpx and wpx <= 4) or (hpx and hpx <= 4) then return end
  2069.     local src = attrs.src and resolveURL(baseURL, attrs.src)
  2070.  
  2071.     local function placeholder(reason)
  2072.       if reason and #reason > 24 then reason = "image" end
  2073.       local text = alt ~= "" and ("[" .. alt .. "]") or ("[" .. (reason or "image") .. "]")
  2074.       pending = true
  2075.       pushWord(text)
  2076.     end
  2077.  
  2078.     if not (opts.images and src) then return placeholder() end
  2079.     imageCount = imageCount + 1
  2080.  
  2081.     local pixPerCell = (IMAGE_MODE == "half" and depth > 1) and 2 or 4
  2082.     local cols = max(4, min(w - s.indent, 120))
  2083.     if wpx then cols = min(cols, max(4, ceil(wpx / 5))) end
  2084.  
  2085.     if opts.status then
  2086.       opts.status("Loading image " .. imageCount .. ": " .. src)
  2087.     end
  2088.     local img, err = loadImage(src, cols, pixPerCell)
  2089.     gc()
  2090.     if not img then return placeholder(err) end
  2091.  
  2092.     emitImage(img, s)
  2093.   end
  2094.  
  2095.   local function closeElement()
  2096.     local e = stack[#stack]
  2097.     local n = e.name
  2098.     if n == "a" and e.linkIdx and LINK_MARKERS and not e.st.hidden then
  2099.       if links[e.linkIdx].hasText then
  2100.         pending = true
  2101.         pushWord("[" .. e.linkIdx .. "]")
  2102.       end
  2103.     end
  2104.  
  2105.     if n == "textarea" and e.control then
  2106.       local c = e.control
  2107.       endLine()
  2108.       local value = c.value or ""
  2109.       local rows = {}
  2110.       for line in (value .. "\n"):gmatch("(.-)\n") do
  2111.         rows[#rows + 1] = line
  2112.       end
  2113.       if #rows == 0 then rows[1] = "" end
  2114.       for i = 1, c.rows do
  2115.         local text = rows[i] or ""
  2116.         text = unicode.sub(text, 1, c.cols)
  2117.         local shown = "| " .. text ..
  2118.           string.rep(" ", max(0, c.cols - unicode.len(text))) .. " |"
  2119.         lines[#lines + 1] = {
  2120.           segs = { { text = shown, fg = c.fg, bg = c.bg, control = c.id } },
  2121.           len = unicode.len(shown), align = e.st.align,
  2122.           indent = e.st.indent, fill = e.st.fill,
  2123.         }
  2124.       end
  2125.       endLine()
  2126.       stack[#stack] = nil
  2127.       return
  2128.     end
  2129.  
  2130.     if n == "select" and e.control then
  2131.       local c = e.control
  2132.       endLine()
  2133.       local option = c.options[c.selected]
  2134.       local text = option and option.text or ""
  2135.       local shown = "[ " ..
  2136.         unicode.sub(text, 1, max(5, w - e.st.indent - 6)) .. " v ]"
  2137.       lines[#lines + 1] = {
  2138.         segs = { { text = shown, fg = c.fg, bg = c.bg, control = c.id } },
  2139.         len = unicode.len(shown), align = e.st.align,
  2140.         indent = e.st.indent, fill = e.st.fill,
  2141.       }
  2142.       stack[#stack] = nil
  2143.       return
  2144.     end
  2145.  
  2146.     if n == "button" and e.control then
  2147.       local c = e.control
  2148.       endLine()
  2149.       local text = trim(c.text)
  2150.       if text == "" then text = "Button" end
  2151.       local shown = "[ " .. text .. " ]"
  2152.       lines[#lines + 1] = {
  2153.         segs = { { text = shown, fg = c.fg, bg = c.bg, control = c.id } },
  2154.         len = unicode.len(shown), align = e.st.align,
  2155.         indent = e.st.indent, fill = e.st.fill,
  2156.       }
  2157.       stack[#stack] = nil
  2158.       return
  2159.     end
  2160.  
  2161.     if BLOCK[n] then
  2162.       endLine()
  2163.       if MARGIN[n] and (not LIST[n] or e.st.listDepth <= 1) then blank() end
  2164.     end
  2165.     stack[#stack] = nil
  2166.   end
  2167.  
  2168.   local function closeTo(i)
  2169.     for _ = #stack, i, -1 do closeElement() end
  2170.   end
  2171.  
  2172.   local function closeUntilAny(targets, stop)
  2173.     for i = #stack, 2, -1 do
  2174.       local n = stack[i].name
  2175.       if targets[n] then return closeTo(i) end
  2176.       if stop[n] then return end
  2177.     end
  2178.   end
  2179.  
  2180.   local function closeTag(name)
  2181.     for i = #stack, 2, -1 do
  2182.       if stack[i].name == name then return closeTo(i) end
  2183.     end
  2184.   end
  2185.  
  2186.   local function openTag(name, attrs, selfClose)
  2187.     if BLOCK[name] and name ~= "html" and name ~= "body" then
  2188.       closeUntilAny({ p = true }, P_STOP)
  2189.     end
  2190.     if AUTO[name] then closeUntilAny(AUTO[name].targets, AUTO[name].stop) end
  2191.  
  2192.     local s = copy(st())
  2193.     s.ownBg, s.ownFg = false, false
  2194.  
  2195.     if HIDE[name] then s.hidden = true end
  2196.     if name == "body" then s.hidden = false end
  2197.     if HEADING[name] or name == "b" or name == "strong" or name == "th" then s.bold = true end
  2198.     if name == "h1" then s.upper = true end
  2199.     if name == "center" or name == "th" then s.align = "center" end
  2200.     if name == "pre" then s.pre = true end
  2201.     if LIST[name] then s.indent = s.indent + 2; s.listDepth = s.listDepth + 1 end
  2202.     if name == "blockquote" then s.indent = s.indent + 4 end
  2203.  
  2204.     local linkIdx
  2205.     if name == "a" and attrs.href then
  2206.       local target = resolveURL(baseURL, attrs.href)
  2207.       if target then
  2208.         links[#links + 1] = { href = target, hasText = false }
  2209.         linkIdx = #links
  2210.         s.link = linkIdx
  2211.         s.fg = s.linkColor or (lum(s.bg) < 128 and 0x66AAFF or 0x0000EE)
  2212.       end
  2213.     end
  2214.  
  2215.     if attrs.align and name ~= "table" then
  2216.       local a = attrs.align:lower()
  2217.       if a == "middle" then a = "center" end
  2218.       if a == "left" or a == "center" or a == "right" then s.align = a end
  2219.     end
  2220.     if attrs.bgcolor then
  2221.       local c = parseColor(attrs.bgcolor, true)
  2222.       if c then s.bg = c; s.ownBg = true end
  2223.     end
  2224.     if name == "font" and attrs.color then
  2225.       local c = parseColor(attrs.color, true)
  2226.       if c then s.fg = c; s.ownFg = true end
  2227.     end
  2228.     if name == "body" then
  2229.       if attrs.text then
  2230.         local c = parseColor(attrs.text, true)
  2231.         if c then s.fg = c; s.ownFg = true end
  2232.       end
  2233.       if attrs.link then
  2234.         local c = parseColor(attrs.link, true)
  2235.         if c then s.linkColor = c end
  2236.       end
  2237.     end
  2238.  
  2239.     if #rules > 0 then
  2240.       local cset = {}
  2241.       if attrs.class then
  2242.         for c in attrs.class:gmatch("%S+") do cset[c] = true end
  2243.       end
  2244.       for _, r in ipairs(rules) do
  2245.         if ruleMatches(r, name, attrs, cset) then
  2246.           for _, p in ipairs(r.props) do applyDecl(s, p[1], p[2]) end
  2247.         end
  2248.       end
  2249.     end
  2250.     if attrs.style then
  2251.       for k, v in attrs.style:gmatch("([%w%-]+)%s*:%s*([^;]+)") do
  2252.         applyDecl(s, k:lower(), trim(v))
  2253.       end
  2254.     end
  2255.  
  2256.     if s.ownBg and BLOCK[name] then s.fill = s.bg end
  2257.     if abs(lum(s.fg) - lum(s.bg)) < 40 then
  2258.       s.fg = lum(s.bg) > 127 and 0x000000 or 0xFFFFFF
  2259.     end
  2260.     if name == "html" or name == "body" then
  2261.       if s.ownBg then page.bg = s.bg end
  2262.     end
  2263.  
  2264.     local form
  2265.     if name == "form" and ENABLE_FORMS then
  2266.       formSeq = formSeq + 1
  2267.       form = {
  2268.         id = formSeq,
  2269.         action = resolveURL(baseURL, attrs.action or baseURL) or baseURL,
  2270.         method = (attrs.method or "GET"):upper(),
  2271.         controls = {},
  2272.       }
  2273.       forms[#forms + 1] = form
  2274.     end
  2275.  
  2276.     if VOID[name] or selfClose then
  2277.       stack[#stack + 1] = { name = name, st = s }
  2278.       if name == "br" then
  2279.         newline()
  2280.       elseif name == "hr" then
  2281.         endLine()
  2282.         local n = max(1, w - s.indent)
  2283.         lines[#lines + 1] = {
  2284.           segs = { { text = string.rep("-", n), fg = s.fg, bg = s.bg } },
  2285.           len = n, align = "left", indent = s.indent, fill = s.fill,
  2286.         }
  2287.       elseif name == "img" then
  2288.         doImage(attrs, s)
  2289.       elseif name == "input" and not s.hidden and ENABLE_FORMS then
  2290.         local t = (attrs.type or "text"):lower()
  2291.         local c = newControl({
  2292.           type = t,
  2293.           name = attrs.name,
  2294.           value = attrs.value or "",
  2295.           checked = attrs.checked ~= nil,
  2296.           disabled = attrs.disabled ~= nil,
  2297.           form = currentForm(),
  2298.           attrs = attrs,
  2299.           fg = s.fg,
  2300.           bg = s.bg,
  2301.         })
  2302.  
  2303.         if t == "submit" or t == "button" or t == "reset" then
  2304.           local label = attrs.value or (t == "reset" and "Reset" or "Submit")
  2305.           pending = true
  2306.           addControlSeg("[ " .. label .. " ]", c, s)
  2307.         elseif t == "checkbox" then
  2308.           pending = true
  2309.           addControlSeg(c.checked and "[x]" or "[ ]", c, s)
  2310.         elseif t == "radio" then
  2311.           pending = true
  2312.           addControlSeg(c.checked and "(o)" or "( )", c, s)
  2313.         elseif t == "hidden" then
  2314.         else
  2315.           local value = c.value
  2316.           if t == "password" then
  2317.             value = string.rep("*", unicode.len(value))
  2318.           end
  2319.           local width = tonumber(attrs.size) or 20
  2320.           width = max(4, min(width, max(4, w - s.indent - 4)))
  2321.           local shown = unicode.sub(value, 1, width)
  2322.           pending = true
  2323.           addControlSeg(
  2324.             "[" .. shown .. string.rep("_", max(0, width - unicode.len(shown))) .. "]",
  2325.             c, s
  2326.           )
  2327.         end
  2328.       end
  2329.       stack[#stack] = nil
  2330.       return
  2331.     end
  2332.  
  2333.     if name == "textarea" and ENABLE_FORMS then
  2334.       local c = newControl({
  2335.         type = "textarea",
  2336.         name = attrs.name,
  2337.         value = "",
  2338.         disabled = attrs.disabled ~= nil,
  2339.         form = currentForm(),
  2340.         attrs = attrs,
  2341.         fg = s.fg,
  2342.         bg = s.bg,
  2343.       })
  2344.       c.rows = max(1, min(tonumber(attrs.rows) or 3, 10))
  2345.       c.cols = max(5, min(tonumber(attrs.cols) or 30, max(5, w - s.indent - 2)))
  2346.       if c.attrs and c.attrs.value then c.value = c.attrs.value end
  2347.       stack[#stack + 1] = { name = "textarea", st = s, control = c }
  2348.       endLine()
  2349.       return
  2350.     end
  2351.  
  2352.     if name == "select" and ENABLE_FORMS then
  2353.       local c = newControl({
  2354.         type = "select",
  2355.         name = attrs.name,
  2356.         value = "",
  2357.         options = {},
  2358.         selected = 1,
  2359.         disabled = attrs.disabled ~= nil,
  2360.         form = currentForm(),
  2361.         attrs = attrs,
  2362.         fg = s.fg,
  2363.         bg = s.bg,
  2364.       })
  2365.       stack[#stack + 1] = { name = "select", st = s, control = c }
  2366.       endLine()
  2367.       return
  2368.     end
  2369.  
  2370.     if name == "option" and ENABLE_FORMS then
  2371.       local parent = stack[#stack]
  2372.       if parent and parent.name == "select" then
  2373.         local c = parent.control
  2374.         local option = {
  2375.           value = attrs.value or "",
  2376.           text = "",
  2377.           selected = attrs.selected ~= nil,
  2378.         }
  2379.         c.options[#c.options + 1] = option
  2380.         if option.selected then
  2381.           c.selected = #c.options
  2382.           c.value = option.value
  2383.         elseif #c.options == 1 then
  2384.           c.selected = 1
  2385.           c.value = option.value
  2386.         end
  2387.         stack[#stack + 1] = { name = "option", st = s, option = option }
  2388.         return
  2389.       end
  2390.     end
  2391.  
  2392.     if name == "button" and ENABLE_FORMS then
  2393.       local c = newControl({
  2394.         type = attrs.type or "submit",
  2395.         name = attrs.name,
  2396.         value = attrs.value or "",
  2397.         text = "",
  2398.         disabled = attrs.disabled ~= nil,
  2399.         form = currentForm(),
  2400.         fg = s.fg,
  2401.         bg = s.bg,
  2402.         attrs = attrs,
  2403.       })
  2404.       stack[#stack + 1] = { name = "button", st = s, control = c }
  2405.       return
  2406.     end
  2407.  
  2408.     if BLOCK[name] then endLine() end
  2409.     stack[#stack + 1] = { name = name, st = s, linkIdx = linkIdx, form = form }
  2410.     if BLOCK[name] and MARGIN[name] and (not LIST[name] or s.listDepth <= 1) then blank() end
  2411.  
  2412.     if name == "li" then
  2413.       local listE
  2414.       for i = #stack - 1, 1, -1 do
  2415.         if LIST[stack[i].name] then listE = stack[i]; break end
  2416.       end
  2417.       local prefix = "* "
  2418.       if listE and listE.name == "ol" then
  2419.         listE.n = (listE.n or 0) + 1
  2420.         prefix = listE.n .. ". "
  2421.       end
  2422.       ensureLine()
  2423.       pending = false
  2424.       addSeg(prefix, s)
  2425.     elseif name == "td" or name == "th" then
  2426.       for i = #stack - 1, 1, -1 do
  2427.         local n = stack[i].name
  2428.         if n == "tr" then
  2429.           local row = stack[i]
  2430.           if (row.cells or 0) > 0 then
  2431.             ensureLine()
  2432.             addSeg(" | ", row.st)
  2433.           end
  2434.           row.cells = (row.cells or 0) + 1
  2435.           pending = false
  2436.           break
  2437.         elseif n == "table" then
  2438.           break
  2439.         end
  2440.       end
  2441.     end
  2442.   end
  2443.  
  2444.   local pos, n, tags = 1, #html, 0
  2445.   while pos <= n do
  2446.     local lt = html:find("<", pos, true)
  2447.     if not lt then
  2448.       addText(html:sub(pos))
  2449.       break
  2450.     end
  2451.     if lt > pos then
  2452.       local t = html:sub(pos, lt - 1)
  2453.       if stack[#stack].name == "title" then title = title .. decodeEntities(t) end
  2454.       addText(t)
  2455.     end
  2456.     local nextpos
  2457.     local c2 = html:sub(lt + 1, lt + 1)
  2458.     if html:sub(lt + 1, lt + 3) == "!--" then
  2459.       local e = html:find("-->", lt + 4, true)
  2460.       nextpos = e and e + 3 or n + 1
  2461.     elseif c2 == "!" or c2 == "?" then
  2462.       local e = html:find(">", lt + 1, true)
  2463.       nextpos = e and e + 1 or n + 1
  2464.     elseif c2 ~= "" and c2:match("[%a/]") then
  2465.       local e = findTagEnd(html, lt + 1)
  2466.       if not e then
  2467.         addText(html:sub(lt))
  2468.         break
  2469.       end
  2470.       local inner = html:sub(lt + 1, e - 1)
  2471.       local closing, name, rest = inner:match("^(/?)([%a][%w%-]*)(.*)$")
  2472.       if not name then
  2473.         addText("<")
  2474.         nextpos = lt + 1
  2475.       else
  2476.         name = name:lower()
  2477.         nextpos = e + 1
  2478.         if closing == "/" then
  2479.           closeTag(name)
  2480.         elseif name == "script" or name == "style" then
  2481.           local cs = html:find(CLOSE_PAT[name], e + 1)
  2482.           if cs then
  2483.             local ce = html:find(">", cs, true)
  2484.             nextpos = (ce or n) + 1
  2485.           else
  2486.             nextpos = n + 1
  2487.           end
  2488.         else
  2489.           openTag(name, parseAttrs(rest), rest:sub(-1) == "/")
  2490.         end
  2491.       end
  2492.     else
  2493.       addText("<")
  2494.       nextpos = lt + 1
  2495.     end
  2496.     pos = nextpos
  2497.     tags = tags + 1
  2498.     if tags % 100 == 0 then os.sleep(0) end
  2499.   end
  2500.  
  2501.   if #stack > 1 then closeTo(2) end
  2502.   endLine()
  2503.   return {
  2504.     lines = lines,
  2505.     links = links,
  2506.     controls = controls,
  2507.     forms = forms,
  2508.     title = decodeEntities(trim(title)),
  2509.     bg = page.bg,
  2510.   }
  2511. end
  2512.  
  2513. local imgBlocks = {}
  2514.  
  2515. local function prefetchImages(doc, first, last)
  2516.   local blocks, lines = {}, doc.lines
  2517.   local i = first
  2518.   while i <= last do
  2519.     local l = lines[i]
  2520.     if l and l.img then
  2521.       local j = i
  2522.       while j < last and lines[j + 1] == l do j = j + 1 end
  2523.       local r1, r2 = i - l.first, j - l.first
  2524.       local old = imgBlocks[l]
  2525.       if old and old.r1 <= r1 and old.r2 >= r2 then
  2526.         blocks[l] = old
  2527.       else
  2528.         local a, b = max(0, r1 - 8), min(l.rows - 1, r2 + 8)
  2529.         local rowBytes = l.img.pw * 6
  2530.         blocks[l] = { r1 = a, r2 = b,
  2531.           data = imgstore.read(l.img, a * rowBytes, (b - a + 1) * rowBytes) }
  2532.       end
  2533.       i = j + 1
  2534.     else
  2535.       i = i + 1
  2536.     end
  2537.   end
  2538.   imgBlocks = blocks
  2539.   return blocks
  2540. end
  2541.  
  2542. local function imageSegs(l, li, blk)
  2543.   local pw = l.img.pw
  2544.   local stride = pw * 3
  2545.   local rgb = blk.data
  2546.   local top = (li - l.first - blk.r1) * 2 * stride + 1
  2547.   local segs, last, n = {}, nil, 0
  2548.   local link = l.link
  2549.   for x = 0, pw - 1 do
  2550.     local o1 = top + x * 3
  2551.     local o2 = o1 + stride
  2552.     local r1, g1, b1 = byte(rgb, o1, o1 + 2)
  2553.     local r2, g2, b2 = byte(rgb, o2, o2 + 2)
  2554.     if invertImages then
  2555.       r1, g1, b1 = 255 - r1, 255 - g1, 255 - b1
  2556.       r2, g2, b2 = 255 - r2, 255 - g2, 255 - b2
  2557.     end
  2558.     local fg = r1 * 65536 + g1 * 256 + b1
  2559.     local bg = r2 * 65536 + g2 * 256 + b2
  2560.     if last and last.fg == fg and last.bg == bg then
  2561.       last.n = last.n + 1
  2562.     else
  2563.       n = n + 1
  2564.       last = { fg = fg, bg = bg, link = link, n = 1 }
  2565.       segs[n] = last
  2566.     end
  2567.   end
  2568.   for i = 1, n do
  2569.     local sg = segs[i]
  2570.     sg.text = sg.n == 1 and "▀" or string.rep("▀", sg.n)
  2571.   end
  2572.   return segs
  2573. end
  2574.  
  2575. local function draw(doc, offset, w, h, url, hit, focusId)
  2576.   local ch = h - 1
  2577.   local blocks = prefetchImages(doc, offset + 1, offset + ch)
  2578.  
  2579.   local buffered = beginBufferRender(w, ch)
  2580.  
  2581.   if buffered then
  2582.     gpu.setBackground(q(doc.bg))
  2583.     gpu.fill(1, 1, w, ch, " ")
  2584.   else
  2585.     gpu.setActiveBuffer(0)
  2586.   end
  2587.  
  2588.   for row = 1, ch do
  2589.     local l = doc.lines[offset + row]
  2590.     hit[row] = {}
  2591.  
  2592.     gpu.setBackground(q(l and l.fill or doc.bg))
  2593.     gpu.fill(1, row, w, 1, " ")
  2594.  
  2595.     if l and l.len > 0 then
  2596.       local avail = w - l.indent
  2597.       local x = 1 + l.indent
  2598.  
  2599.       if l.align == "center" then
  2600.         x = x + max(0, floor((avail - l.len) / 2))
  2601.       elseif l.align == "right" then
  2602.         x = x + max(0, avail - l.len)
  2603.       end
  2604.  
  2605.       for _, sg in ipairs(l.segs or imageSegs(l, offset + row, blocks[l])) do
  2606.         local txt = sg.text
  2607.         local len = unicode.len(txt)
  2608.  
  2609.         local isFocused =
  2610.           sg.control and focusId and sg.control == focusId
  2611.  
  2612.         if isFocused then
  2613.           gpu.setForeground(q(0xFFFFFF))
  2614.           gpu.setBackground(q(0x0055AA))
  2615.         else
  2616.           gpu.setForeground(q(sg.fg))
  2617.           gpu.setBackground(q(sg.bg))
  2618.         end
  2619.  
  2620.         if x + len - 1 > w then
  2621.           txt = unicode.sub(txt, 1, max(0, w - x + 1))
  2622.           len = unicode.len(txt)
  2623.         end
  2624.  
  2625.         if txt ~= "" then
  2626.           gpu.set(x, row, txt)
  2627.         end
  2628.  
  2629.         if sg.link or sg.control then
  2630.           hit[row][#hit[row] + 1] = {
  2631.             x,
  2632.             min(w, x + len - 1),
  2633.             sg.link,
  2634.             sg.control
  2635.           }
  2636.         end
  2637.  
  2638.         x = x + len
  2639.       end
  2640.     end
  2641.   end
  2642.  
  2643.   if buffered then
  2644.     presentBuffer(w, ch)
  2645.   end
  2646.  
  2647.   local imageState = LOAD_IMAGES and "IMG:on" or "IMG:off"
  2648.   local total = max(1, #doc.lines - ch)
  2649.   local pct = floor(min(offset, total) * 100 / total)
  2650.   local name = doc.title ~= "" and doc.title or url
  2651.  
  2652.   status(string.format(
  2653.     "%d%% %s | %s | Arrows:scroll Tab:focus Enter:activate L:link U:url I:invert-img Bksp:back R:reload Q:quit",
  2654.     pct,
  2655.     name,
  2656.     imageState
  2657.   ))
  2658. end
  2659.  
  2660. local function normalizeURL(u)
  2661.   u = trim(u or "")
  2662.   if u == "" then return nil end
  2663.   if not u:match("^https?://") then u = "http://" .. u end
  2664.   return u
  2665. end
  2666.  
  2667. local function prompt(label)
  2668.   local w, h = gpu.getResolution()
  2669.   pcall(gpu.setActiveBuffer, 0)
  2670.   gpu.setForeground(0xFFFFFF)
  2671.   gpu.setBackground(0x000000)
  2672.   gpu.fill(1, h, w, 1, " ")
  2673.   term.setCursor(1, h)
  2674.   io.write(label)
  2675.   local r = io.read()
  2676.   return r
  2677. end
  2678.  
  2679. local function editControl(c, w, h)
  2680.   pcall(gpu.setActiveBuffer, 0)
  2681.   local value = c.value or ""
  2682.   local cursor = unicode.len(value)
  2683.  
  2684.   local function redraw()
  2685.     gpu.setBackground(q(0x000000))
  2686.     gpu.setForeground(q(0xFFFFFF))
  2687.     gpu.fill(1, h, w, 1, " ")
  2688.  
  2689.     local shown = value
  2690.     if c.type == "password" then
  2691.       shown = string.rep("*", unicode.len(value))
  2692.     end
  2693.  
  2694.     local maxWidth = max(1, w - 2)
  2695.     if unicode.len(shown) > maxWidth then
  2696.       shown = unicode.sub(shown, unicode.len(shown) - maxWidth + 1)
  2697.     end
  2698.     gpu.set(1, h, shown .. "_")
  2699.   end
  2700.  
  2701.   redraw()
  2702.  
  2703.   while true do
  2704.     local e = { event.pull() }
  2705.     if e[1] == "key_down" then
  2706.       local ch = e[3]
  2707.       local code = e[4]
  2708.  
  2709.       if code == 28 then
  2710.         c.value = value
  2711.         return true
  2712.       elseif code == 1 then
  2713.         return false
  2714.       elseif code == 14 then
  2715.         if cursor > 0 then
  2716.           value = unicode.sub(value, 1, cursor - 1) .. unicode.sub(value, cursor + 1)
  2717.           cursor = cursor - 1
  2718.         end
  2719.       elseif code == 203 then
  2720.         cursor = max(0, cursor - 1)
  2721.       elseif code == 205 then
  2722.         cursor = min(unicode.len(value), cursor + 1)
  2723.       elseif code == 199 then
  2724.         cursor = 0
  2725.       elseif code == 207 then
  2726.         cursor = unicode.len(value)
  2727.       elseif ch and ch > 0 then
  2728.         local character = unicode.char(ch)
  2729.         value = unicode.sub(value, 1, cursor) .. character .. unicode.sub(value, cursor + 1)
  2730.         cursor = cursor + unicode.len(character)
  2731.       end
  2732.       redraw()
  2733.     end
  2734.   end
  2735. end
  2736.  
  2737. local function submitForm(form, go, status)
  2738.   if not form then return end
  2739.   local data = serializeForm(form.controls)
  2740.   if form.method == "GET" then
  2741.     local target = form.action
  2742.     if data ~= "" then
  2743.       target = target .. (target:find("?", 1, true) and "&" or "?") .. data
  2744.     end
  2745.     go(target, true)
  2746.   else
  2747.     status("POST forms are not supported by this OC internet API yet")
  2748.   end
  2749. end
  2750.  
  2751. local function browse(startUrl)
  2752.   local w, h = gpu.getResolution()
  2753.   local ch = h - 1
  2754.   local url, doc, offset = nil, nil, 0
  2755.   local history = {}
  2756.   local hit = {}
  2757.   local focusedControl = nil
  2758.  
  2759.   local function redraw()
  2760.     draw(doc, offset, w, h, url, hit, focusedControl)
  2761.   end
  2762.  
  2763.   local function open(target, push)
  2764.     status("Loading " .. target .. " ...")
  2765.     local ok, data, finalUrl = fetchPage(target)
  2766.     if not ok then return false, tostring(data) end
  2767.     status("Rendering ...")
  2768.     imgstore.begin()
  2769.     local ok2, d = pcall(layout, data, finalUrl, w, { images = LOAD_IMAGES, status = status })
  2770.     data = nil
  2771.     if not ok2 then
  2772.       imgstore.abort()
  2773.       return false, "Render error: " .. tostring(d)
  2774.     end
  2775.     if push and url then history[#history + 1] = { url = url, offset = offset } end
  2776.     url, doc, offset = finalUrl, d, 0
  2777.     focusedControl = nil
  2778.     imgstore.commit()
  2779.     gc()
  2780.     return true
  2781.   end
  2782.  
  2783.   local ok, err = open(startUrl, false)
  2784.   if not ok then
  2785.     pcall(gpu.setActiveBuffer, 0)
  2786.     gpu.setForeground(0xFFFFFF); gpu.setBackground(0x000000)
  2787.     term.clear()
  2788.     print("Error fetching URL: " .. err)
  2789.     freeRenderBuffer()
  2790.     return
  2791.   end
  2792.   redraw()
  2793.  
  2794.   local function go(target, push)
  2795.     local ok2, e = open(target, push)
  2796.     if ok2 then
  2797.       redraw()
  2798.     else
  2799.       redraw()
  2800.       status("Error: " .. e)
  2801.     end
  2802.   end
  2803.  
  2804.   local function scrollTo(o)
  2805.     o = max(0, min(o, max(0, #doc.lines - ch)))
  2806.     if o ~= offset then
  2807.       offset = o
  2808.       redraw()
  2809.     end
  2810.   end
  2811.  
  2812.   local function activate(c)
  2813.     if not c or c.disabled then return end
  2814.     focusedControl = c.id
  2815.  
  2816.     if c.type == "text" or c.type == "password" or c.type == "search"
  2817.       or c.type == "email" or c.type == "url" or c.type == "number" then
  2818.       editControl(c, w, h)
  2819.       redraw()
  2820.  
  2821.     elseif c.type == "checkbox" then
  2822.       c.checked = not c.checked
  2823.       redraw()
  2824.  
  2825.     elseif c.type == "radio" then
  2826.       for _, other in ipairs(doc.controls) do
  2827.         if other.type == "radio" and other.name == c.name then
  2828.           other.checked = false
  2829.         end
  2830.       end
  2831.       c.checked = true
  2832.       redraw()
  2833.  
  2834.     elseif c.type == "reset" then
  2835.       for _, other in ipairs(doc.controls) do
  2836.         if other.form == c.form then
  2837.           other.value = other.defaultValue or ""
  2838.           other.checked = other.defaultChecked or false
  2839.         end
  2840.       end
  2841.       redraw()
  2842.  
  2843.     elseif c.type == "submit" or c.type == "button" then
  2844.       submitForm(c.form, go, status)
  2845.     end
  2846.   end
  2847.  
  2848.   while true do
  2849.     local e = { event.pull() }
  2850.     local name = e[1]
  2851.     if name == "key_down" then
  2852.       local code = e[4]
  2853.       if code == 200 then scrollTo(offset - 1)
  2854.       elseif code == 208 then scrollTo(offset + 1)
  2855.       elseif code == 201 then scrollTo(offset - (ch - 1))
  2856.       elseif code == 209 then scrollTo(offset + (ch - 1))
  2857.       elseif code == 199 then scrollTo(0)
  2858.       elseif code == 207 then scrollTo(#doc.lines)
  2859.       elseif code == 16 then break
  2860.       elseif code == 19 then go(url, false)
  2861.       elseif code == 14 then
  2862.         local prev = table.remove(history)
  2863.         if prev then
  2864.           go(prev.url, false)
  2865.           scrollTo(prev.offset)
  2866.         end
  2867.       elseif code == 23 then
  2868.         invertImages = not invertImages
  2869.         redraw()
  2870.       elseif code == 22 then
  2871.         local t = normalizeURL(prompt("URL: "))
  2872.         if t then go(t, true) else redraw() end
  2873.       elseif code == 38 then
  2874.         local n = tonumber(prompt("Link number: ") or "")
  2875.         local L = n and doc.links[n]
  2876.         if L then go(L.href, true) else redraw() end
  2877.       elseif code == 15 and TAB_FOCUS then
  2878.         local nextIndex = 1
  2879.         if focusedControl then
  2880.           for i, c in ipairs(doc.controls) do
  2881.             if c.id == focusedControl then nextIndex = i + 1; break end
  2882.           end
  2883.         end
  2884.         if nextIndex > #doc.controls then nextIndex = 1 end
  2885.         focusedControl = nil
  2886.         for i = nextIndex, #doc.controls do
  2887.           if not doc.controls[i].disabled then
  2888.             focusedControl = doc.controls[i].id
  2889.             break
  2890.           end
  2891.         end
  2892.         if not focusedControl and #doc.controls > 0 then
  2893.           focusedControl = doc.controls[1].id
  2894.         end
  2895.         redraw()
  2896.       elseif code == 28 and focusedControl then
  2897.         local c = findControl(doc, focusedControl)
  2898.         if c then activate(c) end
  2899.       end
  2900.     elseif name == "scroll" then
  2901.       scrollTo(offset - (e[5] or 0) * 3)
  2902.     elseif name == "touch" then
  2903.       local x, y = e[3], e[4]
  2904.       for _, r in ipairs(hit[y] or {}) do
  2905.         if x >= r[1] and x <= r[2] then
  2906.           if r[3] then
  2907.             focusedControl = nil
  2908.             go(doc.links[r[3]].href, true)
  2909.           elseif r[4] then
  2910.             local c = findControl(doc, r[4])
  2911.             if c then activate(c) end
  2912.           end
  2913.           break
  2914.         end
  2915.       end
  2916.     end
  2917.   end
  2918.  
  2919.   pcall(gpu.setActiveBuffer, 0)
  2920.   gpu.setForeground(0xFFFFFF)
  2921.   gpu.setBackground(0x000000)
  2922.   term.clear()
  2923.  
  2924.   freeRenderBuffer()
  2925. end
  2926.  
  2927. local function bootUpScreen()
  2928.   term.clear()
  2929.   local logo = [[
  2930.  ::::::::   ::::::::               :::       ::: :::::::::: :::::::::
  2931. :+:    :+: :+:    :+:              :+:       :+: :+:        :+:    :+:
  2932. +:+    +:+ +:+                     +:+       +:+ +:+        +:+    +:+
  2933. +#+    +:+ +#+       +#++:++#++:++ +#+  +:+  +#+ +#++:++#   +#++:++#+
  2934. +#+    +#+ +#+                     +#+ +#+#+ +#+ +#+        +#+    +#+
  2935. #+#    #+# #+#    #+#               #+#+# #+#+#  #+#        #+#    #+#
  2936.  ########   ########                 ###   ###   ########## #########
  2937.     ]]
  2938.   term.setCursor(1, 1)
  2939.   term.write(logo)
  2940.   print()
  2941.   print("Loading...")
  2942.   print()
  2943.   print("OC-WEB 3.0: A Web Browser For OpenComputers Coded by nonogamer9")
  2944.   os.sleep(2)
  2945. end
  2946.  
  2947. local function main(...)
  2948.   local args = { ... }
  2949.  
  2950.   for i = #args, 1, -1 do
  2951.     if args[i] == "--no-image" then
  2952.       LOAD_IMAGES = false
  2953.       table.remove(args, i)
  2954.     end
  2955.   end
  2956.  
  2957.   pcall(gpu.setActiveBuffer, 0)
  2958.   gpu.setForeground(0xFFFFFF)
  2959.   gpu.setBackground(0x000000)
  2960.   bootUpScreen()
  2961.   term.clear()
  2962.   local url = normalizeURL(args[1])
  2963.   if not url then
  2964.     print("Enter a URL:")
  2965.     url = normalizeURL(io.read())
  2966.   end
  2967.   if not url then
  2968.     print("No URL given.")
  2969.     return
  2970.   end
  2971.   local okb, errb = pcall(browse, url)
  2972.   imgstore.shutdown()
  2973.   if not okb then error(errb, 0) end
  2974. end
  2975.  
  2976. main(...)
Advertisement
Add Comment
Please, Sign In to add comment