Skip to content

Commit

Permalink
feat: Extract PNG images by scale (#32)
Browse files Browse the repository at this point in the history
* feat(figma-plugin): png scale export

* feat(generator): generate png scales folder

* chore: changeset
  • Loading branch information
junghyeonsu authored Dec 6, 2023
1 parent 9983622 commit b9e4030
Show file tree
Hide file tree
Showing 13 changed files with 307 additions and 99 deletions.
7 changes: 7 additions & 0 deletions .changeset/wet-cameras-argue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@icona/generator": minor
"@icona/types": minor
"@icona/utils": minor
---

feat: Extract PNG images by scale
2 changes: 1 addition & 1 deletion figma-plugin/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ export const FRAME_NAME = "icona-frame";
export const KEY = {
GITHUB_API_KEY: "github-api-key",
GITHUB_REPO_URL: "github-repo-url",
DEPLOY_WITH_PNG: "deploy-with-png",
PNG_OPTION: "png-option",
};
4 changes: 3 additions & 1 deletion figma-plugin/common/fromPlugin.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { emit as e, on as o } from "@create-figma-plugin/utilities";
import type { IconaIconData } from "@icona/types";

import type { ExportOptions } from "./types.js";

interface UserInfoPayload {
name: string;
id: string;
Expand All @@ -15,7 +17,7 @@ interface GetGithubApiKeyPayload {
}

interface GetDeployWithPngPayload {
deployWithPng?: boolean;
options: ExportOptions;
}

interface GetIconPreviewPayload {
Expand Down
8 changes: 4 additions & 4 deletions figma-plugin/common/fromUi.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { emit as e, on as o } from "@create-figma-plugin/utilities";
import type { IconaIconData } from "@icona/types";

import type { ExportOptions } from "./types.js";

interface GithubData {
owner: string;
name: string;
Expand All @@ -10,13 +12,11 @@ interface GithubData {
interface IconaMetaData {
githubData: GithubData;
icons: Record<string, IconaIconData>;
options?: {
withPng?: boolean;
};
options: ExportOptions;
}

interface SetPngOptionPayload {
withPng: boolean;
options: ExportOptions;
}

interface SetGithubUrlPayload {
Expand Down
10 changes: 6 additions & 4 deletions figma-plugin/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ export interface GithubData {
apiKey: string;
}

export interface IconaMetaData {
githubData: GithubData;
options?: {
withPng?: boolean;
export interface ExportOptions {
png: {
x1: boolean;
x2: boolean;
x3: boolean;
x4: boolean;
};
}
20 changes: 11 additions & 9 deletions figma-plugin/plugin-src/code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
listenSetGithubApiKey,
listenSetGithubUrl,
} from "./listeners";
import { getAssetInIconFrame } from "./service";
import { getAssetFramesInFrame, getSvgFromExtractedNodes } from "./service";
import { getLocalData } from "./utils";

function sendUserInfo() {
Expand All @@ -21,11 +21,15 @@ function sendUserInfo() {
async function sendStorageData() {
const repoUrl = await getLocalData(KEY.GITHUB_REPO_URL);
const apiKey = await getLocalData(KEY.GITHUB_API_KEY);
const deployWithPng = await getLocalData(KEY.DEPLOY_WITH_PNG);
const pngOption = await getLocalData(KEY.PNG_OPTION);

emit("GET_GITHUB_REPO_URL", { repoUrl });
emit("GET_GITHUB_API_KEY", { apiKey });
emit("GET_DEPLOY_WITH_PNG", { deployWithPng });
emit("GET_DEPLOY_WITH_PNG", {
options: pngOption || {
png: { x1: false, x2: false, x3: false, x4: false },
},
});
}

async function setPreviewIcons() {
Expand All @@ -37,13 +41,11 @@ async function setPreviewIcons() {
figma.notify("Icona frame not found");
return;
} else {
const svgDatas = await getAssetInIconFrame(iconaFrame.id, {
withPng: await getLocalData(KEY.DEPLOY_WITH_PNG),
});
const targetFrame = figma.getNodeById(iconaFrame.id) as FrameNode;
const assetFrames = getAssetFramesInFrame(targetFrame);
const datas = await getSvgFromExtractedNodes(assetFrames);

emit("GET_ICON_PREVIEW", {
icons: svgDatas,
});
emit("GET_ICON_PREVIEW", { icons: datas });
}
}

Expand Down
30 changes: 25 additions & 5 deletions figma-plugin/plugin-src/listeners.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,36 @@
import { KEY } from "../common/constants.js";
import { FRAME_NAME, KEY } from "../common/constants.js";
import { emit } from "../common/fromPlugin.js";
import { on } from "../common/fromUi.js";
import { createGithubClient } from "./github.js";
import { exportFromIconaIconData, getAssetFramesInFrame } from "./service.js";
import { setLocalData } from "./utils.js";

export function listenDeployIcon() {
on("DEPLOY_ICON", async ({ githubData, icons }) => {
on("DEPLOY_ICON", async ({ githubData, icons, options }) => {
try {
const { owner, name, apiKey } = githubData;
const pngOption = options.png;

const { createDeployPR } = createGithubClient(owner, name, apiKey);

await createDeployPR(icons);
const iconaFrame = figma.currentPage.findOne((node) => {
return node.name === FRAME_NAME;
});

if (!iconaFrame) {
figma.notify("Icona frame not found");
return;
}

const targetFrame = figma.getNodeById(iconaFrame.id) as FrameNode;
const assetFrames = getAssetFramesInFrame(targetFrame);

const iconaData = await exportFromIconaIconData(assetFrames, icons, {
png: pngOption,
});

await createDeployPR(iconaData);

emit("DEPLOY_DONE", null);
figma.notify("Icons deployed", { timeout: 5000 });
} catch (error) {
Expand All @@ -35,8 +54,9 @@ export function listenSetGithubUrl() {
setLocalData(KEY.GITHUB_REPO_URL, url);
});
}

export function listenPngOption() {
on("SET_PNG_OPTION", ({ withPng }) => {
setLocalData(KEY.DEPLOY_WITH_PNG, withPng);
on("SET_PNG_OPTION", ({ options }) => {
setLocalData(KEY.PNG_OPTION, options);
});
}
119 changes: 78 additions & 41 deletions figma-plugin/plugin-src/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@
import type { IconaIconData } from "@icona/types";
import { Base64 } from "js-base64";

import type { ExportOptions } from "../common/types";

type TargetNode =
| ComponentNode
| InstanceNode
| VectorNode
| ComponentSetNode
| FrameNode
| GroupNode;
type Extracted = {
type ExtractedNode = {
id: string;
name: string;
};
Expand Down Expand Up @@ -41,7 +43,7 @@ const makeComponentName = ({
const findComponentInNode = (
node: TargetNode,
setName?: string,
): Extracted | Extracted[] => {
): ExtractedNode | ExtractedNode[] => {
switch (node.type) {
case "FRAME":
case "GROUP":
Expand Down Expand Up @@ -70,17 +72,8 @@ const findComponentInNode = (
}
};

export async function getAssetInIconFrame(
iconFrameId: string,
options?: {
withPng?: boolean;
},
): Promise<Record<string, IconaIconData>> {
const frame = figma.getNodeById(iconFrameId) as FrameNode;

const withPng = options?.withPng ?? true;

const targetNodes = frame.children.flatMap((child) => {
export function getAssetFramesInFrame(targetFrame: FrameNode): ExtractedNode[] {
const targetNodes = targetFrame.children.flatMap((child) => {
if (
child.type === "COMPONENT" ||
child.type === "INSTANCE" ||
Expand All @@ -94,43 +87,26 @@ export async function getAssetInIconFrame(
return [];
});

const targetComponents = targetNodes.filter((component) => component);
return targetNodes.filter((component) => component);
}

export async function getSvgFromExtractedNodes(nodes: ExtractedNode[]) {
const datas = await Promise.allSettled(
targetComponents.map(async (component) => {
const data = {} as IconaIconData;
nodes.map(async (component) => {
const node = figma.getNodeById(component.id) as ComponentNode;

// base
data.style = {
width: node.width,
height: node.height,
return {
name: component.name,
svg: await node.exportAsync({
format: "SVG_STRING",
svgIdAttribute: true,
}),
};
data.name = component.name;

// svg
const svg = await node.exportAsync({
format: "SVG_STRING",
svgIdAttribute: true,
});
data.svg = svg;

// png
if (withPng) {
const png = await node.exportAsync({ format: "PNG" });
const base64String = Base64.fromUint8Array(png);
data.png = base64String;
}

return data;
}),
);

const dataMap = datas.reduce((acc, cur) => {
if (cur.status === "rejected") {
console.error(cur.reason);
}

if (cur.status === "rejected") console.error(cur.reason);
if (cur.status === "fulfilled") {
const { name, ...rest } = cur.value as IconaIconData;
acc[name] = {
Expand All @@ -144,3 +120,64 @@ export async function getAssetInIconFrame(

return dataMap;
}

export async function exportFromIconaIconData(
nodes: ExtractedNode[],
iconaData: Record<string, IconaIconData>,
options: ExportOptions,
) {
const result = iconaData;

nodes.forEach(async (component) => {
const node = figma.getNodeById(component.id) as ComponentNode;

const exportDatas = await Promise.allSettled(
Object.entries(options.png).map(async ([key, value]) => {
const scale = Number(key.replace("x", ""));

if (!value) {
return {
scale: `${scale}x`,
data: "",
};
}

const exportData = await node.exportAsync({
format: "PNG",
constraint: {
type: "SCALE",
value: scale,
},
});

const base64String = Base64.fromUint8Array(exportData);

return {
scale: `${scale}x`,
data: base64String,
};
}),
);

const pngDatas = exportDatas.reduce((acc, cur) => {
if (cur.status === "rejected") console.error(cur.reason);
if (cur.status === "fulfilled") {
const { scale, data } = cur.value as {
scale: string;
data: string;
};
acc[scale] = data;
}

return acc;
}, {} as Record<string, string>);

// name = "icon_name"
result[component.name] = {
...result[component.name],
...pngDatas,
};
});

return result;
}
Loading

0 comments on commit b9e4030

Please sign in to comment.