Workaround for Saving to Camera Roll


--# 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

--# SaveToCameraRoll
function saveToCameraRoll(img)
    -- Font for styling output
    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>'
    
    local socket = require "socket"
    
    -- Save image temporarily
    saveImage("Documents:temp",img)
    
    -- 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 to read raw data
    local data = io.open(path, "rb")
    local content = data:read("*all")
    data:close()
    local img = image(content)
    
    -- Generate html
    local html = template
    :gsub("{{textFont}}",textFont)
    :gsub("{{src}}","img.png")
    
    print "Opening browser... \
Follow the instructions that appear."
    
    -- Create a webserver that serves two requests: one for the html page and one for the image
    server = assert(socket.tcp()) -- create a simple tcp master object
    assert(server:bind("localhost", 0)) -- bind it to localhost, at a random port
    server:listen(5) -- listen for connections (backlog 5)
    server:settimeout(1) -- max timeout 1 second, so we don't get stuck in an infinite waiting loop
    
    local ip, port = server:getsockname() -- get the ip and port
    openURL("http://"..ip..":"..port, false) -- open Safari
    
    -- This should loop twice
    while true do
        local client,err = server:accept() -- wait for connections
    
        if client then
            local line, err = client:receive() -- get data from client
            local route = line:match("GET (/[^ ]*)")
            
                
            if not err then
                -- Any other path besides root will be served the image, then the loop will end and the server will close
                if route ~= "/" then
                    local f = assert(io.open(path, "rb"))
                    local current = f:seek()
                    local length = f:seek("end") -- get the length of the image file
                    f:seek("set", current) -- go back to original position
                    
                    -- Send initial headers
                    client:send(
                        ("HTTP/1.0 200 OK\
Content-Length: %d\
Content-Type: image/png\
Connection: close\
\
")
                        :format(length)
                    )
                    
                    -- Read the file in chunks
                    local size = 2^13 -- good buffer size (8K)
                    while true do
                        local block = f:read(size)
                        if not block then break end
                        client:send(block)
                    end
                    f:close()
                    
                    break
                end 
                
                client:send(
                    ("HTTP/1.0 200 OK\
Content-Length: %d\
Content-Type: text/html\
Connection: close\
\
")
                    :format(#html)
                )
                client:send(html)
            end
             client:close()
        else
            break
        end
    end
    
    -- Final cleanup
    os.remove(path) -- remove file
    server:close() -- kill the server
    collectgarbage()
end

saveToCameraRoll is a function that gets a codeaimage and provides a way for the user to save it as a photo in the Photos app. The process is:

  • The image is saved temporarily, and its raw data is read by io
  • A server is created that serves the image file and some html
  • The user is directed to the server’s webpage, from where the image can be held until the “save to camera roll” dialog appears

I included a sample drawing app for testing.

@em2 This is an interesting way of saving the image. Sometimes I would get an error saying it couldn’t connect to the server, but most of the time it worked OK. I made a change for myself. Instead of using a triple tap to save the image, I used parameter.action(“Save image”,saveCanvas) and added the function saveCanvas() below. So instead of a triple tap, I just press the Save image parameter button. Even though this works, doing a screen snapshot is a little easier. Maybe a Codea function could be added in a future version to save the screen image to the camera roll. Anyways, this is a good example for everyone to look at, including myself.

function saveCanvas()
    saveToCameraRoll(canvas)
end

PS I increased the settimeout value to 3 and haven’t received the server error yet.

@dave1707 good tip for the timeout value, thanks.
I agree screenshotting might be easier, but I find this function useful for saving PNG’s with alpha or with higher resolution.
The same principle may also be useful for serving up other kinds of files, now that I think of it… Hmm…

hm, that’s actually quite interesting approach! thank you for sharing.