0
Fork 0
mirror of https://github.com/penpot/penpot-exporter-figma-plugin.git synced 2025-01-03 05:10:13 -05:00

Add image library to optimize performance (#133)

* Add image library to optimize performance

* fix and improve

* Add changelog and improve naming

* refactor

* improve

* fix todos
This commit is contained in:
Jordi Sala Morales 2024-06-03 17:29:33 +02:00 committed by GitHub
parent a4113382bb
commit f726dc9cec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 122 additions and 32 deletions

View file

@ -0,0 +1,5 @@
---
"penpot-exporter": patch
---
Optimize image exporting when there are multiple copies of the same image in the file

View file

@ -0,0 +1,23 @@
import { ImageColor } from '@ui/lib/types/utils/imageColor';
class ImageLibrary {
private images: Record<string, ImageColor> = {};
public register(hash: string, image: ImageColor) {
this.images[hash] = image;
}
public get(hash: string): ImageColor | undefined {
return this.images[hash];
}
public all(): Record<string, ImageColor> {
return this.images;
}
public init(images: Record<string, ImageColor>): void {
this.images = images;
}
}
export const imagesLibrary = new ImageLibrary();

View file

@ -1,4 +1,5 @@
import { componentsLibrary } from '@plugin/ComponentLibrary'; import { componentsLibrary } from '@plugin/ComponentLibrary';
import { imagesLibrary } from '@plugin/ImageLibrary';
import { PenpotDocument } from '@ui/types'; import { PenpotDocument } from '@ui/types';
@ -27,6 +28,7 @@ export const transformDocumentNode = async (node: DocumentNode): Promise<PenpotD
return { return {
name: node.name, name: node.name,
children, children,
components: componentsLibrary.all() components: componentsLibrary.all(),
images: imagesLibrary.all()
}; };
}; };

View file

@ -1,3 +1,4 @@
import { imagesLibrary } from '@plugin/ImageLibrary';
import { detectMimeType } from '@plugin/utils'; import { detectMimeType } from '@plugin/utils';
import { Fill } from '@ui/lib/types/utils/fill'; import { Fill } from '@ui/lib/types/utils/fill';
@ -9,28 +10,46 @@ export const translateImageFill = async (fill: ImagePaint): Promise<Fill | undef
return { return {
fillOpacity: !fill.visible ? 0 : fill.opacity, fillOpacity: !fill.visible ? 0 : fill.opacity,
fillImage: fillImage fillImage
}; };
}; };
const translateImage = async (imageHash: string | null): Promise<ImageColor | undefined> => { const translateImage = async (imageHash: string | null): Promise<ImageColor | undefined> => {
if (!imageHash) return; if (!imageHash) return;
const imageColor = imagesLibrary.get(imageHash) ?? (await generateAndRegister(imageHash));
if (!imageColor) return;
const { dataUri, ...rest } = imageColor;
return {
...rest,
imageHash
};
};
const generateAndRegister = async (imageHash: string) => {
const image = figma.getImageByHash(imageHash); const image = figma.getImageByHash(imageHash);
if (!image) return; if (!image) return;
const bytes = await image.getBytesAsync(); const bytes = await image.getBytesAsync();
const size = await image.getSizeAsync(); const { width, height } = await image.getSizeAsync();
const b64 = figma.base64Encode(bytes); const b64 = figma.base64Encode(bytes);
const mimeType = detectMimeType(b64); const mtype = detectMimeType(b64);
const dataUri = `data:${mimeType};base64,${b64}`; const dataUri = `data:${mtype};base64,${b64}`;
return { const imageColor: ImageColor = {
width: size.width, width,
height: size.height, height,
mtype: mimeType, mtype,
dataUri,
keepAspectRatio: true, keepAspectRatio: true,
dataUri: dataUri,
id: '00000000-0000-0000-0000-000000000000' id: '00000000-0000-0000-0000-000000000000'
}; };
imagesLibrary.register(imageHash, imageColor);
return imageColor;
}; };

View file

