Figma Plugin For Images

Remove the burden of uploading images when you search for and add images to your Figma projects. The plugin allows you to select image files from your computer or dropbox and insert the selected image into your Figma file without needing to upload the file.

This plugin has been built to save you time and energy. It helps you upload and resize any images quickly, without having to open up your image editor.

Figma is a plugin tool that allows you to quickly and seamlessly upload images to our site. This tool is still in beta stage, so please try it out and send us feedback!

Figma Plugin For Images

In this section we will look at how to manipulate images within a Figma plugin. We will learn how to retrieve an image from a document, how to decode the image to manipulate its bytes, and how to write a modified image back to the document. As a concrete example, we will show you how to invert the colors of an image.

As you probably know, images in Figma are stored inside the fills of an object. So we first need to retrieve the fills off the current selection.

Retrieve fills from current selection

async function invertImages(node) {
const newFills = []
for (const paint of node.fills) {
if (paint.type === 'IMAGE') {
// Get the (encoded) bytes for this image.
const image = figma.getImageByHash(paint.imageHash)
const bytes = await image.getBytesAsync()

// TODO: Do something with the bytes!
}
}
node.fills = newFills
}

// Assume the current selection has fills.
// In an actual plugin, this won't necessarily be true!
const selected = figma.currentPage.selection[0] as GeometryMixin
invertImages(selected)

Calling getBytesAsync returns the raw bytes of the image, as it is stored on disk. This can be useful if you want to download the image or upload it somewhere else. However, in our case, we want to manipulate the image pixel-by-pixel, as a matrix of RGBA samples. Doing so requires decoding the image.

This could be done by importing some image decoding library. Instead, what we’ll do is ask the browser to decode the image for us. We can do this by putting the image in a <canvas> element which will give us access to the getImageData and putImageData functions provided by browsers. However, you may recall from the execution model section that you will need to create an <iframe> to access browser APIs. We’ll do that later: for now, here is our extended plugin code that sends the original image to the (yet-to-be-implemented) worker, receives a modified image, and replaces the image fill:

code.ts: Send image data to ui

async function invertImages(node) {
const newFills = []
for (const paint of node.fills) {
if (paint.type === 'IMAGE') {
// Get the (encoded) bytes for this image.
const image = figma.getImageByHash(paint.imageHash)
const bytes = await image.getBytesAsync()

// Create an invisible iframe to act as a "worker" which
// will do the task of decoding and send us a message
// when it's done.
figma.showUI(__html__, { visible: false })

// Send the raw bytes of the file to the worker.
figma.ui.postMessage(bytes)

// Wait for the worker's response.
const newBytes = await new Promise((resolve, reject) => {
figma.ui.onmessage = value => resolve(value)
})

// Create a new paint for the new image.
const newPaint = JSON.parse(JSON.stringify(paint))
newPaint.imageHash = figma.createImage(newBytes).hash
newFills.push(newPaint)
}
}
node.fills = newFills
}

Now we will implement the worker that will actually decode, modify, and encode the image. Remember, this code needs to be in a separate file referred to by the ui section of the manifest. This worker will need to listen to messages from the plugin code and respond with a message back once it’s accomplished its task. We will implement encode and decode later.

ui.html: Decode, modify, and encode image

<script>
// Create an event handler to receive messages from the main
// thread.
window.onmessage = async (event) => {
// Just get the bytes directly from the pluginMessage since
// that's the only type of message we'll receive in this
// plugin. In more complex plugins, you'll want to check the
// type of the message.
const bytes = event.data.pluginMessage

const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')

const imageData = await decode(canvas, ctx, bytes)
const pixels = imageData.data

// Do the actual work of inverting the colors.
for (let i = 0; i < pixels.length; i += 4) {
pixels[i + 0] = 255 - pixels[i + 0]
pixels[i + 1] = 255 - pixels[i + 1]
pixels[i + 2] = 255 - pixels[i + 2]
// Don't invert the alpha channel.
}

const newBytes = await encode(canvas, ctx, imageData)
window.parent.postMessage({pluginMessage: newBytes}, '*')
}
</script>

As you can see in the example above, we create a <canvas> object. The handle for the data backing <canvas> is called a context and is obtained with canvas.getContext('2d'). The context allows us to retrieve and write ImageData objects. ImageData objects have a .data field which is an array containing the colors of each sample (or pixel) in the image in sequence, stored as [R, G, B, A, R, G, B, A, ...]. To invert the colors int the image, we take each color channel value and replace it with 255 - value.

Provided below are the implementations of the encode and decode functions.

Encode and decode images

// Encoding an image is also done by sticking pixels in an
// HTML canvas and by asking the canvas to serialize it into
// an actual PNG file via canvas.toBlob().
async function encode(canvas, ctx, imageData) {
ctx.putImageData(imageData, 0, 0)
return await new Promise((resolve, reject) => {
canvas.toBlob(blob => {
const reader = new FileReader()
reader.onload = () => resolve(new Uint8Array(reader.result))
reader.onerror = () => reject(new Error('Could not read from blob'))
reader.readAsArrayBuffer(blob)
})
})
}

// Decoding an image can be done by sticking it in an HTML
// canvas, as we can read individual pixels off the canvas.
async function decode(canvas, ctx, bytes) {
const url = URL.createObjectURL(new Blob([bytes]))
const image = await new Promise((resolve, reject) => {
const img = new Image()
img.onload = () => resolve(img)
img.onerror = () => reject()
img.src = url
})
canvas.width = image.width
canvas.height = image.height
ctx.drawImage(image, 0, 0)
const imageData = ctx.getImageData(0, 0, image.width, image.height)
return imageData
}

The example above won’t work for GIFs, which are more like video files than image files. In fact, <canvas> doesn’t really support GIFs. You will need to use a third-party Javascript library capable of encoding and decoding GIFs.

Leave a Comment