diff --git a/.changeset/tasty-geckos-eat.md b/.changeset/tasty-geckos-eat.md new file mode 100644 index 00000000..e087c684 --- /dev/null +++ b/.changeset/tasty-geckos-eat.md @@ -0,0 +1,5 @@ +--- +"penpot-exporter": patch +--- + +Optimize image exporting when there are multiple copies of the same image in the file diff --git a/plugin-src/ImageLibrary.ts b/plugin-src/ImageLibrary.ts new file mode 100644 index 00000000..46610825 --- /dev/null +++ b/plugin-src/ImageLibrary.ts @@ -0,0 +1,23 @@ +import { ImageColor } from '@ui/lib/types/utils/imageColor'; + +class ImageLibrary { + private images: Record = {}; + + public register(hash: string, image: ImageColor) { + this.images[hash] = image; + } + + public get(hash: string): ImageColor | undefined { + return this.images[hash]; + } + + public all(): Record { + return this.images; + } + + public init(images: Record): void { + this.images = images; + } +} + +export const imagesLibrary = new ImageLibrary(); diff --git a/plugin-src/transformers/transformDocumentNode.ts b/plugin-src/transformers/transformDocumentNode.ts index b1abb1cb..251f1c7c 100644 --- a/plugin-src/transformers/transformDocumentNode.ts +++ b/plugin-src/transformers/transformDocumentNode.ts @@ -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 => { 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; }; diff --git a/ui-src/lib/types/utils/imageColor.ts b/ui-src/lib/types/utils/imageColor.ts index c19302d5..39923a9f 100644 --- a/ui-src/lib/types/utils/imageColor.ts +++ b/ui-src/lib/types/utils/imageColor.ts @@ -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 }; diff --git a/ui-src/parser/creators/createArtboard.ts b/ui-src/parser/creators/createArtboard.ts index 3386235a..3bd7b925 100644 --- a/ui-src/parser/creators/createArtboard.ts +++ b/ui-src/parser/creators/createArtboard.ts @@ -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 }); diff --git a/ui-src/parser/creators/createBool.ts b/ui-src/parser/creators/createBool.ts index 8e0e4f14..cf51cade 100644 --- a/ui-src/parser/creators/createBool.ts +++ b/ui-src/parser/creators/createBool.ts @@ -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 diff --git a/ui-src/parser/creators/createCircle.ts b/ui-src/parser/creators/createCircle.ts index 436b6122..401d9c0a 100644 --- a/ui-src/parser/creators/createCircle.ts +++ b/ui-src/parser/creators/createCircle.ts @@ -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 }); diff --git a/ui-src/parser/creators/createComponentLibrary.ts b/ui-src/parser/creators/createComponentLibrary.ts index 0df1a096..14f5819d 100644 --- a/ui-src/parser/creators/createComponentLibrary.ts +++ b/ui-src/parser/creators/createComponentLibrary.ts @@ -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, diff --git a/ui-src/parser/creators/createPath.ts b/ui-src/parser/creators/createPath.ts index a5990be1..1643c36a 100644 --- a/ui-src/parser/creators/createPath.ts +++ b/ui-src/parser/creators/createPath.ts @@ -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 diff --git a/ui-src/parser/creators/createRectangle.ts b/ui-src/parser/creators/createRectangle.ts index 9583aa80..2131d4a2 100644 --- a/ui-src/parser/creators/createRectangle.ts +++ b/ui-src/parser/creators/createRectangle.ts @@ -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 }); diff --git a/ui-src/parser/creators/createText.ts b/ui-src/parser/creators/createText.ts index e0a01431..d1a0de6c 100644 --- a/ui-src/parser/creators/createText.ts +++ b/ui-src/parser/creators/createText.ts @@ -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; +}; diff --git a/ui-src/parser/creators/symbols/index.ts b/ui-src/parser/creators/symbols/index.ts index 3dc3f71f..2cb6725e 100644 --- a/ui-src/parser/creators/symbols/index.ts +++ b/ui-src/parser/creators/symbols/index.ts @@ -1,4 +1,4 @@ export * from './symbolBlendMode'; export * from './symbolBoolType'; -export * from './symbolFillGradients'; +export * from './symbolFills'; export * from './symbolPathContent'; diff --git a/ui-src/parser/creators/symbols/symbolFillGradients.ts b/ui-src/parser/creators/symbols/symbolFills.ts similarity index 55% rename from ui-src/parser/creators/symbols/symbolFillGradients.ts rename to ui-src/parser/creators/symbols/symbolFills.ts index f16e6429..e91b5fdb 100644 --- a/ui-src/parser/creators/symbols/symbolFillGradients.ts +++ b/ui-src/parser/creators/symbols/symbolFills.ts @@ -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 + }; +}; diff --git a/ui-src/parser/parse.ts b/ui-src/parser/parse.ts index 39cf2295..bce7ac29 100644 --- a/ui-src/parser/parse.ts +++ b/ui-src/parser/parse.ts @@ -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(); diff --git a/ui-src/types/penpotDocument.ts b/ui-src/types/penpotDocument.ts index 9f8b8b04..3a6f945f 100644 --- a/ui-src/types/penpotDocument.ts +++ b/ui-src/types/penpotDocument.ts @@ -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; + images: Record; };