@ -1,6 +1,5 @@
import { Uuid } from './uuid'; import { Uuid } from './uuid';
//@TODO: check how this exports the image through a dataUri
export type ImageColor = { export type ImageColor = {
name?: string; name?: string;
width: number; width: number;
@ -9,4 +8,5 @@ export type ImageColor = {
id?: Uuid; id?: Uuid;
keepAspectRatio?: boolean; keepAspectRatio?: boolean;
dataUri?: string; dataUri?: string;
imageHash?: string; // @TODO: move to any other place
}; };

View file

@ -2,7 +2,7 @@ import { PenpotFile } from '@ui/lib/types/penpotFile';
import { FrameShape } from '@ui/lib/types/shapes/frameShape'; import { FrameShape } from '@ui/lib/types/shapes/frameShape';
import { Uuid } from '@ui/lib/types/utils/uuid'; import { Uuid } from '@ui/lib/types/utils/uuid';
import { parseFigmaId } from '@ui/parser'; import { parseFigmaId } from '@ui/parser';
import { symbolBlendMode, symbolFillGradients } from '@ui/parser/creators/symbols'; import { symbolBlendMode, symbolFills } from '@ui/parser/creators/symbols';
import { createItems } from '.'; import { createItems } from '.';
@ -15,7 +15,7 @@ export const createArtboard = (
file.addArtboard({ file.addArtboard({
id, id,
shapeRef: shapeRef ?? parseFigmaId(file, figmaRelatedId, true), shapeRef: shapeRef ?? parseFigmaId(file, figmaRelatedId, true),
fills: symbolFillGradients(fills), fills: symbolFills(fills),
blendMode: symbolBlendMode(blendMode), blendMode: symbolBlendMode(blendMode),
...rest ...rest
}); });

View file

@ -1,7 +1,7 @@
import { PenpotFile } from '@ui/lib/types/penpotFile'; import { PenpotFile } from '@ui/lib/types/penpotFile';
import { BoolShape } from '@ui/lib/types/shapes/boolShape'; import { BoolShape } from '@ui/lib/types/shapes/boolShape';
import { parseFigmaId } from '@ui/parser'; import { parseFigmaId } from '@ui/parser';
import { symbolBlendMode, symbolBoolType, symbolFillGradients } from '@ui/parser/creators/symbols'; import { symbolBlendMode, symbolBoolType, symbolFills } from '@ui/parser/creators/symbols';
import { createItems } from '.'; import { createItems } from '.';
@ -12,7 +12,7 @@ export const createBool = (
file.addBool({ file.addBool({
id: parseFigmaId(file, figmaId), id: parseFigmaId(file, figmaId),
shapeRef: parseFigmaId(file, figmaRelatedId, true), shapeRef: parseFigmaId(file, figmaRelatedId, true),
fills: symbolFillGradients(fills), fills: symbolFills(fills),
blendMode: symbolBlendMode(blendMode), blendMode: symbolBlendMode(blendMode),
boolType: symbolBoolType(boolType), boolType: symbolBoolType(boolType),
...rest ...rest

View file

@ -1,7 +1,7 @@
import { PenpotFile } from '@ui/lib/types/penpotFile'; import { PenpotFile } from '@ui/lib/types/penpotFile';
import { CircleShape } from '@ui/lib/types/shapes/circleShape'; import { CircleShape } from '@ui/lib/types/shapes/circleShape';
import { parseFigmaId } from '@ui/parser'; import { parseFigmaId } from '@ui/parser';
import { symbolBlendMode, symbolFillGradients } from '@ui/parser/creators/symbols'; import { symbolBlendMode, symbolFills } from '@ui/parser/creators/symbols';
export const createCircle = ( export const createCircle = (
file: PenpotFile, file: PenpotFile,
@ -10,7 +10,7 @@ export const createCircle = (
file.createCircle({ file.createCircle({
id: parseFigmaId(file, figmaId), id: parseFigmaId(file, figmaId),
shapeRef: parseFigmaId(file, figmaRelatedId, true), shapeRef: parseFigmaId(file, figmaRelatedId, true),
fills: symbolFillGradients(fills), fills: symbolFills(fills),
blendMode: symbolBlendMode(blendMode), blendMode: symbolBlendMode(blendMode),
...rest ...rest
}); });

View file

@ -1,6 +1,7 @@
import { componentsLibrary } from '@plugin/ComponentLibrary'; import { componentsLibrary } from '@plugin/ComponentLibrary';
import { PenpotFile } from '@ui/lib/types/penpotFile'; import { PenpotFile } from '@ui/lib/types/penpotFile';
import { symbolBlendMode, symbolFills } from '@ui/parser/creators/symbols';
import { uiComponents } from '@ui/parser/libraries'; import { uiComponents } from '@ui/parser/libraries';
import { createItems } from '.'; import { createItems } from '.';
@ -12,10 +13,12 @@ export const createComponentLibrary = (file: PenpotFile) => {
return; return;
} }
const { children = [], ...rest } = component; const { children = [], fills, blendMode, ...rest } = component;
file.startComponent({ file.startComponent({
...rest, ...rest,
fills: symbolFills(fills),
blendMode: symbolBlendMode(blendMode),
id: uiComponent.componentId, id: uiComponent.componentId,
componentId: uiComponent.componentId, componentId: uiComponent.componentId,
mainInstancePage: uiComponent.mainInstancePage, mainInstancePage: uiComponent.mainInstancePage,

View file

@ -1,11 +1,7 @@
import { PenpotFile } from '@ui/lib/types/penpotFile'; import { PenpotFile } from '@ui/lib/types/penpotFile';
import { PathShape } from '@ui/lib/types/shapes/pathShape'; import { PathShape } from '@ui/lib/types/shapes/pathShape';
import { parseFigmaId } from '@ui/parser'; import { parseFigmaId } from '@ui/parser';
import { import { symbolBlendMode, symbolFills, symbolPathContent } from '@ui/parser/creators/symbols';
symbolBlendMode,
symbolFillGradients,
symbolPathContent
} from '@ui/parser/creators/symbols';
export const createPath = ( export const createPath = (
file: PenpotFile, file: PenpotFile,
@ -14,7 +10,7 @@ export const createPath = (
file.createPath({ file.createPath({
id: parseFigmaId(file, figmaId), id: parseFigmaId(file, figmaId),
shapeRef: parseFigmaId(file, figmaRelatedId, true), shapeRef: parseFigmaId(file, figmaRelatedId, true),
fills: symbolFillGradients(fills), fills: symbolFills(fills),
blendMode: symbolBlendMode(blendMode), blendMode: symbolBlendMode(blendMode),
content: symbolPathContent(content), content: symbolPathContent(content),
...rest ...rest

View file

@ -1,7 +1,7 @@
import { PenpotFile } from '@ui/lib/types/penpotFile'; import { PenpotFile } from '@ui/lib/types/penpotFile';
import { RectShape } from '@ui/lib/types/shapes/rectShape'; import { RectShape } from '@ui/lib/types/shapes/rectShape';
import { parseFigmaId } from '@ui/parser'; import { parseFigmaId } from '@ui/parser';
import { symbolBlendMode, symbolFillGradients } from '@ui/parser/creators/symbols'; import { symbolBlendMode, symbolFills } from '@ui/parser/creators/symbols';
export const createRectangle = ( export const createRectangle = (
file: PenpotFile, file: PenpotFile,
@ -10,7 +10,7 @@ export const createRectangle = (
file.createRect({ file.createRect({
id: parseFigmaId(file, figmaId), id: parseFigmaId(file, figmaId),
shapeRef: parseFigmaId(file, figmaRelatedId, true), shapeRef: parseFigmaId(file, figmaRelatedId, true),
fills: symbolFillGradients(fills), fills: symbolFills(fills),
blendMode: symbolBlendMode(blendMode), blendMode: symbolBlendMode(blendMode),
...rest ...rest
}); });

View file

@ -1,16 +1,33 @@
import { PenpotFile } from '@ui/lib/types/penpotFile'; import { PenpotFile } from '@ui/lib/types/penpotFile';
import { TextShape } from '@ui/lib/types/shapes/textShape'; import { TextContent, TextShape } from '@ui/lib/types/shapes/textShape';
import { parseFigmaId } from '@ui/parser'; import { parseFigmaId } from '@ui/parser';
import { symbolBlendMode } from '@ui/parser/creators/symbols'; import { symbolBlendMode, symbolFills } from '@ui/parser/creators/symbols';
export const createText = ( export const createText = (
file: PenpotFile, file: PenpotFile,
{ type, blendMode, figmaId, figmaRelatedId, ...rest }: TextShape { type, blendMode, figmaId, content, figmaRelatedId, ...rest }: TextShape
) => { ) => {
file.createText({ file.createText({
id: parseFigmaId(file, figmaId), id: parseFigmaId(file, figmaId),
shapeRef: parseFigmaId(file, figmaRelatedId, true), shapeRef: parseFigmaId(file, figmaRelatedId, true),
content: parseContent(content),
blendMode: symbolBlendMode(blendMode), blendMode: symbolBlendMode(blendMode),
...rest ...rest
}); });
}; };
const parseContent = (content: TextContent | undefined): TextContent | undefined => {
if (!content) return;
content.children?.forEach(paragraphSet => {
paragraphSet.children.forEach(paragraph => {
paragraph.children.forEach(textNode => {
textNode.fills = symbolFills(textNode.fills);
});
paragraph.fills = symbolFills(paragraph.fills);
});
});
return content;
};

View file

@ -1,4 +1,4 @@
export * from './symbolBlendMode'; export * from './symbolBlendMode';
export * from './symbolBoolType'; export * from './symbolBoolType';
export * from './symbolFillGradients'; export * from './symbolFills';
export * from './symbolPathContent'; export * from './symbolPathContent';

View file

@ -1,7 +1,10 @@
import { imagesLibrary } from '@plugin/ImageLibrary';
import { Fill } from '@ui/lib/types/utils/fill'; import { Fill } from '@ui/lib/types/utils/fill';
import { Gradient, LINEAR_TYPE, RADIAL_TYPE } from '@ui/lib/types/utils/gradient'; import { Gradient, LINEAR_TYPE, RADIAL_TYPE } from '@ui/lib/types/utils/gradient';
import { ImageColor } from '@ui/lib/types/utils/imageColor';
export const symbolFillGradients = (fills?: Fill[]): Fill[] | undefined => { export const symbolFills = (fills?: Fill[]): Fill[] | undefined => {
if (!fills) return; if (!fills) return;
return fills.map(fill => { return fills.map(fill => {
@ -9,6 +12,10 @@ export const symbolFillGradients = (fills?: Fill[]): Fill[] | undefined => {
fill.fillColorGradient = symbolFillGradient(fill.fillColorGradient); fill.fillColorGradient = symbolFillGradient(fill.fillColorGradient);
} }
if (fill.fillImage) {
fill.fillImage = symbolFillImage(fill.fillImage);
}
return fill; return fill;
}); });
}; };
@ -29,3 +36,16 @@ const symbolFillGradient = ({ type, ...rest }: Gradient): Gradient | undefined =
console.error(`Unsupported gradient type: ${String(type)}`); console.error(`Unsupported gradient type: ${String(type)}`);
}; };
const symbolFillImage = ({ imageHash, ...rest }: ImageColor): ImageColor | undefined => {
if (!imageHash) return;
const imageColor = imagesLibrary.get(imageHash);
if (!imageColor) return;
return {
...rest,
dataUri: imageColor?.dataUri
};
};

View file

@ -1,4 +1,5 @@
import { componentsLibrary } from '@plugin/ComponentLibrary'; import { componentsLibrary } from '@plugin/ComponentLibrary';
import { imagesLibrary } from '@plugin/ImageLibrary';
import { createFile } from '@ui/lib/penpot'; import { createFile } from '@ui/lib/penpot';
import { createComponentLibrary, createPage } from '@ui/parser/creators'; import { createComponentLibrary, createPage } from '@ui/parser/creators';
@ -7,8 +8,10 @@ import { PenpotDocument } from '@ui/types';
import { idLibrary } from '.'; import { idLibrary } from '.';
export const parse = ({ name, children = [], components }: PenpotDocument) => { export const parse = ({ name, children = [], components, images }: PenpotDocument) => {
componentsLibrary.init(components); componentsLibrary.init(components);
imagesLibrary.init(images);
uiComponents.init(); uiComponents.init();
idLibrary.init(); idLibrary.init();

View file

@ -1,8 +1,10 @@
import { PenpotPage } from '@ui/lib/types/penpotPage'; import { PenpotPage } from '@ui/lib/types/penpotPage';
import { ComponentShape } from '@ui/lib/types/shapes/componentShape'; import { ComponentShape } from '@ui/lib/types/shapes/componentShape';
import { ImageColor } from '@ui/lib/types/utils/imageColor';
export type PenpotDocument = { export type PenpotDocument = {
name: string; name: string;
children?: PenpotPage[]; children?: PenpotPage[];
components: Record<string, ComponentShape>; components: Record<string, ComponentShape>;
images: Record<string, ImageColor>;
}; };