Save to Camera Roll (an experiment/failure)

With a little help from a Base64 library I copied from lua-users wiki, I created this drawing app that is supposed to save an image to the camera roll. Triple-tapping brings up a window with instructions on how to save the image. Unfortunately, I learned that UIWebViews do not allow saving to the camera roll, so you cannot hold on the image to get the “save image” dialog to pop up. Any ideas on how to get it to work properly?

--# Base64
-- Base64.lua

-- #!/usr/bin/env lua
-- working lua base64 codec (c) 2006-2008 by Alex Kloss
-- compatible with lua 5.1
-- http://www.it-rfc.de
-- licensed under the terms of the LGPL2
-- modified by Rui Viana for gitty client

Base64 = class()

-- bitshift functions (<<, >> equivalent)
-- shift left
function Base64.lsh(value,shift)
    return (value*(2^shift)) % 256
end

-- shift right
function Base64.rsh(value,shift)
    return math.floor(value/2^shift) % 256
end

-- return single bit (for OR)
function Base64.bit(x,b)
    return (x % 2^b - x % 2^(b-1) > 0)
end

-- logic OR for number values
function Base64.lor(x,y)
    result = 0
    for p=1,8 do result = result + (((Base64.bit(x,p) or Base64.bit(y,p)) == true) and 2^(p-1) or 0) end
    return result
end

-- encryption table
local base64chars = {[0]='A',[1]='B',[2]='C',[3]='D',[4]='E',[5]='F',[6]='G',[7]='H',[8]='I',[9]='J',[10]='K',[11]='L',[12]='M',[13]='N',[14]='O',[15]='P',[16]='Q',[17]='R',[18]='S',[19]='T',[20]='U',[21]='V',[22]='W',[23]='X',[24]='Y',[25]='Z',[26]='a',[27]='b',[28]='c',[29]='d',[30]='e',[31]='f',[32]='g',[33]='h',[34]='i',[35]='j',[36]='k',[37]='l',[38]='m',[39]='n',[40]='o',[41]='p',[42]='q',[43]='r',[44]='s',[45]='t',[46]='u',[47]='v',[48]='w',[49]='x',[50]='y',[51]='z',[52]='0',[53]='1',[54]='2',[55]='3',[56]='4',[57]='5',[58]='6',[59]='7',[60]='8',[61]='9',[62]='-',[63]='_'}

