Skip to content

Instantly share code, notes, and snippets.

@jbarr21
Last active February 6, 2023 22:24
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jbarr21/dcfc53136a5ff1511dd458831de7addf to your computer and use it in GitHub Desktop.
Save jbarr21/dcfc53136a5ff1511dd458831de7addf to your computer and use it in GitHub Desktop.
Export a color palette from Figma given a file with multiple tables of colors with semantic names on the row labels and theme names on the column names
/*
* Copyright (c) 2019. Uber Technologies
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#!/usr/bin/env kscript
@file:DependsOn("com.github.ajalt:clikt:1.5.0")
@file:DependsOn("com.squareup.moshi:moshi-kotlin:1.7.0")
@file:DependsOn("com.squareup.okhttp3:okhttp:3.11.0")
@file:DependsOn("de.vandermeer:asciitable:0.3.2")
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.option
import com.squareup.moshi.*
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import okhttp3.OkHttpClient
import okhttp3.Request
import java.net.URLEncoder
import java.util.concurrent.TimeUnit
import de.vandermeer.asciitable.AsciiTable
import java.io.File
object Consts {
const val figmaAuthToken = "YOUR_FIGMA_AUTH_TOKEN_HERE"
const val platformTrackerFileId = "YOUR_FIGMA_FILE_ID_HERE"
}
interface Node {
val id: String
val name: String
fun colorForCell(colorFrame: FigmaNode): FigmaColor
}
enum class BgColorNode(override val id: String) : Node {
GLOBAL("61:0");
BACKGROUND("70:9"),
ICON("94:361"),
BORDER("94:773");
override fun colorForCell(colorFrame: FigmaNode): FigmaColor {
val rectChild = colorFrame.children.firstOrNull { it.type == FigmaType.RECTANGLE.name }
val textChild = colorFrame.children.firstOrNull { it.type == FigmaType.TEXT.name }
return when {
rectChild?.backgroundColor != null -> rectChild.backgroundColor
rectChild?.fills?.first()?.color != null -> rectChild.fills.first().color
colorFrame.backgroundColor != null -> colorFrame.backgroundColor
textChild?.backgroundColor != null -> textChild.backgroundColor
else -> throw java.lang.IllegalStateException("No color for cell")
}
}
}
enum class FgColorNode(override val id: String) : Node {
TEXT("94:56");
override fun colorForCell(colorFrame: FigmaNode): FigmaColor {
val textChild = colorFrame.children.firstOrNull { it.type == FigmaType.TEXT.name }
return when {
textChild?.fills?.first()?.color != null -> textChild.fills.first().color
else -> throw java.lang.IllegalStateException("No color for cell")
}
}
}
enum class Theme {
RIDER_LIGHT,
RIDER_DARK,
EATS,
SAMPLE
}
class FigmaExporter : CliktCommand() {
private val ascii: Boolean by option("-a", "--ascii", help = "Local path to generated QR code in PNG format").flag(default = false)
private val html: Boolean by option("-h", "--html", help = "Local path to generated QR code in PNG format").flag(default = false)
private val json: Boolean by option("-j", "--json", help = "Public url of APK").flag(default = false)
override fun run() {
val nodes: List<Node> = BgColorNode.values().toMutableList() + FgColorNode.values()
nodes.map { parseColorSheet(Consts.platformTrackerFileId, it) }
.toList()
.let {
val outputAll = !ascii && !html && !json
if (outputAll || ascii) outputAsciiTables(it)
if (outputAll || html) outputHtmlTables(it)
if (outputAll || json) outputJson(it)
}
}
fun parseColorSheet(fileId: String, nodeId: Node, headers: List<String> = Theme.values().map { it.name }): ColorSheet {
val figmaNodeUrl = "https://api.figma.com/v1/files/$fileId/nodes?ids=${URLEncoder.encode(nodeId.id, Charsets.UTF_8.name())}"
val inputRows = FigmaApi.makeRequest(figmaNodeUrl).nodes.values.first().document.children
val outputRows = mutableListOf<List<ColorCell>>()
val outputRowLabels = mutableListOf<String>()
inputRows.reversed().filter { it.name != "HEADER" && it.children.isNotEmpty() }.forEach { inputRow ->
val outputRow = mutableListOf<ColorCell>()
var themeColumnIndex = 0
inputRow.children.forEachIndexed { index, colorFrame ->
if (index != 0 && colorFrame.type == FigmaType.FRAME.name) {
val textChild = colorFrame.children.firstOrNull { it.type == FigmaType.TEXT.name }
val colorName = textChild?.name ?: "unknown"
val color = nodeId.colorForCell(colorFrame)
outputRow += ColorCell(colorName, color)
themeColumnIndex++
}
}
if (outputRow.isNotEmpty()) {
outputRows += outputRow
outputRowLabels += inputRow.name
}
}
return ColorSheet(nodeId.name, headers, outputRowLabels, outputRows)
}
fun outputHtmlTables(colorSheets: List<ColorSheet>) {
val css = """
table { border-collapse: collapse; }
table, td, th { border: 1px solid white; alignment: center; padding: 20px; }
""".trimIndent()
val html = "<html><head><title>Figma Styles Export</title><style>{{CSS}}</style></head><body>{{TABLES}}</body></html>"
.replace("{{CSS}}", css)
.replace("{{TABLES}}", colorSheets.map { buildHtmlTable(it) }.joinToString("<br><br>"))
val outputFile = File("color-sheets.html")
outputFile.writeText(html)
println("\nWrote HTML to ${outputFile.absolutePath}")
}
private fun buildHtmlTable(colorSheet: ColorSheet): String {
val tableHeaders = (mutableListOf(colorSheet.name) + colorSheet.headers)
.map { "<th>${it.capitalize()}</th>" }
.joinToString("", prefix = "<tr>", postfix = "</tr>")
val tableRows = colorSheet.rows
.mapIndexed { index, row ->
val rowLabelCell = "<td>${colorSheet.rowLabels[index]}</td>"
val rowCells = row.map { "<td bgcolor='${it.color.hex()}'>${it.name}</td>" }
return@mapIndexed (mutableListOf(rowLabelCell) + rowCells)
.joinToString("", prefix = "<tr>", postfix = "</tr>")
}
.joinToString("")
return "<table>{{HEADERS}}{{ROWS}}</table>"
.replace("{{HEADERS}}", tableHeaders)
.replace("{{ROWS}}", tableRows)
}
fun outputAsciiTables(colorSheets: List<ColorSheet>) {
colorSheets.forEach { colorSheet ->
AsciiTable().apply {
addRule()
addRow(colorSheet.headers.toMutableList().apply { add(0, colorSheet.name) })
addRule()
colorSheet.rows.forEachIndexed { index, row ->
addRow(mutableListOf(colorSheet.rowLabels[index]) + row.map { "${it.name} (${it.color.rgb()})" })
addRule()
}
println("\n" + render(200))
}
}
}
}
FigmaExporter().main(args)
// Figma API
object FigmaApi {
val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
val okHttpClient = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.addNetworkInterceptor { it.proceed(it.request().newBuilder().addHeader("X-FIGMA-TOKEN", Consts.figmaAuthToken).build()) }
.build()
fun makeRequest(url: String): FigmaResponse {
return okHttpClient.newCall(Request.Builder().url(url).build())
.execute()
.body()
?.string()
?.let {
return@let moshi.adapter<FigmaResponse>(FigmaResponse::class.java).fromJson(it)
} ?: throw IllegalStateException("Unable to fetch requested data from Figma")
}
}
data class FigmaResponse(val name: String, val lastModified: String, val version: Long, val nodes: Map<String, FigmaDocument>)
data class FigmaDocument(val document: FigmaNode, val components: Map<String, Any>, val styles: Map<String, FigmaStyle>)
data class FigmaNode(
val id: String,
val name: String,
val type: String,
val blendMode: String,
val children: List<FigmaNode> = emptyList(),
val backgroundColor: FigmaColor?,
val fills: List<FigmaFill> = emptyList()
)
data class FigmaColor(val r: Double, val g: Double, val b: Double, val a: Double)
data class FigmaStyle(val name: String, val styleType: String)
data class FigmaFill(val type: String, val blendMode: String, val color: FigmaColor)
enum class FigmaType { FRAME, TEXT, RECTANGLE }
enum class FigmaBlendMode { SOLID, PASS_THROUGH }
// Utils
fun FigmaColor.rgb() = listOf(r, g, b).map { it.pctOf255() }.joinToString(", ")
fun FigmaColor.hex() = listOf(r, g, b).map { it.toHex() }.joinToString("", prefix = "#")
fun Double.pctOf255() = Math.round(this * 255)
fun Double.toHex() = pctOf255().toString(16).padStart(2, '0')
fun String.capitalize() = toLowerCase().split("[ \\_]".toRegex()).map { it[0].toUpperCase() + it.substring(1) }.joinToString(" ")
data class ColorSheet(val name: String, val headers: List<String>, val rowLabels: List<String>, val rows: List<List<ColorCell>>)
data class ColorCell(val name: String, val color: FigmaColor)
data class ColorMapping(val mapping: List<ColorTheme> = emptyList())
data class ColorTheme(val name: String, val groups: List<ColorGroup> = emptyList())
data class ColorGroup(val name: String, val colors: List<NamedColorCell> = emptyList())
data class NamedColorCell(val name: String, val hex: String, val r: Int, val g: Int, val b: Int)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment