Skip to content

Commit

Permalink
Find vectors and export the image
Browse files Browse the repository at this point in the history
  • Loading branch information
yiqunw700 committed Sep 10, 2024
1 parent ceb9c07 commit 14569be
Show file tree
Hide file tree
Showing 3 changed files with 230 additions and 8 deletions.
4 changes: 4 additions & 0 deletions support-figma/extended-layout-plugin/src/image-res-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* limitations under the License.
*/
import * as Utils from "./utils";
import * as VectorRes from "./vector-res-module";

const IMAGE_HASH_TO_RES_KEY = "image_hash_to_res";

Expand Down Expand Up @@ -43,13 +44,16 @@ export async function exportAllImagesAsync() {
}

save();
let {vectorResToImageBytesMap, vectorResToNodesMap} = await VectorRes.exportAllVectorsAsync();

figma.showUI(__html__, { width: 600, height: 600 });
figma.ui.postMessage({
msg: "image-export",
imageBytesArray: Array.from(imageBytesMap),
imageNodesArray: Array.from(imageHashToNodesMap),
imageResNameArray: Array.from(imageHashToResMap),
vectorBytesArray: Array.from(vectorResToImageBytesMap),
vectorResNameArray: Array.from(vectorResToNodesMap),
});
}

Expand Down
54 changes: 46 additions & 8 deletions support-figma/extended-layout-plugin/src/ui.html
Original file line number Diff line number Diff line change
Expand Up @@ -1341,7 +1341,7 @@

// jpeg and png will be downloaded as png. Gif will be downloaded as gif. Other
// image types are not supported.
async function processImage(imageHash, imageBytes) {
async function processImage(imageBytes) {
const imageType = getImageType(imageBytes);
switch(imageType) {
case 'png':
Expand All @@ -1366,7 +1366,7 @@
}

// Display the images
async function createImageOutput(imageNodesArray, imageResNameArray, imageBytesArray) {
async function createImageOutput(imageNodesArray, imageResNameArray, imageBytesArray, vectorResNameArray, vectorBytesArray) {
const imageInfo = document.getElementById('imageInfo');
imageInfo.innerHTML = "";

Expand Down Expand Up @@ -1400,7 +1400,7 @@
resNameElement.appendChild(input);
imageInfo.appendChild(resNameElement);

let {element, outputImgBytes, imageFormat} = await processImage(imageHash, imageBytes);
let {element, outputImgBytes, imageFormat} = await processImage(imageBytes);
imageInfo.appendChild(element);

for (const nodeId of imageNodesMap.get(imageHash)) {
Expand All @@ -1417,6 +1417,46 @@
jszip.file(`${resName}.${imageFormat}`, outputImgBytes, {base64: true});
}

let vectorResMap = new Map(vectorResNameArray);

for (const [vectorResName, vectorBytes] of vectorBytesArray) {
const resNameElement = document.createElement('div');
resNameElement.style.marginTop = '8px';
resNameElement.textContent = 'Image res name: ';
const resName = vectorResName;
const input = document.createElement("input");
input.addEventListener('change', function (evt) {
const newValue = this.value;
parent.postMessage({
pluginMessage: {
msg: 'update-vector-res-name',
resName: newValue,
nodes: vectorResNameMap.get(vectorResName)
}
}, '*');
});
input.setAttribute("type", "text");
input.setAttribute("value", resName);
resNameElement.appendChild(input);
imageInfo.appendChild(resNameElement);

let {element, outputImgBytes, imageFormat} = await processImage(vectorBytes);
imageInfo.appendChild(element);

for (const nodeId of vectorResMap.get(vectorResName)) {
const nodeIdElement = document.createElement('div');
nodeIdElement.style.marginTop = '8px';
nodeIdElement.textContent = "Node id: ";
nodeIdElement.appendChild(createNodeIdSpan(nodeId));
imageInfo.appendChild(nodeIdElement);
}

const divider = document.createElement('hr');
imageInfo.appendChild(divider);

jszip.file(`${resName}.${imageFormat}`, outputImgBytes, {base64: true});
}

const downloadButton = document.getElementById('downloadImageButton');

downloadButton.addEventListener('click', ()=>{
Expand Down Expand Up @@ -1570,10 +1610,8 @@
}
else if (msg.msg == 'meters-selection') {
setMeterData(msg);
}

if (msg.msg == 'localization') {
openPage('localizationOptions')
} else if (msg.msg == 'localization') {
openPage('localizationOptions');
} else if (msg.msg == 'localization-output') {
loadOutputStringData(msg.output);
createOutputStringTable('outputStrings');
Expand All @@ -1585,7 +1623,7 @@
createOutputStringTable('outputStrings');
} else if (msg.msg == 'image-export') {
openPage('imageExport');
createImageOutput(msg.imageNodesArray, msg.imageResNameArray, msg.imageBytesArray);
createImageOutput(msg.imageNodesArray, msg.imageResNameArray, msg.imageBytesArray, msg.vectorResNameArray, msg.vectorBytesArray);
}

}
Expand Down
180 changes: 180 additions & 0 deletions support-figma/extended-layout-plugin/src/vector-res-module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
/**
* Copyright 2024 Google LLC
*
* 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.
*/
import * as Utils from "./utils";

const IMAGE_REPLACEMENT_RES_NAME = "image_replacement_res_name";

// Node id to vector root(frame node)
const vectorFrames = new Map<string, SceneNode>();
// ResName to image bytes map
const resToImageBytesMap = new Map<string, Uint8Array>();
// ResName to node id array map
const resToNodesMap = new Map<string, Array<string>>();

export async function exportAllVectorsAsync(): Promise<{
vectorResToImageBytesMap: Map<string, Uint8Array>;
vectorResToNodesMap: Map<string, Array<string>>;
}> {
await figma.loadAllPagesAsync();

vectorFrames.clear();
resToImageBytesMap.clear();
resToNodesMap.clear();

const vectorNodes = figma.root.findAll((node) => node.type === "VECTOR");

for (const vectorNode of vectorNodes) {
const frameAncestor = findFrameAncestor(vectorNode);
if (frameAncestor && isVectorRoot(frameAncestor)) {
vectorFrames.set(frameAncestor.id, frameAncestor);
}
}

for (const [frameNodeId, frameNode] of vectorFrames) {
const imageBytes = await frameNode.exportAsync({format: "PNG", colorProfile: "SRGB", contentsOnly: true });
const cachedResName = getResName(frameNode);
var resName = cachedResName
? cachedResName
: `ic_${Utils.toSnakeCase(frameNode.name)}`;
var index = 0;
while (resToImageBytesMap.has(resName)) {
index += 1;
resName = resName + "_" + index;
}
resToImageBytesMap.set(resName, imageBytes);
resToNodesMap.set(resName, [frameNodeId]);
setResName(frameNode, resName);
}

return { vectorResToImageBytesMap: resToImageBytesMap, vectorResToNodesMap: resToNodesMap };
}

function getResName(node: SceneNode): string {
return node.getSharedPluginData(
Utils.SHARED_PLUGIN_NAMESPACE,
IMAGE_REPLACEMENT_RES_NAME
);
}

function setResName(node: SceneNode, resName: string) {
node.setSharedPluginData(
Utils.SHARED_PLUGIN_NAMESPACE,
IMAGE_REPLACEMENT_RES_NAME,
resName
);
}

function convertToVector(node: SceneNode): string {
if (node.type === "VECTOR") {
const fills = node.fills as ReadonlyArray<Paint>;
if (fills) {
for (const fill of fills) {
if (fill.type === "SOLID" && fill.visible !== false) {
for (const vectorPath of node.vectorPaths) {
let outputString = ` <path\n android:pathData="${vectorPath.data}"\n android:fillColor="${toHex(fill.color)}"/>\n`;
return outputString;
}
}
}
}
} else if (node.type === "GROUP") {
let outputString = ` <group android:translationX="${node.x}" android:translationY="${node.y}">\n`;
// Recurse into any children.
let maybeParent = node as ChildrenMixin;
if (maybeParent.children) {
for (let child of maybeParent.children) {
outputString = outputString.concat(convertToVector(child));
}
}
outputString = outputString.concat(" </group>\n");
return outputString;
} else if (node.type === "FRAME") {
let outputString = `<?xml version="1.0" encoding="utf-8"?>\n`;
outputString = outputString.concat(
`<vector xmlns:android="http://schemas.android.com/apk/res/android"\n android:width="${node.width}dp"\n android:height="${node.height}dp"\n android:viewportWidth="${node.width}"\n android:viewportHeight="${node.height}">\n`
);
// Recurse into any children.
let maybeParent = node as ChildrenMixin;
if (maybeParent.children) {
for (let child of maybeParent.children) {
outputString = outputString.concat(convertToVector(child));
}
}
outputString = outputString.concat("</vector>");
return outputString;
}
return "";
}

function findFrameAncestor(node: PageNode | SceneNode): FrameNode | undefined {
if (node.parent?.type === "FRAME") {
return node.parent!!;
}

if (node.parent?.type === "GROUP") {
return findFrameAncestor(node.parent!!);
}

return undefined;
}

function isVectorRoot(frameNode: FrameNode): boolean {
for (const childNode of frameNode.children) {
if (!isVectorOrTextOrVectorGroup(childNode)) {
return false;
}
}
return true;
}

// Returns true if the node is a vector node or a group node with vector as descendants.
function isVectorOrTextOrVectorGroup(node: SceneNode): boolean {
if (node.type === "VECTOR") {
return true;
}
if (node.type === "TEXT") {
return true;
}
if (node.type === "GROUP") {
return isVectorGroup(node);
}
return false;
}

function isVectorGroup(groupNode: GroupNode): boolean {
for (const childNode of groupNode.children) {
if (childNode.type === "GROUP") {
if (!isVectorGroup(childNode)) {
return false;
}
} else if (childNode.type !== "VECTOR" && childNode.type != "TEXT") {
return false;
}
}
return true;
}

function toHex(rgb: RGB) {
return rgbToHex(rgb.r, rgb.g, rgb.b);
}

function rgbToHex(r: number, g: number, b: number) {
const componentToHex = (c: number) => {
const hex = Math.round(c * 255).toString(16);
return hex.length == 1 ? "0" + hex : hex;
};
return "#" + componentToHex(r) + componentToHex(g) + componentToHex(b);
}

0 comments on commit 14569be

Please sign in to comment.