/Artal

A .PSD parsing library for LÖVE

Primary LanguageLuaMIT LicenseMIT

Artal

A .PSD parsing library for LÖVE

https://love2d.org/

Purpose is to expose the structure of .PSD files into LÖVE.

  • ImageData for the layers.
  • Names.
  • Blendmodes.
  • Clipping mode.
  • Structure, folder / image.

Adobe documentation on the PSD file format: http://www.adobe.com/devnet-apps/photoshop/fileformatashtml/

artal.lua

artal = require("artal")
psdTable       = artal.newPSD(FileNameOrFileData)              -- full structure with the layers loaded in as images.
psdTable       = artal.newPSD(FileNameOrFileData, "info")      -- full structure.
ImageDataOrNil = artal.newPSD(FileNameOrFileData, layerNumber) -- ImageData for the specified layer number.
ImageData      = artal.newPSD(FileNameOrFileData, "composed")
-- ImageData of the composed image as it's stored in the psd file itself.
-- Note that Photoshop has an slightly erroneous implementation composing the alpha into the composed image.
-- So images without a fully opaque background will be slightly blended with white.

Sample code:

local artal = require("artal")
love.graphics.setBackgroundColor(1, 1, 1)

img = artal.newPSD("sample.psd")

function love.draw()
    for i = 1, #img do
        love.graphics.draw(
            img[i].image,
            nil,       -- Position X
            nil,       -- Position Y
            nil,       -- Rotation
            nil,       -- Scale X
            nil,       -- Scale Y
            img[i].ox, -- Offset X
            img[i].oy) -- Offset Y
    end
end

Full structure artal extracts from the psd.

local artal = require("artal")
love.graphics.setBackgroundColor(1, 1, 1)

img = artal.newPSD("sample.psd")

function love.draw()

    -- Image info.
    love.graphics.setColor(0, 0, 0)
    love.graphics.print("Global Image info",         0, 14 * 0)
    love.graphics.print("Layer Count: "..#img,       0, 14 * 1)
    love.graphics.print("Width: "..      img.width,  0, 14 * 2)
    love.graphics.print("Height: "..     img.height, 0, 14 * 3)
    for i = 1, #img do
        love.graphics.setColor(0, 0, 0)
        love.graphics.print("Layer index: "..i,                        (i - 1) * 200, 70 + 14 * 0)
        love.graphics.print("name: "..       img[i].name,              (i - 1) * 200, 70 + 14 * 1)
        love.graphics.print("type: "..       img[i].type,              (i - 1) * 200, 70 + 14 * 2)
        love.graphics.print("blend: "..      img[i].blend,             (i - 1) * 200, 70 + 14 * 3)
        love.graphics.print("clip: "..       tostring(img[i].clip),    (i - 1) * 200, 70 + 14 * 4)
        love.graphics.print("ox: "..         img[i].ox,                (i - 1) * 200, 70 + 14 * 5)
        love.graphics.print("oy: "..         img[i].oy,                (i - 1) * 200, 70 + 14 * 6)
        love.graphics.print("getWidth: "..   img[i].image:getWidth(),  (i - 1) * 200, 70 + 14 * 7)
        love.graphics.print("getHeight: "..  img[i].image:getHeight(), (i - 1) * 200, 70 + 14 * 8)
        
        -- Bounding Boxes
        love.graphics.rectangle(
            "line",
            (i - 1) * 200 - img[i].ox - 0.5,
            70 + 14 * 9   - img[i].oy - 0.5,
            img[i].image:getWidth()  + 1,
            img[i].image:getHeight() + 1)

        love.graphics.setColor(1, 1, 1)
        love.graphics.draw(
            img[i].image,
            (i - 1) * 200,
            70 + 14 * 9,
            nil,
            nil,
            nil,
            img[i].ox,
            img[i].oy)
    end
    
end

Layer Types

These are the type of layers

"image" = image layer with imagedata.
"empty" = image layer without imagedata.
"open"  = beginning of group layer.
"close" = end of group layer.

Layers between an "open" and a "close" are nested inside that group.

Blend Modes

These are all blendmodes available.

There's sample code for these first 5 blendmodes.
"norm" = normal
"pass" = pass through
"mul"  = multiply
"scrn" = screen
"over" = overlay

"diss" = dissolve
"dark" = darken
"idiv" = color burn
"lbrn" = linear burn
"dkCl" = darker color
"lite" = lighten
"div"  = color dodge
"lddg" = linear dodge
"lgCl" = lighter color
"sLit" = soft light
"hLit" = hard light
"vLit" = vivid light
"lLit" = linear light
"pLit" = pin light
"hMix" = hard mix
"diff" = difference
"smud" = exclusion
"fsub" = subtract
"fdiv" = divide
"hue"  = hue
"sat"  = saturation
"colr" = color
"lum"  = luminosity

Loading specific layers.

local artal = require("artal")

love.graphics.setBackgroundColor(1, 1, 1)

local fileData = love.filesystem.newFileData("sample.psd")
img = artal.newPSD(fileData,"info")

for i = 1, #img do
    if img[i].type == "image" and string.find(img[i].name, "Blob") then -- Only load layers with Blob in the name
        img[i].image = love.graphics.newImage(artal.newPSD(fileData, i))
    end
end

function love.draw()
    for i = 1, #img do
        if img[i].image then
            love.graphics.draw(
                img[i].image,
                nil,
                nil,
                nil,
                nil,
                nil,
                img[i].ox,
                img[i].oy)
        end
    end
end

writetable.lua

Create a string from tables. So you can inspect tables created by artal.newPSD(). The structure below is generated from writetable.lua. And you can use that to visualize your own tables as well.

local writetable = require("writetable")
tableAsString = writetable.createStringFromTable(table)
{
    -- Table with 4 indexes, and 2 string keys.
    -- Array values are all of type: "table".
    height = 200,
    width = 200,
    [1] = 
    {
        -- Table with 7 string keys.
        oy = 0,
        image = "Image: 0x6b119bff80",
        ox = 0,
        type = "image",
        blend = "norm",
        name = "Background",
        clip = false,
    },
    [2] = 
    {
        -- Table with 7 string keys.
        oy = -40,
        image = "Image: 0x6b119c0140",
        ox = -68,
        type = "image",
        blend = "norm",
        name = "Red Blob",
        clip = false,
    },
    [3] = 
    {
        -- Table with 7 string keys.
        oy = -17,
        image = "Image: 0x6b119c0220",
        ox = -95,
        type = "image",
        blend = "norm",
        name = "Blue Blob",
        clip = true,
    },
    [4] = 
    {
        -- Table with 7 string keys.
        oy = -27,
        image = "Image: 0x6b12844c80",
        ox = -8,
        type = "image",
        blend = "over",
        name = "Multiple Blobs",
        clip = true,
    },
}

psdShader.lua:

NOTE: This library is very much incomplete. But it's at least a starting point for you to understand how you can create shaders that mimics photoshop effects.

It generates shaders for blending and clipping layers. Blendmodes implemented: Alpha, Multiply, Screen and Overlay.

It may require a "Swap canvas" system If you need a global blend mode. See below for samples.

local psdShader = require("psdShader")

-- If you pass in "mul", "scrn" or "over" to globalBlendmode the shader generated will require a swapcanvas.
shaderString = psdShader.createShaderString(globalBlendmode, blendmodeBeingClipped, ...)

-- Passing in canvas is only required if you use a shader with globalBlendmode: mul, scrn, over.
psdShader.setShader(shader, canvas1, canvas2) 

-- Rotation and shearing not implemented. love.graphics.push() and friends does not work either.
-- The image that is passed in is also retained. Unlike love.graphics.draw().
psdShader.drawClip(drawOrderIndex, image, x, y, r, sx, sy, ox, oy, kx, ky) 
resultCanvas = psdShader.flatten(psdTableClipTo, psdTableBeingClipped, ...)

Clipping sample

local artal     = require("artal")
local psdShader = require("psdShader")

love.graphics.setBackgroundColor(1, 1, 1)

img = artal.newPSD("sample.psd")

local blendShader = {}
blendShader.clip = love.graphics.newShader(psdShader.createShaderString("norm", "norm", "over"))

function love.draw()
    love.graphics.draw(   img[1].image, nil, nil, nil, nil, nil, img[1].ox, img[1].oy)
    psdShader.setShader(blendShader.clip)
    psdShader.drawClip(1, img[3].image, nil, nil, nil, nil, nil, img[3].ox, img[3].oy)
    psdShader.drawClip(2, img[4].image, nil, nil, nil, nil, nil, img[4].ox, img[4].oy)
    love.graphics.draw(   img[2].image, nil, nil, nil, nil, nil, img[2].ox, img[2].oy)
    love.graphics.setShader()
end

Blendmode sample

local artal     = require("artal")
local psdShader = require("psdShader")
love.graphics.setBackgroundColor(1, 1, 1)

img = artal.newPSD("sample.psd")

local blendShader = {}
blendShader.mul  = love.graphics.newShader(psdShader.createShaderString("mul"))
blendShader.scrn = love.graphics.newShader(psdShader.createShaderString("scrn"))
blendShader.over = love.graphics.newShader(psdShader.createShaderString("over"))

local canvas = {} -- These blendmode all requires a swap canvas
canvas[1] = love.graphics.newCanvas(love.graphics.getDimensions())
canvas[2] = love.graphics.newCanvas(love.graphics.getDimensions())

function love.draw()

    love.graphics.setCanvas(canvas[1])
    love.graphics.clear(1, 1, 1)

    for i = 1, #img do
        if  img[i].blend == "mul" or
            img[i].blend == "over" or
            img[i].blend == "scrn" then
            
            psdShader.setShader(blendShader[img[i].blend], canvas[1], canvas[2])
        end
        love.graphics.draw(img[i].image, nil, nil, nil, nil, nil, img[i].ox, img[i].oy)
        love.graphics.setShader()
    end
    
    -- Draw result to screen
    local preCanvas = love.graphics.getCanvas()
    love.graphics.setCanvas(nil)
    love.graphics.setBlendMode("alpha", "premultiplied")
    love.graphics.draw(preCanvas)
    love.graphics.setBlendMode("alpha")
end

Blend and Clipping

local artal     = require("artal")
local psdShader = require("psdShader")

love.graphics.setBackgroundColor(1, 1, 1)

img = artal.newPSD("sample.psd")

local blendShader = {}
blendShader.clipAndBlend = love.graphics.newShader(psdShader.createShaderString("mul", "over", "scrn"))

local canvas = {} -- These blendmode all requires a swap canvas
canvas[1] = love.graphics.newCanvas(love.graphics.getDimensions())
canvas[2] = love.graphics.newCanvas(love.graphics.getDimensions())

function love.draw()
    love.graphics.setCanvas(canvas[1])

    love.graphics.clear(1, 1, 1)

    love.graphics.draw(   img[1].image, nil, nil, nil, nil, nil, img[1].ox, img[1].oy)
    psdShader.setShader(blendShader.clipAndBlend, canvas[1], canvas[2])
    psdShader.drawClip(1, img[3].image, nil, nil, nil, nil, nil, img[3].ox, img[3].oy)
    psdShader.drawClip(2, img[4].image, nil, nil, nil, nil, nil, img[4].ox, img[4].oy)
    love.graphics.draw(   img[2].image, nil, nil, nil, nil, nil, img[2].ox, img[2].oy)
    love.graphics.setShader()
    
    -- Draw result to screen
    local preCanvas = love.graphics.getCanvas()
    love.graphics.setCanvas(nil)
    love.graphics.setBlendMode("alpha", "premultiplied")
    love.graphics.draw(preCanvas)
    love.graphics.setBlendMode("alpha")
end