--[[ Lua Image functions /*************************************************************************** * __________ __ ___. * Open \______ \ ____ ____ | | _\_ |__ _______ ___ * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ * \/ \/ \/ \/ \/ * $Id$ * * Copyright (C) 2017 William Wilgus * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY * KIND, either express or implied. * ****************************************************************************/ ]] --[[ Exposed Functions _img.save _img.search _img.rotate _img.resize _img.tile _img.new _img.load -- Exposed Constants _img.RLI_INFO_ALL _img.RLI_INFO_TYPE _img.RLI_INFO_WIDTH _img.RLI_INFO_HEIGHT _img.RLI_INFO_ELEMS _img.RLI_INFO_BYTES _img.RLI_INFO_DEPTH _img.RLI_INFO_FORMAT _img.RLI_INFO_ADDRESS ]] --[[Other rbimage Functions: -------------------------------------------------------------------------------- img:_len() or #img -- returns number of pixels in image img:__tostring([item]) or tostring(img) -- returns data about the image item = 0 is the same as tostring(img) otherwise item = 1 is the first item in list item = 7 is the 7th item item = 8 is the data address in hex -- See Constants _img.RLI_INFO_.... img:_data(element) -- returns/sets raw pixel data NOTE!! this data is defined by the target and targets with different color depth, bit packing, etc will not be compatible with the same image's data on another target ]] -------------------------------------------------------------------------------- if not rb.lcd_framebuffer then rb.splash(rb.HZ, "No Support!") return nil end local _img = {} do local rocklib_image = getmetatable(rb.lcd_framebuffer()) setmetatable(_img, rocklib_image) -- internal constants local _NIL = nil -- _NIL placeholder local _math = require("math_ex") -- math functions needed local LCD_W, LCD_H = rb.LCD_WIDTH, rb.LCD_HEIGHT local _copy = rocklib_image.copy local _get = rocklib_image.get local _marshal = rocklib_image.marshal local _points = rocklib_image.points -- returns new image -of- img sized to fit w/h tiling to fit if needed _img.tile = function(img, w, h) local hs , ws = img:height(), img:width() local t_img = rb.new_image(w, h) for x = 1, w, ws do _copy(t_img, img, x, 1, 1, 1) end for y = hs, h, hs do _copy(t_img, t_img, 1, y, 1, 1, w, hs) end return t_img end -- resizes src to size of dst _img.resize = function(dst, src) -- simple nearest neighbor resize derived from rockbox - pluginlib_bmp.c -- pretty rough results highly recommend building one more suited.. local dw, dh = dst:width(), dst:height() local xstep = (bit.lshift(src:width(), 8) / (dw)) + 1 local ystep = (bit.lshift(src:height(), 8) / (dh)) local xpos, ypos = 0, 0 local src_x, src_y -- walk the dest get src pixel local function rsz_trans(val, x, y) if x == 1 then src_y = bit.rshift(ypos,8) + 1 xpos = xstep - bit.rshift(xstep,4) + 1 ypos = ypos + ystep; end src_x = bit.rshift(xpos,8) + 1 xpos = xpos + xstep return (_get(src, src_x, src_y, true) or 0) end --/* (dst*, [x1, y1, x2, y2, dx, dy, clip, function]) */ _marshal(dst, 1, 1, dw, dh, _NIL, _NIL, false, rsz_trans) end -- returns new image -of- img rotated in whole degrees 0 - 360 _img.rotate = function(img, degrees) -- we do this backwards as if dest was the unrotated object degrees = 360 - degrees local c, s = _math.d_cos(degrees), _math.d_sin(degrees) -- get the center of the source image local s_xctr, s_yctr = img:width() / 2, img:height() / 2 -- get the the new center of the dest image at rotation angle local d_xctr = ((math.abs(s_xctr * c) + math.abs(s_yctr * s))/ 10000) + 1 local d_yctr = ((math.abs(s_xctr * s) + math.abs(s_yctr * c))/ 10000) + 1 -- calculate size of rect new image will occupy local dw, dh = d_xctr * 2 - 1, d_yctr * 2 - 1 local r_img = rb.new_image(dw, dh) -- r_img:clear() -- doesn't need cleared as we walk every pixel --[[rotation works on origin of 0,0 we need to offset to the center of the image and then place the upper left back at the origin (0, 0)]] --[[0,0|-----| ^< |-------| v> 0,0|-------| | | | 0,0 | | | |_____| |_______| |_______| ]] -- walk the dest get translated src pixel, oversamples src to fill gaps local function rot_trans(val, x, y) -- move center x/y to the origin local xtran = x - d_xctr; local ytran = y - d_yctr; -- rotate about the center of the image by x degrees local yrot = ((xtran * s) + (ytran * c)) / 10000 + s_yctr local xrot = ((xtran * c) - (ytran * s)) / 10000 + s_xctr -- upper left of src image back to origin, copy src pixel return _get(img, xrot, yrot, true) or 0 end _marshal(r_img, 1, 1, dw, dh, _NIL, _NIL, false, rot_trans) return r_img end -- saves img to file: name _img.save = function(img, name) -- bmp saving derived from rockbox - screendump.c -- bitdepth is limited by the device -- eg. device displays greyscale, rgb images are saved greyscale local file local bbuffer = {} -- concat buffer for s_bytes local fbuffer = {} -- concat buffer for file writes, reused local function s_bytesLE(bits, value) -- bits must be multiples of 8 (sizeof byte) local byte local nbytes = bit.rshift(bits, 3) for b = 1, nbytes do if value > 0 then byte = value % 256 value = (value - byte) / 256 else byte = 0 end bbuffer[b] = string.char(byte) end return table.concat(bbuffer, _NIL, 1, nbytes) end local function s_bytesBE(bits, value) -- bits must be multiples of 8 (sizeof byte) local byte local nbytes = bit.rshift(bits, 3) for b = nbytes, 1, -1 do if value > 0 then byte = value % 256 value = (value - byte) / 256 else byte = 0 end bbuffer[b] = string.char(byte) end return table.concat(bbuffer, _NIL, 1, nbytes) end local cmp = {["r"] = function(c) return bit.band(bit.rshift(c, 16), 0xFF) end, ["g"] = function(c) return bit.band(bit.rshift(c, 08), 0xFF) end, ["b"] = function(c) return bit.band(c, 0xFF) end} local function bmp_color(color) return s_bytesLE(8, cmp.b(color)).. s_bytesLE(8, cmp.g(color)).. s_bytesLE(8, cmp.r(color)).. s_bytesLE(8, 0) .. "" end -- c_cmp(color, c.r)) local function bmp_color_mix(c1, c2, num, den) -- mixes c1 and c2 as ratio of numerator / denominator -- used 2x each save results local bc1, gc1, rc1 = cmp.b(c1), cmp.g(c1), cmp.r(c1) return s_bytesLE(8, cmp.b(c2) - bc1 * num / den + bc1).. s_bytesLE(8, cmp.g(c2) - gc1 * num / den + gc1).. s_bytesLE(8, cmp.r(c2) - rc1 * num / den + rc1).. s_bytesLE(8, 0) .. "" end local w, h = img:width(), img:height() local depth = tonumber(img:__tostring(6)) -- RLI_INFO_DEPTH = 0x6 local format = tonumber(img:__tostring(7)) -- RLI_INFO_FORMAT = 0x7 local bpp, bypl -- bits per pixel, bytes per line -- bypl, pad rows to a multiple of 4 bytes if depth <= 4 then bpp = 8 -- 256 color image bypl = (w + 3) elseif depth <= 16 then bpp = 16 bypl = (w * 2 + 3) elseif depth <= 24 then bpp = 24 bypl = (w * 3 + 3) else bpp = 32 bypl = (w * 4 + 3) end local linebytes = bit.band(bypl, bit.bnot(3)) local bytesperpixel = bit.rshift(bpp, 3) local headersz = 54 local imgszpad = h * linebytes local compression, n_colors = 0, 0 local h_ppm, v_ppm = 0x00000EC4, 0x00000EC4 --Pixels Per Meter ~ 96 dpi if depth == 16 then compression = 3 -- BITFIELDS n_colors = 3 elseif depth <= 8 then n_colors = bit.lshift(1, depth) end headersz = headersz + (4 * n_colors) file = io.open('/' .. name, "w+") -- overwrite, rb ignores the 'b' flag if not file then rb.splash(rb.HZ, "Error opening /" .. name) return end -- create a bitmap header 'rope' with image details -- concatenated at end local bmpheader = fbuffer bmpheader[01] = "BM" bmpheader[02] = s_bytesLE(32, headersz + imgszpad) bmpheader[03] = "\0\0\0\0" -- WORD reserved 1 & 2 bmpheader[04] = s_bytesLE(32, headersz) -- BITMAPCOREHEADER size bmpheader[05] = s_bytesLE(32, 40) -- BITMAPINFOHEADER size bmpheader[06] = s_bytesLE(32, w) bmpheader[07] = s_bytesLE(32, h) bmpheader[08] = "\1\0" -- WORD color planes ALWAYS 1 bmpheader[09] = s_bytesLE(16, bpp) -- bits/pixel bmpheader[10] = s_bytesLE(32, compression) bmpheader[11] = s_bytesLE(32, imgszpad) bmpheader[12] = s_bytesLE(32, h_ppm) -- biXPelsPerMeter bmpheader[13] = s_bytesLE(32, v_ppm) -- biYPelsPerMeter bmpheader[14] = s_bytesLE(32, n_colors) bmpheader[15] = s_bytesLE(32, n_colors) -- Color Table (#n_colors entries) if depth == 1 then -- assuming positive display bmpheader[#bmpheader + 1] = bmp_color(0xFFFFFF) bmpheader[#bmpheader + 1] = bmp_color(0x0) elseif depth == 2 then bmpheader[#bmpheader + 1] = bmp_color(0xFFFFFF) bmpheader[#bmpheader + 1] = bmp_color_mix(0xFFFFFF, 0, 1, 3) bmpheader[#bmpheader + 1] = bmp_color_mix(0xFFFFFF, 0, 2, 3) bmpheader[#bmpheader + 1] = bmp_color(0x0) elseif depth == 16 then if format == 555 then -- red bitfield mask bmpheader[#bmpheader + 1] = s_bytesLE(32, 0x00007C00) -- green bitfield mask bmpheader[#bmpheader + 1] = s_bytesLE(32, 0x000003E0) -- blue bitfield mask bmpheader[#bmpheader + 1] = s_bytesLE(32, 0x0000001F) else --565 -- red bitfield mask bmpheader[#bmpheader + 1] = s_bytesLE(32, 0x0000F800) -- green bitfield mask bmpheader[#bmpheader + 1] = s_bytesLE(32, 0x000007E0) -- blue bitfield mask bmpheader[#bmpheader + 1] = s_bytesLE(32, 0x0000001F) end end file:write(table.concat(fbuffer))-- write the header to the file now for i=1, #fbuffer do fbuffer[i] = _NIL end -- reuse table local imgdata = fbuffer -- pad rows to a multiple of 4 bytes local bytesleft = linebytes - (bytesperpixel * w) local t_data = {} local fs_bytes_E = s_bytesLE -- default save in Little Endian if format == 3553 then -- RGB565SWAPPED fs_bytes_E = s_bytesBE -- Saves in Big Endian end -- Bitmap lines start at bottom unless biHeight is negative for point in _points(img, 1, h, w + bytesleft, 1) do imgdata[#imgdata + 1] = fs_bytes_E(bpp, point or 0) if #fbuffer >= 31 then -- buffered write, increase # for performance file:write(table.concat(fbuffer)) for i=1, #fbuffer do fbuffer[i] = _NIL end -- reuse table end end file:write(table.concat(fbuffer)) --write leftovers to file fbuffer = _NIL file:close() end -- save(img, name) --searches an image for target color _img.search = function(img, x1, y1, x2, y2, targetclr, variation, stepx, stepy) if variation > 128 then variation = 128 end if variation < -128 then variation = -128 end local targeth = targetclr + variation local targetl = targetclr - variation if targeth < targetl then local swap = targeth targeth = targetl targetl = swap end for point, x, y in _points(img, x1, y1, x2, y2, stepx, stepy) do if point >= targetl and point <= targeth then return point, x, y end end return nil, nil, nil end --[[ we won't be extending these into RLI_IMAGE]] -- creates a new rbimage size w x h _img.new = function(w, h) return rb.new_image(w, h) end -- returns new image -of- file: name (_NIL if error) _img.load = function(name) return rb.read_bmp_file("/" .. name) end -- expose tostring constants to outside through _img table _img.RLI_INFO_ALL = 0x0 _img.RLI_INFO_TYPE = 0x1 _img.RLI_INFO_WIDTH = 0x2 _img.RLI_INFO_HEIGHT = 0x3 _img.RLI_INFO_ELEMS = 0x4 _img.RLI_INFO_BYTES = 0x5 _img.RLI_INFO_DEPTH = 0x6 _img.RLI_INFO_FORMAT = 0x7 _img.RLI_INFO_ADDRESS = 0x8 end -- _img functions return _img