-- function encode
-- encodes input string to base64.
function Base64.encode(data)
    local bytes = {}
    local result = ""
    for spos=0,string.len(data)-1,3 do
        for byte=1,3 do bytes[byte] = string.byte(string.sub(data,(spos+byte))) or 0 end
        result = string.format('%s%s%s%s%s',
            result,
            base64chars[Base64.rsh(bytes[1],2)],
            base64chars[Base64.lor(
                Base64.lsh((bytes[1] % 4),4), 
                Base64.rsh(bytes[2],4))] or "=",
            ((#data-spos) > 1) and 
                base64chars[Base64.lor(Base64.lsh(bytes[2] % 16,2), 
                                        Base64.rsh(bytes[3],6))] or "=",
            ((#data-spos) > 2) and base64chars[(bytes[3] % 64)] or "=")
    end
    return result
end

-- decryption table
local base64bytes = {['A']=0,['B']=1,['C']=2,['D']=3,['E']=4,['F']=5,['G']=6,['H']=7,['I']=8,['J']=9,['K']=10,['L']=11,['M']=12,['N']=13,['O']=14,['P']=15,['Q']=16,['R']=17,['S']=18,['T']=19,['U']=20,['V']=21,['W']=22,['X']=23,['Y']=24,['Z']=25,['a']=26,['b']=27,['c']=28,['d']=29,['e']=30,['f']=31,['g']=32,['h']=33,['i']=34,['j']=35,['k']=36,['l']=37,['m']=38,['n']=39,['o']=40,['p']=41,['q']=42,['r']=43,['s']=44,['t']=45,['u']=46,['v']=47,['w']=48,['x']=49,['y']=50,['z']=51,['0']=52,['1']=53,['2']=54,['3']=55,['4']=56,['5']=57,['6']=58,['7']=59,['8']=60,['9']=61,['-']=62,['_']=63,['=']=nil}

-- function decode
-- decode base64 input to string
function Base64.decode(data)
    local chars = {}
    local result=""
    for dpos=0,string.len(data)-1,4 do
        for char=1,4 do chars[char] = base64bytes[(string.sub(data,(dpos+char),(dpos+char)) or "=")] end
        result = string.format('%s%s%s%s',
            result,
            string.char(Base64.lor(Base64.lsh(chars[1],2), Base64.rsh(chars[2],4))),
            (chars[3] ~= nil) and 
            string.char(Base64.lor(Base64.lsh(chars[2],4), Base64.rsh(chars[3],2))) or "",
            (chars[4] ~= nil) and 
            string.char(Base64.lor(Base64.lsh(chars[3],6) % 192, (chars[4]))) or "")
    end
    return result
end

--# Main
-- Save to Camera Roll

-- Setup
function setup()
    print("Hello Camera Roll!\
")
    print("Tap and drag to paint\
Change brush settings with parameters\
Triple-tap to export to Camera Roll")
    parameter.color("BrushColor")
    parameter.number("BrushSize",1,20)
    canvas = image(WIDTH,HEIGHT)
end

function draw()
    -- Drawing
    background(255)
    strokeWidth(5)
    
    -- Draw canvas
    if canvas then
        sprite(canvas,WIDTH/2,HEIGHT/2)
        
        -- Draw border
        noFill()
        stroke(5)
        rect(0,0,WIDTH,HEIGHT)
    end
end

function touched(touch)
    -- Draw to canvas if touch moving
    if touch.state == MOVING then
        setContext(canvas)
        fill(BrushColor)
        noStroke()
        ellipse(touch.x,touch.y,BrushSize)
        lineCapMode(ROUND)
        smooth()
        strokeWidth(BrushSize)
        stroke(BrushColor)
        line(touch.prevX,touch.prevY,touch.x,touch.y)
        setContext()
    end
    
    -- Save to camera roll on triple tap
    if touch.state == ENDED and touch.tapCount == 3 then
        saveToCameraRoll(canvas)
    end
end

--# MimeTypes
MIME = {
	[".aac"] = "audio/aac",
	[".abw"] = "application/x-abiword",
	[".arc"] = "application/octet-stream",
	[".avi"] = "video/x-msvideo",
	[".azw"] = "application/vnd.amazon.ebook",
	[".bin"] = "application/octet-stream",
	[".bz"] = "application/x-bzip",
	[".bz2"] = "application/x-bzip2",
	[".csh"] = "application/x-csh",
	[".css"] = "text/css",
	[".csv"] = "text/csv",
	[".doc"] = "application/msword",
	[".eot"] = "application/vnd.ms-fontobject",
	[".epub"] = "application/epub+zip",
	[".gif"] = "image/gif",
	[".htm"] = "text/html",
	[".html"] = "text/html",
	[".ico"] = "image/x-icon",
	[".ics"] = "text/calendar",
	[".jar"] = "application/java-archive",
	[".jpeg"] = ".jpg",
	[".js"] = "application/javascript",
	[".json"] = "application/json",
	[".mid"] = ".midi",
	[".mpeg"] = "video/mpeg",
	[".mpkg"] = "application/vnd.apple.installer+xml",
	[".odp"] = "application/vnd.oasis.opendocument.presentation",
	[".ods"] = "application/vnd.oasis.opendocument.spreadsheet",
	[".odt"] = "application/vnd.oasis.opendocument.text",
	[".oga"] = "audio/ogg",
	[".ogv"] = "video/ogg",
	[".ogx"] = "application/ogg",
	[".otf"] = "font/otf",
	[".png"] = "image/png",
	[".pdf"] = "application/pdf",
	[".ppt"] = "application/vnd.ms-powerpoint",
	[".rar"] = "application/x-rar-compressed",
	[".rtf"] = "application/rtf",
	[".sh"] = "application/x-sh",
	[".svg"] = "image/svg+xml",
	[".swf"] = "application/x-shockwave-flash",
	[".tar"] = "application/x-tar",
	[".tif"] = "image/tiff",
	[".tiff"] = "image/tiff",
	[".ts"] = "video/vnd.dlna.mpeg-tts",
	[".ttf"] = "font/ttf",
	[".vsd"] = "application/vnd.visio",
	[".wav"] = "audio/x-wav",
	[".weba"] = "audio/webm",
	[".webm"] = "video/webm",
	[".webp"] = "image/webp",
	[".woff"] = "font/woff",
	[".woff2"] = "font/woff2",
	[".xhtml"] = "application/xhtml+xml",
	[".xls"] = "application/vnd.ms-excel",
	[".xml"] = "XML",
	[".xul"] = ".zip",
	[".3gp"] = "video/3gpp",
	[".3g2"] = "video/3gpp2",
	[".7z"] = "application/x-7z-compressed"
}
--# SaveToCameraRoll
function saveToCameraRoll(img,v)
    local textFont = font()
    
    -- HTML template
    local template = [[<!DOCTYPE html> <html> <head> <title>Save Image</title> <style>body{font-family:{{textFont}}; margin:20; color: #444;}img{width: 50%; position: relative; left: 25%; border: 1px dashed #444;}</style> </head> <body> <h1>Save Image</h1> <p>Hold on the image below until a save dialog appears</p><img src="{{src}}" alt="Image"> </body> </html>]]
    
    -- Time/debug
    local socket = require "socket"
    local _print = print
    local print = function(...)
        if not v then
            print(...)
        end
    end
    local gt = socket.gettime
    local finish = function(t)
        print("Done ("..tostring(math.floor((gt()-t)*1000)).." ms)")
    end
    
    print "Saving image..."
    local t = gt()
    
    -- Save image
    saveImage("Documents:temp",img)
    print(readImage("Documents:temp"))
    
    -- Setup path
    local ENV = os.getenv("HOME")
    local DOCUMENTS = "Documents"
    local ASSETPACK = ""
    local FILENAME = "temp.png"
    local path = string.format("%s/%s/%s/%s",ENV,DOCUMENTS,ASSETPACK,FILENAME)
    
    -- Open image
    local data = io.open(path)
    local content = data:read("*all")
    data:close()
    local img = image(content)
    
    finish(t)
    print(img)
    
    print "Encoding content..."
    t = gt()
    local encodedContent = Base64.encode(content)
    finish(t)
    
    print "Generating HTML..."
    t = gt()
    local html = template
    :gsub("{{textFont}}",textFont)
    :gsub("{{src}}","data:image/png;base64,"..encodedContent)
    finish(t)
    
    print "Encoding HTML..."
    t = gt()
    local encodedHtml = Base64.encode(html)
    finish(t)
    
    print "Opening browser... \
Follow the instructions that appear.\
If the page fails to load, tap on the url bar and tap on 'Go' on your keyboard"
    t = gt()
    openURL("data:text/html;base64,"..encodedHtml,true) -- option 1: in-app browser
    -- openURL("http://data:text/html;base64,"..encodedHtml,false) -- option 2: safari (fails to load)
    finish(t)
    
    collectgarbage()
end

@em2 Unfortunately, Codea doesn’t have a proper way of saving images to the Camera roll yet.

The way I do it is to use pasteboard.copy( image ), then once the image is copied, leave Codea, go to an app like iMessages (Notes doesn’t work) then paste the image in the place where you’d normally write a text, then send the text to yourself. Once the image is sent, hold your finger over the image and then press “save”. It’s a bit more complicated than I would hope for, but it’s not as complicated as it sounds. It’s also the only way of saving images to the camera roll I know of that actually works.

Too bad that’s not very user-friendly. Thanks anyway, @Kolosso.

An easier way to save a Codea image to the camera roll is:

1: Save the Codea image to the Codea Dropbox folder.
2: Sync the Codea Dropbox folder with the Dropbox app folder.
3: In the Dropbox app, select the image you want to move.
4: In the upper right corner, tap on the 3 dots.
5: Select Export then select Save Image which will copy it to the Camera roll. 
6: If you don't want to save the images in Dropbox or Codea then delete them.

@dave1707 That might not be convenient for app users.

Maybe uploads through the Dropbox api so that they won’t have to sync it themselves. (I’m not even sure they can from an app bundled with Xcode)

@em2 Another way is to use pasteboard.copy as @Kolosso said. Exit Codea and go into notes and paste the image there. From notes, select the image, then in the upper right, tap the box with the up arrow, then select save image.

@dave1707 Notes doesn’t seem to always work. That’s why I told him to use iMessages

@Kolosso I tried all kinds of images, from full screen to small ones, and haven’t had any trouble with Notes.

@dave1707 Weird.

@Kolosso What kind of problems did you have. Maybe if I knew, I could see if I could cause the same error and maybe a work around.

--# Main
-- Use this function to perform your initial setup
function setup()
    pasteboard.copy( readImage("Planet Cute:Character Boy") )
    print("image copied")
end

@dave1707 If I use this code to copy an image to my pasteboard, then go to Notes, create a new note, then press the paste button, nothing happens. I can tell nothing is loading because I can still type letters in the note.

But when I go to iMessages or Mail, I can press the paste button and it works perfectly.

My guess as to why yours works and not mine is that I may have an older version of Notes (I have an older iPad). It might also be that I have Codea Scratchpad and you have Codea.

@Kolosso I had no trouble with your code on my iPad Air. Everything worked OK. You need to get off of scratchpad and on the real Codea. You’re missing a lot. Hopefully your iPad isn’t too old for the current version. I also have an iPad 1 that has the old version of Codea and Scratchpad. Later on I’ll try your code on there and see what happens.

I’m unable to get Codea because everytime I try to buy an app on the AppStore, It asks me security questions that I don’t remember setting up. I’m planning on calling Apple in a month or two to fix it, and maybe then I can buy Codea.

@Kolosso Why are you waiting a month to two. As for trying the copy on my iPad 1, pasteboard.copy isn’t available in the version of Codea that’s on there. I put Scratchpad on my iPad Air and tried your code. It worked OK, so it must be the version of Notes that’s different.

@dave1707 You’re absolutely right. I’ll do it tomorrow (the day’s over for me).