summaryrefslogtreecommitdiffstats
path: root/apps/plugins/lua/include_lua/image.lua
blob: 2b2a1ce19fd67d9d5a10129349a259cf1ddd4e63 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
--[[ 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