mirror of
https://github.com/penpot/penpot-exporter-figma-plugin.git
synced 2024-12-22 05:33:02 -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:
parent
a4113382bb
commit
f726dc9cec
16 changed files with 122 additions and 32 deletions
5
.changeset/tasty-geckos-eat.md
Normal file
5
.changeset/tasty-geckos-eat.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"penpot-exporter": patch
|
||||
---
|
||||
|
||||
Optimize image exporting when there are multiple copies of the same image in the file
|
23
plugin-src/ImageLibrary.ts
Normal file
23
plugin-src/ImageLibrary.ts
Normal 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();
|
|
@ -1,4 +1,5 @@
|
|||
import { componentsLibrary } from '@plugin/ComponentLibrary';
|
||||
import { imagesLibrary } from '@plugin/ImageLibrary';
|
||||
|
||||
import { PenpotDocument } from '@ui/types';
|
||||
|
||||
|
@ -27,6 +28,7 @@ export const transformDocumentNode = async (node: DocumentNode): Promise<PenpotD
|
|||
return {
|
||||
name: node.name,
|
||||
children,
|
||||
components: componentsLibrary.all()
|
||||
components: componentsLibrary.all(),
|
||||
images: imagesLibrary.all()
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { imagesLibrary } from '@plugin/ImageLibrary';
|
||||
import { detectMimeType } from '@plugin/utils';
|
||||
|
||||
import { Fill } from '@ui/lib/types/utils/fill';
|
||||
|
@ -9,28 +10,46 @@ export const translateImageFill = async (fill: ImagePaint): Promise<Fill | undef
|
|||
|
||||
return {
|
||||
fillOpacity: !fill.visible ? 0 : fill.opacity,
|
||||
fillImage: fillImage
|
||||
fillImage
|
||||
};
|
||||
};
|
||||
|
||||
const translateImage = async (imageHash: string | null): Promise<ImageColor | undefined> => {
|
||||
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);
|
||||
|
||||
if (!image) return;
|
||||
|
||||
const bytes = await image.getBytesAsync();
|
||||
const size = await image.getSizeAsync();
|
||||
const { width, height } = await image.getSizeAsync();
|
||||
const b64 = figma.base64Encode(bytes);
|
||||
const mimeType = detectMimeType(b64);
|
||||
const dataUri = `data:${mimeType};base64,${b64}`;
|
||||
const mtype = detectMimeType(b64);
|
||||
const dataUri = `data:${mtype};base64,${b64}`;
|
||||
|
||||
return {
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
mtype: mimeType,
|
||||
const imageColor: ImageColor = {
|
||||
width,
|
||||
height,
|
||||
mtype,
|
||||
dataUri,
|
||||
keepAspectRatio: true,
|
||||
dataUri: dataUri,
|
||||
id: '00000000-0000-0000-0000-000000000000'
|
||||
};
|
||||
|
||||
imagesLibrary.register(imageHash, imageColor);
|
||||
|
||||
return imageColor;
|
||||
};
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { Uuid } from './uuid';
|
||||
|
||||
//@TODO: check how this exports the image through a dataUri
|
||||
export type ImageColor = {
|
||||
name?: string;
|
||||
width: number;
|
||||
|
@ -9,4 +8,5 @@ export type ImageColor = {
|
|||
id?: Uuid;
|
||||
keepAspectRatio?: boolean;
|
||||
dataUri?: string;
|
||||
imageHash?: string; // @TODO: move to any other place
|
||||
};
|
||||
|
|
|
@ -2,7 +2,7 @@ import { PenpotFile } from '@ui/lib/types/penpotFile';
|
|||
import { FrameShape } from '@ui/lib/types/shapes/frameShape';
|
||||
import { Uuid } from '@ui/lib/types/utils/uuid';
|
||||
import { parseFigmaId } from '@ui/parser';
|
||||
import { symbolBlendMode, symbolFillGradients } from '@ui/parser/creators/symbols';
|
||||
import { symbolBlendMode, symbolFills } from '@ui/parser/creators/symbols';
|
||||
|
||||
import { createItems } from '.';
|
||||
|
||||
|
@ -15,7 +15,7 @@ export const createArtboard = (
|
|||
file.addArtboard({
|
||||
id,
|
||||
shapeRef: shapeRef ?? parseFigmaId(file, figmaRelatedId, true),
|
||||
fills: symbolFillGradients(fills),
|
||||
fills: symbolFills(fills),
|
||||
blendMode: symbolBlendMode(blendMode),
|
||||
...rest
|
||||
});
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { PenpotFile } from '@ui/lib/types/penpotFile';
|
||||
import { BoolShape } from '@ui/lib/types/shapes/boolShape';
|
||||
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 '.';
|
||||
|
||||
|
@ -12,7 +12,7 @@ export const createBool = (
|
|||
file.addBool({
|
||||
id: parseFigmaId(file, figmaId),
|
||||
shapeRef: parseFigmaId(file, figmaRelatedId, true),
|
||||
fills: symbolFillGradients(fills),
|
||||
fills: symbolFills(fills),
|
||||
blendMode: symbolBlendMode(blendMode),
|
||||
boolType: symbolBoolType(boolType),
|
||||
...rest
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { PenpotFile } from '@ui/lib/types/penpotFile';
|
||||
import { CircleShape } from '@ui/lib/types/shapes/circleShape';
|
||||
import { parseFigmaId } from '@ui/parser';
|
||||
import { symbolBlendMode, symbolFillGradients } from '@ui/parser/creators/symbols';
|
||||
import { symbolBlendMode, symbolFills } from '@ui/parser/creators/symbols';
|
||||
|
||||
export const createCircle = (
|
||||
file: PenpotFile,
|
||||
|
@ -10,7 +10,7 @@ export const createCircle = (
|
|||
file.createCircle({
|
||||
id: parseFigmaId(file, figmaId),
|
||||
shapeRef: parseFigmaId(file, figmaRelatedId, true),
|
||||
fills: symbolFillGradients(fills),
|
||||
fills: symbolFills(fills),
|
||||
blendMode: symbolBlendMode(blendMode),
|
||||
...rest
|
||||
});
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { componentsLibrary } from '@plugin/ComponentLibrary';
|
||||
|
||||
import { PenpotFile } from '@ui/lib/types/penpotFile';
|
||||
import { symbolBlendMode, symbolFills } from '@ui/parser/creators/symbols';
|
||||
import { uiComponents } from '@ui/parser/libraries';
|
||||
|
||||
import { createItems } from '.';
|
||||
|
@ -12,10 +13,12 @@ export const createComponentLibrary = (file: PenpotFile) => {
|
|||
return;
|
||||
}
|
||||
|
||||
const { children = [], ...rest } = component;
|
||||
const { children = [], fills, blendMode, ...rest } = component;
|
||||
|
||||
file.startComponent({
|
||||
...rest,
|
||||
fills: symbolFills(fills),
|
||||
blendMode: symbolBlendMode(blendMode),
|
||||
id: uiComponent.componentId,
|
||||
componentId: uiComponent.componentId,
|
||||
mainInstancePage: uiComponent.mainInstancePage,
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
import { PenpotFile } from '@ui/lib/types/penpotFile';
|
||||
import { PathShape } from '@ui/lib/types/shapes/pathShape';
|
||||
import { parseFigmaId } from '@ui/parser';
|
||||
import {
|
||||
symbolBlendMode,
|
||||
symbolFillGradients,
|
||||
symbolPathContent
|
||||
} from '@ui/parser/creators/symbols';
|
||||
import { symbolBlendMode, symbolFills, symbolPathContent } from '@ui/parser/creators/symbols';
|
||||
|
||||
export const createPath = (
|
||||
file: PenpotFile,
|
||||
|
@ -14,7 +10,7 @@ export const createPath = (
|
|||
file.createPath({
|
||||
id: parseFigmaId(file, figmaId),
|
||||
shapeRef: parseFigmaId(file, figmaRelatedId, true),
|
||||
fills: symbolFillGradients(fills),
|
||||
fills: symbolFills(fills),
|
||||
blendMode: symbolBlendMode(blendMode),
|
||||
content: symbolPathContent(content),
|
||||
...rest
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { PenpotFile } from '@ui/lib/types/penpotFile';
|
||||
import { RectShape } from '@ui/lib/types/shapes/rectShape';
|
||||
import { parseFigmaId } from '@ui/parser';
|
||||
import { symbolBlendMode, symbolFillGradients } from '@ui/parser/creators/symbols';
|
||||
import { symbolBlendMode, symbolFills } from '@ui/parser/creators/symbols';
|
||||
|
||||
export const createRectangle = (
|
||||
file: PenpotFile,
|
||||
|
@ -10,7 +10,7 @@ export const createRectangle = (
|
|||
file.createRect({
|
||||
id: parseFigmaId(file, figmaId),
|
||||
shapeRef: parseFigmaId(file, figmaRelatedId, true),
|
||||
fills: symbolFillGradients(fills),
|
||||
fills: symbolFills(fills),
|
||||
blendMode: symbolBlendMode(blendMode),
|
||||
...rest
|
||||
});
|
||||
|
|
|
@ -1,16 +1,33 @@
|
|||
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 { symbolBlendMode } from '@ui/parser/creators/symbols';
|
||||
import { symbolBlendMode, symbolFills } from '@ui/parser/creators/symbols';
|
||||
|
||||
export const createText = (
|
||||
file: PenpotFile,
|
||||
{ type, blendMode, figmaId, figmaRelatedId, ...rest }: TextShape
|
||||
{ type, blendMode, figmaId, content, figmaRelatedId, ...rest }: TextShape
|
||||
) => {
|
||||
file.createText({
|
||||
id: parseFigmaId(file, figmaId),
|
||||
shapeRef: parseFigmaId(file, figmaRelatedId, true),
|
||||
content: parseContent(content),
|
||||
blendMode: symbolBlendMode(blendMode),
|
||||
...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;
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export * from './symbolBlendMode';
|
||||
export * from './symbolBoolType';
|
||||
export * from './symbolFillGradients';
|
||||
export * from './symbolFills';
|
||||
export * from './symbolPathContent';
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import { imagesLibrary } from '@plugin/ImageLibrary';
|
||||
|
||||
import { Fill } from '@ui/lib/types/utils/fill';
|
||||
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;
|
||||
|
||||
return fills.map(fill => {
|
||||
|
@ -9,6 +12,10 @@ export const symbolFillGradients = (fills?: Fill[]): Fill[] | undefined => {
|
|||
fill.fillColorGradient = symbolFillGradient(fill.fillColorGradient);
|
||||
}
|
||||
|
||||
if (fill.fillImage) {
|
||||
fill.fillImage = symbolFillImage(fill.fillImage);
|
||||
}
|
||||
|
||||
return fill;
|
||||
});
|
||||
};
|
||||
|
@ -29,3 +36,16 @@ const symbolFillGradient = ({ type, ...rest }: Gradient): Gradient | undefined =
|
|||
|
||||
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
|
||||
};
|
||||
};
|
|
@ -1,4 +1,5 @@
|
|||
import { componentsLibrary } from '@plugin/ComponentLibrary';
|
||||
import { imagesLibrary } from '@plugin/ImageLibrary';
|
||||
|
||||
import { createFile } from '@ui/lib/penpot';
|
||||
import { createComponentLibrary, createPage } from '@ui/parser/creators';
|
||||
|
@ -7,8 +8,10 @@ import { PenpotDocument } from '@ui/types';
|
|||
|
||||
import { idLibrary } from '.';
|
||||
|
||||
export const parse = ({ name, children = [], components }: PenpotDocument) => {
|
||||
export const parse = ({ name, children = [], components, images }: PenpotDocument) => {
|
||||
componentsLibrary.init(components);
|
||||
imagesLibrary.init(images);
|
||||
|
||||
uiComponents.init();
|
||||
idLibrary.init();
|
||||
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import { PenpotPage } from '@ui/lib/types/penpotPage';
|
||||
import { ComponentShape } from '@ui/lib/types/shapes/componentShape';
|
||||
import { ImageColor } from '@ui/lib/types/utils/imageColor';
|
||||
|
||||
export type PenpotDocument = {
|
||||
name: string;
|
||||
children?: PenpotPage[];
|
||||
components: Record<string, ComponentShape>;
|
||||
images: Record<string, ImageColor>;
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue