diff --git a/.changeset/pretty-plants-kick.md b/.changeset/pretty-plants-kick.md new file mode 100644 index 0000000..f437c2c --- /dev/null +++ b/.changeset/pretty-plants-kick.md @@ -0,0 +1,5 @@ +--- +"penpot-exporter": minor +--- + +Optimize images before generating zip file diff --git a/plugin-src/ImageLibrary.ts b/plugin-src/ImageLibrary.ts index 4661082..c8e0a83 100644 --- a/plugin-src/ImageLibrary.ts +++ b/plugin-src/ImageLibrary.ts @@ -1,21 +1,19 @@ -import { ImageColor } from '@ui/lib/types/utils/imageColor'; - class ImageLibrary { - private images: Record = {}; + private images: Record = {}; - public register(hash: string, image: ImageColor) { + public register(hash: string, image: Image | null) { this.images[hash] = image; } - public get(hash: string): ImageColor | undefined { + public get(hash: string): Image | null | undefined { return this.images[hash]; } - public all(): Record { + public all(): Record { return this.images; } - public init(images: Record): void { + public init(images: Record): void { this.images = images; } } diff --git a/plugin-src/transformers/partials/transformFills.ts b/plugin-src/transformers/partials/transformFills.ts index 19eee60..6a8df81 100644 --- a/plugin-src/transformers/partials/transformFills.ts +++ b/plugin-src/transformers/partials/transformFills.ts @@ -2,10 +2,10 @@ import { translateFills } from '@plugin/translators/fills'; import { ShapeAttributes } from '@ui/lib/types/shapes/shape'; -export const transformFills = async ( +export const transformFills = ( node: MinimalFillsMixin & DimensionAndPositionMixin -): Promise> => { +): Pick => { return { - fills: await translateFills(node.fills) + fills: translateFills(node.fills) }; }; diff --git a/plugin-src/transformers/partials/transformStrokes.ts b/plugin-src/transformers/partials/transformStrokes.ts index 9f01a39..95f62f8 100644 --- a/plugin-src/transformers/partials/transformStrokes.ts +++ b/plugin-src/transformers/partials/transformStrokes.ts @@ -13,9 +13,9 @@ const hasFillGeometry = (node: GeometryMixin): boolean => { return node.fillGeometry.length > 0; }; -export const transformStrokes = async ( +export const transformStrokes = ( node: GeometryMixin | (GeometryMixin & IndividualStrokesMixin) -): Promise> => { +): Pick => { const vectorNetwork = isVectorLike(node) ? node.vectorNetwork : undefined; const strokeCaps = (stroke: Stroke) => { @@ -30,15 +30,15 @@ export const transformStrokes = async ( }; return { - strokes: await translateStrokes(node, strokeCaps) + strokes: translateStrokes(node, strokeCaps) }; }; -export const transformStrokesFromVector = async ( +export const transformStrokesFromVector = ( node: VectorNode, vector: Command[], vectorRegion: VectorRegion | undefined -): Promise> => { +): Pick => { const strokeCaps = (stroke: Stroke) => { if (vectorRegion !== undefined) return stroke; @@ -54,7 +54,7 @@ export const transformStrokesFromVector = async ( }; return { - strokes: await translateStrokes(node, strokeCaps) + strokes: translateStrokes(node, strokeCaps) }; }; diff --git a/plugin-src/transformers/partials/transformText.ts b/plugin-src/transformers/partials/transformText.ts index 03b523b..3f86908 100644 --- a/plugin-src/transformers/partials/transformText.ts +++ b/plugin-src/transformers/partials/transformText.ts @@ -4,9 +4,7 @@ import { translateGrowType, translateVerticalAlign } from '@plugin/translators/t import { TextAttributes, TextShape } from '@ui/lib/types/shapes/textShape'; -export const transformText = async ( - node: TextNode -): Promise> => { +export const transformText = (node: TextNode): TextAttributes & Pick => { const styledTextSegments = node.getStyledTextSegments([ 'fontName', 'fontSize', @@ -31,9 +29,9 @@ export const transformText = async ( children: [ { type: 'paragraph', - children: await translateStyleTextSegments(node, styledTextSegments), + children: translateStyleTextSegments(node, styledTextSegments), ...transformTextStyle(node, styledTextSegments[0]), - ...(await transformFills(node)) + ...transformFills(node) } ] } diff --git a/plugin-src/transformers/partials/transformVectorPaths.ts b/plugin-src/transformers/partials/transformVectorPaths.ts index f5c15e0..aaadd66 100644 --- a/plugin-src/transformers/partials/transformVectorPaths.ts +++ b/plugin-src/transformers/partials/transformVectorPaths.ts @@ -31,40 +31,30 @@ export const transformVectorPathsAsContent = ( }; }; -export const transformVectorPaths = async ( +export const transformVectorPaths = ( node: VectorNode, baseX: number, baseY: number -): Promise => { - const pathShapes = await Promise.all( - node.vectorPaths - .filter((vectorPath, index) => { - return ( - nodeHasFills(node, vectorPath, (node.vectorNetwork.regions ?? [])[index]) || - node.strokes.length > 0 - ); - }) - .map((vectorPath, index) => - transformVectorPath( - node, - vectorPath, - (node.vectorNetwork.regions ?? [])[index], - baseX, - baseY - ) - ) - ); +): PathShape[] => { + const pathShapes = node.vectorPaths + .filter((vectorPath, index) => { + return ( + nodeHasFills(node, vectorPath, (node.vectorNetwork.regions ?? [])[index]) || + node.strokes.length > 0 + ); + }) + .map((vectorPath, index) => + transformVectorPath(node, vectorPath, (node.vectorNetwork.regions ?? [])[index], baseX, baseY) + ); - const geometryShapes = await Promise.all( - node.fillGeometry - .filter( - geometry => - !node.vectorPaths.find( - vectorPath => normalizePath(vectorPath.data) === normalizePath(geometry.data) - ) - ) - .map(geometry => transformVectorPath(node, geometry, undefined, baseX, baseY)) - ); + const geometryShapes = node.fillGeometry + .filter( + geometry => + !node.vectorPaths.find( + vectorPath => normalizePath(vectorPath.data) === normalizePath(geometry.data) + ) + ) + .map(geometry => transformVectorPath(node, geometry, undefined, baseX, baseY)); return [...geometryShapes, ...pathShapes]; }; @@ -96,13 +86,13 @@ const nodeHasFills = ( return !!(vectorPath.windingRule !== 'NONE' && (vectorRegion?.fills || node.fills)); }; -const transformVectorPath = async ( +const transformVectorPath = ( node: VectorNode, vectorPath: VectorPath, vectorRegion: VectorRegion | undefined, baseX: number, baseY: number -): Promise => { +): PathShape => { const normalizedPaths = parseSVG(vectorPath.data); return { @@ -110,13 +100,11 @@ const transformVectorPath = async ( name: 'svg-path', content: translateCommandsToSegments(normalizedPaths, baseX + node.x, baseY + node.y), fills: - vectorPath.windingRule === 'NONE' - ? [] - : await translateFills(vectorRegion?.fills ?? node.fills), + vectorPath.windingRule === 'NONE' ? [] : translateFills(vectorRegion?.fills ?? node.fills), svgAttrs: { fillRule: translateWindingRule(vectorPath.windingRule) }, - ...(await transformStrokesFromVector(node, normalizedPaths, vectorRegion)), + ...transformStrokesFromVector(node, normalizedPaths, vectorRegion), ...transformEffects(node), ...transformDimensionAndPositionFromVectorPath(vectorPath, baseX, baseY), ...transformSceneNode(node), diff --git a/plugin-src/transformers/transformBooleanNode.ts b/plugin-src/transformers/transformBooleanNode.ts index 5e92423..ef414a3 100644 --- a/plugin-src/transformers/transformBooleanNode.ts +++ b/plugin-src/transformers/transformBooleanNode.ts @@ -24,9 +24,9 @@ export const transformBooleanNode = async ( boolType: translateBoolType(node.booleanOperation), ...transformFigmaIds(node), ...(await transformChildren(node, baseX, baseY)), - ...(await transformFills(node)), + ...transformFills(node), ...transformEffects(node), - ...(await transformStrokes(node)), + ...transformStrokes(node), ...transformDimensionAndPosition(node, baseX, baseY), ...transformSceneNode(node), ...transformBlend(node), diff --git a/plugin-src/transformers/transformComponentNode.ts b/plugin-src/transformers/transformComponentNode.ts index 37c3d06..fd3ac2b 100644 --- a/plugin-src/transformers/transformComponentNode.ts +++ b/plugin-src/transformers/transformComponentNode.ts @@ -24,9 +24,9 @@ export const transformComponentNode = async ( name: node.name, path: node.parent?.type === 'COMPONENT_SET' ? node.parent.name : '', ...transformFigmaIds(node), - ...(await transformFills(node)), + ...transformFills(node), ...transformEffects(node), - ...(await transformStrokes(node)), + ...transformStrokes(node), ...transformSceneNode(node), ...transformBlend(node), ...transformProportion(node), diff --git a/plugin-src/transformers/transformDocumentNode.ts b/plugin-src/transformers/transformDocumentNode.ts index b860088..19c1ad8 100644 --- a/plugin-src/transformers/transformDocumentNode.ts +++ b/plugin-src/transformers/transformDocumentNode.ts @@ -28,10 +28,20 @@ export const transformDocumentNode = async (node: DocumentNode): Promise = {}; + + for (const [key, image] of Object.entries(imagesLibrary.all())) { + const bytes = await image?.getBytesAsync(); + + if (!bytes) continue; + + images[key] = bytes; + } + return { name: node.name, children, components: componentsLibrary.all(), - images: imagesLibrary.all() + images }; }; diff --git a/plugin-src/transformers/transformEllipseNode.ts b/plugin-src/transformers/transformEllipseNode.ts index a21d8ae..86083fd 100644 --- a/plugin-src/transformers/transformEllipseNode.ts +++ b/plugin-src/transformers/transformEllipseNode.ts @@ -12,18 +12,18 @@ import { import { CircleShape } from '@ui/lib/types/shapes/circleShape'; -export const transformEllipseNode = async ( +export const transformEllipseNode = ( node: EllipseNode, baseX: number, baseY: number -): Promise => { +): CircleShape => { return { type: 'circle', name: node.name, ...transformFigmaIds(node), - ...(await transformFills(node)), + ...transformFills(node), ...transformEffects(node), - ...(await transformStrokes(node)), + ...transformStrokes(node), ...transformDimension(node), ...transformRotationAndPosition(node, baseX, baseY), ...transformSceneNode(node), diff --git a/plugin-src/transformers/transformFrameNode.ts b/plugin-src/transformers/transformFrameNode.ts index ae50be5..6d6aabe 100644 --- a/plugin-src/transformers/transformFrameNode.ts +++ b/plugin-src/transformers/transformFrameNode.ts @@ -29,7 +29,7 @@ export const transformFrameNode = async ( // they plan to add it in the future. Refactor this when available. frameSpecificAttributes = { // @see: https://forum.figma.com/t/why-are-strokes-not-available-on-section-nodes/41658 - ...(await transformStrokes(node)), + ...transformStrokes(node), // @see: https://forum.figma.com/t/add-a-blendmode-property-for-sectionnode/58560 ...transformBlend(node), ...transformProportion(node), @@ -43,7 +43,7 @@ export const transformFrameNode = async ( name: node.name, showContent: isSectionNode(node) ? true : !node.clipsContent, ...transformFigmaIds(node), - ...(await transformFills(node)), + ...transformFills(node), ...frameSpecificAttributes, ...(await transformChildren(node, baseX + node.x, baseY + node.y)), ...transformDimensionAndPosition(node, baseX, baseY), diff --git a/plugin-src/transformers/transformInstanceNode.ts b/plugin-src/transformers/transformInstanceNode.ts index 66578ee..e761db8 100644 --- a/plugin-src/transformers/transformInstanceNode.ts +++ b/plugin-src/transformers/transformInstanceNode.ts @@ -30,9 +30,9 @@ export const transformInstanceNode = async ( mainComponentFigmaId: mainComponent.id, isComponentRoot: isComponentRoot(node), ...transformFigmaIds(node), - ...(await transformFills(node)), + ...transformFills(node), ...transformEffects(node), - ...(await transformStrokes(node)), + ...transformStrokes(node), ...transformSceneNode(node), ...transformBlend(node), ...transformProportion(node), @@ -60,11 +60,14 @@ const isUnprocessableComponent = (mainComponent: ComponentNode): boolean => { const isComponentRoot = (node: InstanceNode): boolean => { let parent = node.parent; + while (parent !== null) { if (parent.type === 'COMPONENT' || parent.type === 'INSTANCE') { return false; } + parent = parent.parent; } + return true; }; diff --git a/plugin-src/transformers/transformPathNode.ts b/plugin-src/transformers/transformPathNode.ts index 5457312..85072b4 100644 --- a/plugin-src/transformers/transformPathNode.ts +++ b/plugin-src/transformers/transformPathNode.ts @@ -16,17 +16,17 @@ const hasFillGeometry = (node: StarNode | LineNode | PolygonNode): boolean => { return 'fillGeometry' in node && node.fillGeometry.length > 0; }; -export const transformPathNode = async ( +export const transformPathNode = ( node: StarNode | LineNode | PolygonNode, baseX: number, baseY: number -): Promise => { +): PathShape => { return { type: 'path', name: node.name, ...transformFigmaIds(node), - ...(hasFillGeometry(node) ? await transformFills(node) : []), - ...(await transformStrokes(node)), + ...(hasFillGeometry(node) ? transformFills(node) : []), + ...transformStrokes(node), ...transformEffects(node), ...transformVectorPathsAsContent(node, baseX, baseY), ...transformDimensionAndPosition(node, baseX, baseY), diff --git a/plugin-src/transformers/transformRectangleNode.ts b/plugin-src/transformers/transformRectangleNode.ts index 0e432ea..b76f6dd 100644 --- a/plugin-src/transformers/transformRectangleNode.ts +++ b/plugin-src/transformers/transformRectangleNode.ts @@ -13,18 +13,18 @@ import { import { RectShape } from '@ui/lib/types/shapes/rectShape'; -export const transformRectangleNode = async ( +export const transformRectangleNode = ( node: RectangleNode, baseX: number, baseY: number -): Promise => { +): RectShape => { return { type: 'rect', name: node.name, ...transformFigmaIds(node), - ...(await transformFills(node)), + ...transformFills(node), ...transformEffects(node), - ...(await transformStrokes(node)), + ...transformStrokes(node), ...transformDimension(node), ...transformRotationAndPosition(node, baseX, baseY), ...transformSceneNode(node), diff --git a/plugin-src/transformers/transformSceneNode.ts b/plugin-src/transformers/transformSceneNode.ts index 17b6b59..0cadc88 100644 --- a/plugin-src/transformers/transformSceneNode.ts +++ b/plugin-src/transformers/transformSceneNode.ts @@ -27,10 +27,10 @@ export const transformSceneNode = async ( switch (node.type) { case 'RECTANGLE': - penpotNode = await transformRectangleNode(node, baseX, baseY); + penpotNode = transformRectangleNode(node, baseX, baseY); break; case 'ELLIPSE': - penpotNode = await transformEllipseNode(node, baseX, baseY); + penpotNode = transformEllipseNode(node, baseX, baseY); break; case 'SECTION': case 'FRAME': @@ -41,15 +41,15 @@ export const transformSceneNode = async ( penpotNode = await transformGroupNode(node, baseX, baseY); break; case 'TEXT': - penpotNode = await transformTextNode(node, baseX, baseY); + penpotNode = transformTextNode(node, baseX, baseY); break; case 'VECTOR': - penpotNode = await transformVectorNode(node, baseX, baseY); + penpotNode = transformVectorNode(node, baseX, baseY); break; case 'STAR': case 'POLYGON': case 'LINE': - penpotNode = await transformPathNode(node, baseX, baseY); + penpotNode = transformPathNode(node, baseX, baseY); break; case 'BOOLEAN_OPERATION': penpotNode = await transformBooleanNode(node, baseX, baseY); diff --git a/plugin-src/transformers/transformTextNode.ts b/plugin-src/transformers/transformTextNode.ts index 8298d88..1e723ff 100644 --- a/plugin-src/transformers/transformTextNode.ts +++ b/plugin-src/transformers/transformTextNode.ts @@ -11,21 +11,17 @@ import { import { TextShape } from '@ui/lib/types/shapes/textShape'; -export const transformTextNode = async ( - node: TextNode, - baseX: number, - baseY: number -): Promise => { +export const transformTextNode = (node: TextNode, baseX: number, baseY: number): TextShape => { return { type: 'text', name: node.name, ...transformFigmaIds(node), - ...(await transformText(node)), + ...transformText(node), ...transformDimensionAndPosition(node, baseX, baseY), ...transformEffects(node), ...transformSceneNode(node), ...transformBlend(node), ...transformProportion(node), - ...(await transformStrokes(node)) + ...transformStrokes(node) }; }; diff --git a/plugin-src/transformers/transformVectorNode.ts b/plugin-src/transformers/transformVectorNode.ts index 29fab02..6c6a62a 100644 --- a/plugin-src/transformers/transformVectorNode.ts +++ b/plugin-src/transformers/transformVectorNode.ts @@ -11,12 +11,12 @@ import { transformGroupNodeLike } from '.'; * If there are no regions on the vector network, we treat it like a normal `PathShape`. * If there are regions, we treat the vector node as a `GroupShape` with multiple `PathShape` children. */ -export const transformVectorNode = async ( +export const transformVectorNode = ( node: VectorNode, baseX: number, baseY: number -): Promise => { - const children = await transformVectorPaths(node, baseX, baseY); +): GroupShape | PathShape => { + const children = transformVectorPaths(node, baseX, baseY); if (children.length === 1) { return { diff --git a/plugin-src/translators/fills/translateFills.ts b/plugin-src/translators/fills/translateFills.ts index d57978b..71b904c 100644 --- a/plugin-src/translators/fills/translateFills.ts +++ b/plugin-src/translators/fills/translateFills.ts @@ -7,7 +7,7 @@ import { rgbToHex } from '@plugin/utils'; import { Fill } from '@ui/lib/types/utils/fill'; -export const translateFill = async (fill: Paint): Promise => { +export const translateFill = (fill: Paint): Fill | undefined => { switch (fill.type) { case 'SOLID': return translateSolidFill(fill); @@ -16,21 +16,22 @@ export const translateFill = async (fill: Paint): Promise => { case 'GRADIENT_RADIAL': return translateGradientRadialFill(fill); case 'IMAGE': - return await translateImageFill(fill); + return translateImageFill(fill); } console.error(`Unsupported fill type: ${fill.type}`); }; -export const translateFills = async ( +export const translateFills = ( fills: readonly Paint[] | typeof figma.mixed | undefined -): Promise => { +): Fill[] => { if (fills === undefined || fills === figma.mixed) return []; const penpotFills: Fill[] = []; for (const fill of fills) { - const penpotFill = await translateFill(fill); + const penpotFill = translateFill(fill); + if (penpotFill) { // fills are applied in reverse order in Figma, that's why we unshift penpotFills.unshift(penpotFill); diff --git a/plugin-src/translators/fills/translateImageFill.ts b/plugin-src/translators/fills/translateImageFill.ts index d2f3ab2..43f0300 100644 --- a/plugin-src/translators/fills/translateImageFill.ts +++ b/plugin-src/translators/fills/translateImageFill.ts @@ -1,13 +1,10 @@ -import { fromByteArray } from 'base64-js'; - import { imagesLibrary } from '@plugin/ImageLibrary'; -import { detectMimeType } from '@plugin/utils'; import { Fill } from '@ui/lib/types/utils/fill'; -import { ImageColor } from '@ui/lib/types/utils/imageColor'; +import { PartialImageColor } from '@ui/lib/types/utils/imageColor'; -export const translateImageFill = async (fill: ImagePaint): Promise => { - const fillImage = await translateImage(fill.imageHash); +export const translateImageFill = (fill: ImagePaint): Fill | undefined => { + const fillImage = translateImage(fill.imageHash); if (!fillImage) return; return { @@ -16,42 +13,14 @@ export const translateImageFill = async (fill: ImagePaint): Promise => { +const translateImage = (imageHash: string | null): PartialImageColor | undefined => { if (!imageHash) return; - const imageColor = imagesLibrary.get(imageHash) ?? (await generateAndRegister(imageHash)); - - if (!imageColor) return; - - const { dataUri, ...rest } = imageColor; + if (imagesLibrary.get(imageHash) === undefined) { + imagesLibrary.register(imageHash, figma.getImageByHash(imageHash)); + } return { - ...rest, imageHash }; }; - -const generateAndRegister = async (imageHash: string) => { - const image = figma.getImageByHash(imageHash); - - if (!image) return; - - const bytes = await image.getBytesAsync(); - const { width, height } = await image.getSizeAsync(); - const b64 = fromByteArray(bytes); - const mtype = detectMimeType(b64); - const dataUri = `data:${mtype};base64,${b64}`; - - const imageColor: ImageColor = { - width, - height, - mtype, - dataUri, - keepAspectRatio: true, - id: '00000000-0000-0000-0000-000000000000' - }; - - imagesLibrary.register(imageHash, imageColor); - - return imageColor; -}; diff --git a/plugin-src/translators/text/translateStyleTextSegments.ts b/plugin-src/translators/text/translateStyleTextSegments.ts index 84f3f96..cee2132 100644 --- a/plugin-src/translators/text/translateStyleTextSegments.ts +++ b/plugin-src/translators/text/translateStyleTextSegments.ts @@ -12,16 +12,14 @@ import { import { TextNode as PenpotTextNode, TextStyle } from '@ui/lib/types/shapes/textShape'; -export const translateStyleTextSegments = async ( +export const translateStyleTextSegments = ( node: TextNode, segments: StyleTextSegment[] -): Promise => { - const partials = await Promise.all( - segments.map(async segment => ({ - textNode: await translateStyleTextSegment(node, segment), - segment - })) - ); +): PenpotTextNode[] => { + const partials = segments.map(segment => ({ + textNode: translateStyleTextSegment(node, segment), + segment + })); return translateParagraphProperties(node, partials); }; @@ -41,12 +39,9 @@ export const transformTextStyle = (node: TextNode, segment: StyleTextSegment): T }; }; -const translateStyleTextSegment = async ( - node: TextNode, - segment: StyleTextSegment -): Promise => { +const translateStyleTextSegment = (node: TextNode, segment: StyleTextSegment): PenpotTextNode => { return { - fills: await translateFills(segment.fills), + fills: translateFills(segment.fills), text: segment.characters, ...transformTextStyle(node, segment) }; diff --git a/plugin-src/translators/translateStrokes.ts b/plugin-src/translators/translateStrokes.ts index f6ecc5f..a70145a 100644 --- a/plugin-src/translators/translateStrokes.ts +++ b/plugin-src/translators/translateStrokes.ts @@ -2,31 +2,28 @@ import { translateFill } from '@plugin/translators/fills'; import { Stroke, StrokeAlignment, StrokeCaps } from '@ui/lib/types/utils/stroke'; -export const translateStrokes = async ( +export const translateStrokes = ( node: MinimalStrokesMixin | (MinimalStrokesMixin & IndividualStrokesMixin), strokeCaps: (stroke: Stroke) => Stroke = stroke => stroke -): Promise => { +): Stroke[] => { const sharedStrokeProperties: Stroke = { strokeWidth: translateStrokeWeight(node), strokeAlignment: translateStrokeAlignment(node.strokeAlign), strokeStyle: node.dashPattern.length ? 'dashed' : 'solid' }; - return await Promise.all( - node.strokes.map( - async (paint, index) => - await translateStroke(paint, sharedStrokeProperties, strokeCaps, index === 0) - ) + return node.strokes.map((paint, index) => + translateStroke(paint, sharedStrokeProperties, strokeCaps, index === 0) ); }; -export const translateStroke = async ( +export const translateStroke = ( paint: Paint, sharedStrokeProperties: Stroke, strokeCaps: (stroke: Stroke) => Stroke, firstStroke: boolean -): Promise => { - const fill = await translateFill(paint); +): Stroke => { + const fill = translateFill(paint); let stroke: Stroke = { strokeColor: fill?.fillColor, diff --git a/plugin-src/utils/detectMimeType.ts b/plugin-src/utils/detectMimeType.ts deleted file mode 100644 index 6a7dc0c..0000000 --- a/plugin-src/utils/detectMimeType.ts +++ /dev/null @@ -1,18 +0,0 @@ -export interface Signatures { - [key: string]: string; -} - -const signatures: Signatures = { - 'R0lGODdh': 'image/gif', - 'R0lGODlh': 'image/gif', - 'iVBORw0KGgo': 'image/png', - '/9j/': 'image/jpeg' -}; - -export const detectMimeType = (b64: string) => { - for (const s in signatures) { - if (b64.indexOf(s) === 0) { - return signatures[s]; - } - } -}; diff --git a/plugin-src/utils/index.ts b/plugin-src/utils/index.ts index 67ff2e1..113505f 100644 --- a/plugin-src/utils/index.ts +++ b/plugin-src/utils/index.ts @@ -2,7 +2,6 @@ export * from './applyMatrixToPoint'; export * from './calculateAdjustment'; export * from './calculateLinearGradient'; export * from './calculateRadialGradient'; -export * from './detectMimeType'; export * from './getBoundingBox'; export * from './matrixInvert'; export * from './rgbToHex'; diff --git a/ui-src/context/useFigma.ts b/ui-src/context/useFigma.ts index e4330a1..35cecb6 100644 --- a/ui-src/context/useFigma.ts +++ b/ui-src/context/useFigma.ts @@ -69,7 +69,7 @@ export const useFigma = (): UseFigmaHook => { parent.postMessage({ pluginMessage: { type, data } }, '*'); }; - const onMessage = (event: MessageEvent<{ pluginMessage?: PluginMessage }>) => { + const onMessage = async (event: MessageEvent<{ pluginMessage?: PluginMessage }>) => { if (!event.data.pluginMessage) return; const { pluginMessage } = event.data; @@ -78,7 +78,7 @@ export const useFigma = (): UseFigmaHook => { case 'PENPOT_DOCUMENT': { setDownloading(true); - const file = parse(pluginMessage.data); + const file = await parse(pluginMessage.data); file.export(); break; diff --git a/ui-src/lib/types/utils/fill.ts b/ui-src/lib/types/utils/fill.ts index 3047e46..2169156 100644 --- a/ui-src/lib/types/utils/fill.ts +++ b/ui-src/lib/types/utils/fill.ts @@ -1,5 +1,5 @@ import { Gradient } from './gradient'; -import { ImageColor } from './imageColor'; +import { ImageColor, PartialImageColor } from './imageColor'; import { Uuid } from './uuid'; export type Fill = { @@ -8,5 +8,5 @@ export type Fill = { fillColorGradient?: Gradient; fillColorRefFile?: Uuid; fillColorRefId?: Uuid; - fillImage?: ImageColor; + fillImage?: ImageColor | PartialImageColor; // @TODO: move to any other place }; diff --git a/ui-src/lib/types/utils/imageColor.ts b/ui-src/lib/types/utils/imageColor.ts index 39923a9..a8908d7 100644 --- a/ui-src/lib/types/utils/imageColor.ts +++ b/ui-src/lib/types/utils/imageColor.ts @@ -8,5 +8,9 @@ export type ImageColor = { id?: Uuid; keepAspectRatio?: boolean; dataUri?: string; - imageHash?: string; // @TODO: move to any other place +}; + +// @TODO: move to any other place +export type PartialImageColor = { + imageHash: string; }; diff --git a/ui-src/lib/types/utils/stroke.ts b/ui-src/lib/types/utils/stroke.ts index 0bcf4b6..645f8ba 100644 --- a/ui-src/lib/types/utils/stroke.ts +++ b/ui-src/lib/types/utils/stroke.ts @@ -1,5 +1,5 @@ import { Gradient } from './gradient'; -import { ImageColor } from './imageColor'; +import { ImageColor, PartialImageColor } from './imageColor'; import { Uuid } from './uuid'; export type Stroke = { @@ -13,7 +13,7 @@ export type Stroke = { strokeCapStart?: StrokeCaps; strokeCapEnd?: StrokeCaps; strokeColorGradient?: Gradient; - strokeImage?: ImageColor; + strokeImage?: ImageColor | PartialImageColor; }; export type StrokeAlignment = 'center' | 'inner' | 'outer'; diff --git a/ui-src/parser/creators/createArtboard.ts b/ui-src/parser/creators/createArtboard.ts index 3bd7b92..1fa9136 100644 --- a/ui-src/parser/creators/createArtboard.ts +++ b/ui-src/parser/creators/createArtboard.ts @@ -2,13 +2,23 @@ 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, symbolFills } from '@ui/parser/creators/symbols'; +import { symbolBlendMode, symbolFills, symbolStrokes } from '@ui/parser/creators/symbols'; import { createItems } from '.'; export const createArtboard = ( file: PenpotFile, - { type, fills, blendMode, children = [], figmaId, figmaRelatedId, shapeRef, ...rest }: FrameShape + { + type, + fills, + strokes, + blendMode, + children = [], + figmaId, + figmaRelatedId, + shapeRef, + ...rest + }: FrameShape ): Uuid | undefined => { const id = parseFigmaId(file, figmaId); @@ -16,6 +26,7 @@ export const createArtboard = ( id, shapeRef: shapeRef ?? parseFigmaId(file, figmaRelatedId, true), fills: symbolFills(fills), + strokes: symbolStrokes(strokes), blendMode: symbolBlendMode(blendMode), ...rest }); diff --git a/ui-src/parser/creators/createBool.ts b/ui-src/parser/creators/createBool.ts index cf51cad..2181052 100644 --- a/ui-src/parser/creators/createBool.ts +++ b/ui-src/parser/creators/createBool.ts @@ -1,18 +1,34 @@ import { PenpotFile } from '@ui/lib/types/penpotFile'; import { BoolShape } from '@ui/lib/types/shapes/boolShape'; import { parseFigmaId } from '@ui/parser'; -import { symbolBlendMode, symbolBoolType, symbolFills } from '@ui/parser/creators/symbols'; +import { + symbolBlendMode, + symbolBoolType, + symbolFills, + symbolStrokes +} from '@ui/parser/creators/symbols'; import { createItems } from '.'; export const createBool = ( file: PenpotFile, - { type, fills, boolType, blendMode, figmaId, figmaRelatedId, children = [], ...rest }: BoolShape + { + type, + fills, + strokes, + boolType, + blendMode, + figmaId, + figmaRelatedId, + children = [], + ...rest + }: BoolShape ) => { file.addBool({ id: parseFigmaId(file, figmaId), shapeRef: parseFigmaId(file, figmaRelatedId, true), fills: symbolFills(fills), + strokes: symbolStrokes(strokes), blendMode: symbolBlendMode(blendMode), boolType: symbolBoolType(boolType), ...rest diff --git a/ui-src/parser/creators/createCircle.ts b/ui-src/parser/creators/createCircle.ts index 401d9c0..b9279ff 100644 --- a/ui-src/parser/creators/createCircle.ts +++ b/ui-src/parser/creators/createCircle.ts @@ -1,16 +1,17 @@ import { PenpotFile } from '@ui/lib/types/penpotFile'; import { CircleShape } from '@ui/lib/types/shapes/circleShape'; import { parseFigmaId } from '@ui/parser'; -import { symbolBlendMode, symbolFills } from '@ui/parser/creators/symbols'; +import { symbolBlendMode, symbolFills, symbolStrokes } from '@ui/parser/creators/symbols'; export const createCircle = ( file: PenpotFile, - { type, fills, blendMode, figmaId, figmaRelatedId, ...rest }: CircleShape + { type, fills, strokes, blendMode, figmaId, figmaRelatedId, ...rest }: CircleShape ) => { file.createCircle({ id: parseFigmaId(file, figmaId), shapeRef: parseFigmaId(file, figmaRelatedId, true), fills: symbolFills(fills), + strokes: symbolStrokes(strokes), blendMode: symbolBlendMode(blendMode), ...rest }); diff --git a/ui-src/parser/creators/createComponentLibrary.ts b/ui-src/parser/creators/createComponentLibrary.ts index 14f5819..4bdf69c 100644 --- a/ui-src/parser/creators/createComponentLibrary.ts +++ b/ui-src/parser/creators/createComponentLibrary.ts @@ -1,7 +1,7 @@ import { componentsLibrary } from '@plugin/ComponentLibrary'; import { PenpotFile } from '@ui/lib/types/penpotFile'; -import { symbolBlendMode, symbolFills } from '@ui/parser/creators/symbols'; +import { symbolBlendMode, symbolFills, symbolStrokes } from '@ui/parser/creators/symbols'; import { uiComponents } from '@ui/parser/libraries'; import { createItems } from '.'; @@ -13,11 +13,12 @@ export const createComponentLibrary = (file: PenpotFile) => { return; } - const { children = [], fills, blendMode, ...rest } = component; + const { children = [], fills, strokes, blendMode, ...rest } = component; file.startComponent({ ...rest, fills: symbolFills(fills), + strokes: symbolStrokes(strokes), blendMode: symbolBlendMode(blendMode), id: uiComponent.componentId, componentId: uiComponent.componentId, diff --git a/ui-src/parser/creators/createPath.ts b/ui-src/parser/creators/createPath.ts index 1643c36..3fe9660 100644 --- a/ui-src/parser/creators/createPath.ts +++ b/ui-src/parser/creators/createPath.ts @@ -1,16 +1,22 @@ import { PenpotFile } from '@ui/lib/types/penpotFile'; import { PathShape } from '@ui/lib/types/shapes/pathShape'; import { parseFigmaId } from '@ui/parser'; -import { symbolBlendMode, symbolFills, symbolPathContent } from '@ui/parser/creators/symbols'; +import { + symbolBlendMode, + symbolFills, + symbolPathContent, + symbolStrokes +} from '@ui/parser/creators/symbols'; export const createPath = ( file: PenpotFile, - { type, fills, blendMode, content, figmaId, figmaRelatedId, ...rest }: PathShape + { type, fills, strokes, blendMode, content, figmaId, figmaRelatedId, ...rest }: PathShape ) => { file.createPath({ id: parseFigmaId(file, figmaId), shapeRef: parseFigmaId(file, figmaRelatedId, true), fills: symbolFills(fills), + strokes: symbolStrokes(strokes), blendMode: symbolBlendMode(blendMode), content: symbolPathContent(content), ...rest diff --git a/ui-src/parser/creators/createRectangle.ts b/ui-src/parser/creators/createRectangle.ts index 2131d4a..b418e5f 100644 --- a/ui-src/parser/creators/createRectangle.ts +++ b/ui-src/parser/creators/createRectangle.ts @@ -1,16 +1,17 @@ import { PenpotFile } from '@ui/lib/types/penpotFile'; import { RectShape } from '@ui/lib/types/shapes/rectShape'; import { parseFigmaId } from '@ui/parser'; -import { symbolBlendMode, symbolFills } from '@ui/parser/creators/symbols'; +import { symbolBlendMode, symbolFills, symbolStrokes } from '@ui/parser/creators/symbols'; export const createRectangle = ( file: PenpotFile, - { type, fills, blendMode, figmaId, figmaRelatedId, ...rest }: RectShape + { type, fills, strokes, blendMode, figmaId, figmaRelatedId, ...rest }: RectShape ) => { file.createRect({ id: parseFigmaId(file, figmaId), shapeRef: parseFigmaId(file, figmaRelatedId, true), fills: symbolFills(fills), + strokes: symbolStrokes(strokes), blendMode: symbolBlendMode(blendMode), ...rest }); diff --git a/ui-src/parser/creators/createText.ts b/ui-src/parser/creators/createText.ts index d1a0de6..93d7b93 100644 --- a/ui-src/parser/creators/createText.ts +++ b/ui-src/parser/creators/createText.ts @@ -1,17 +1,18 @@ import { PenpotFile } from '@ui/lib/types/penpotFile'; import { TextContent, TextShape } from '@ui/lib/types/shapes/textShape'; import { parseFigmaId } from '@ui/parser'; -import { symbolBlendMode, symbolFills } from '@ui/parser/creators/symbols'; +import { symbolBlendMode, symbolFills, symbolStrokes } from '@ui/parser/creators/symbols'; export const createText = ( file: PenpotFile, - { type, blendMode, figmaId, content, figmaRelatedId, ...rest }: TextShape + { type, blendMode, strokes, figmaId, content, figmaRelatedId, ...rest }: TextShape ) => { file.createText({ id: parseFigmaId(file, figmaId), shapeRef: parseFigmaId(file, figmaRelatedId, true), content: parseContent(content), blendMode: symbolBlendMode(blendMode), + strokes: symbolStrokes(strokes), ...rest }); }; diff --git a/ui-src/parser/creators/symbols/index.ts b/ui-src/parser/creators/symbols/index.ts index 2cb6725..611fc67 100644 --- a/ui-src/parser/creators/symbols/index.ts +++ b/ui-src/parser/creators/symbols/index.ts @@ -2,3 +2,4 @@ export * from './symbolBlendMode'; export * from './symbolBoolType'; export * from './symbolFills'; export * from './symbolPathContent'; +export * from './symbolStrokes'; diff --git a/ui-src/parser/creators/symbols/symbolFills.ts b/ui-src/parser/creators/symbols/symbolFills.ts index a4c2815..0ade017 100644 --- a/ui-src/parser/creators/symbols/symbolFills.ts +++ b/ui-src/parser/creators/symbols/symbolFills.ts @@ -1,8 +1,7 @@ -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'; +import { ImageColor, PartialImageColor } from '@ui/lib/types/utils/imageColor'; +import { uiImages } from '@ui/parser/libraries'; export const symbolFills = (fills?: Fill[]): Fill[] | undefined => { if (!fills) return; @@ -39,19 +38,16 @@ const symbolFillGradient = (fillGradient: Gradient): Gradient => { } }; -const symbolFillImage = (fillImage: ImageColor): ImageColor | undefined => { - if (fillImage.dataUri) return fillImage; +export const symbolFillImage = ( + fillImage: ImageColor | PartialImageColor +): ImageColor | undefined => { + if (!isPartialFillColor(fillImage)) return fillImage; - const { imageHash, ...rest } = fillImage; - - if (!imageHash) return; - - const imageColor = imagesLibrary.get(imageHash); - - if (!imageColor) return; - - return { - ...rest, - dataUri: imageColor?.dataUri - }; + return uiImages.get(fillImage.imageHash); +}; + +const isPartialFillColor = ( + imageColor: ImageColor | PartialImageColor +): imageColor is PartialImageColor => { + return 'imageHash' in imageColor; }; diff --git a/ui-src/parser/creators/symbols/symbolStrokes.ts b/ui-src/parser/creators/symbols/symbolStrokes.ts new file mode 100644 index 0000000..6c38597 --- /dev/null +++ b/ui-src/parser/creators/symbols/symbolStrokes.ts @@ -0,0 +1,15 @@ +import { Stroke } from '@ui/lib/types/utils/stroke'; + +import { symbolFillImage } from '.'; + +export const symbolStrokes = (strokes?: Stroke[]): Stroke[] | undefined => { + if (!strokes) return; + + return strokes.map(stroke => { + if (stroke.strokeImage) { + stroke.strokeImage = symbolFillImage(stroke.strokeImage); + } + + return stroke; + }); +}; diff --git a/ui-src/parser/index.ts b/ui-src/parser/index.ts index 6ce32a0..6d8e38f 100644 --- a/ui-src/parser/index.ts +++ b/ui-src/parser/index.ts @@ -1,3 +1,4 @@ export * from './IdLibrary'; export * from './parse'; +export * from './parseImage'; export * from './parseFigmaId'; diff --git a/ui-src/parser/libraries/UiImages.ts b/ui-src/parser/libraries/UiImages.ts new file mode 100644 index 0000000..ed819ff --- /dev/null +++ b/ui-src/parser/libraries/UiImages.ts @@ -0,0 +1,23 @@ +import { ImageColor } from '@ui/lib/types/utils/imageColor'; + +class UiImages { + private images: Record = {}; + + public register(id: string, image: ImageColor) { + this.images[id] = image; + } + + public get(id: string): ImageColor | undefined { + return this.images[id]; + } + + public all(): ImageColor[] { + return Object.values(this.images); + } + + public init() { + this.images = {}; + } +} + +export const uiImages = new UiImages(); diff --git a/ui-src/parser/libraries/index.ts b/ui-src/parser/libraries/index.ts index 69657ca..fe59ab7 100644 --- a/ui-src/parser/libraries/index.ts +++ b/ui-src/parser/libraries/index.ts @@ -1 +1,2 @@ export * from './UiComponents'; +export * from './UiImages'; diff --git a/ui-src/parser/parse.ts b/ui-src/parser/parse.ts index bce7ac2..d7877ba 100644 --- a/ui-src/parser/parse.ts +++ b/ui-src/parser/parse.ts @@ -1,16 +1,20 @@ import { componentsLibrary } from '@plugin/ComponentLibrary'; -import { imagesLibrary } from '@plugin/ImageLibrary'; import { createFile } from '@ui/lib/penpot'; import { createComponentLibrary, createPage } from '@ui/parser/creators'; -import { uiComponents } from '@ui/parser/libraries/UiComponents'; +import { uiComponents, uiImages } from '@ui/parser/libraries'; import { PenpotDocument } from '@ui/types'; -import { idLibrary } from '.'; +import { idLibrary, parseImage } from '.'; -export const parse = ({ name, children = [], components, images }: PenpotDocument) => { +export const parse = async ({ name, children = [], components, images }: PenpotDocument) => { componentsLibrary.init(components); - imagesLibrary.init(images); + + for (const [key, bytes] of Object.entries(images)) { + if (!bytes) continue; + + uiImages.register(key, await parseImage(bytes)); + } uiComponents.init(); idLibrary.init(); diff --git a/ui-src/parser/parseImage.ts b/ui-src/parser/parseImage.ts new file mode 100644 index 0000000..8958fcd --- /dev/null +++ b/ui-src/parser/parseImage.ts @@ -0,0 +1,53 @@ +import { ImageColor } from '@ui/lib/types/utils/imageColor'; +import { detectMimeType } from '@ui/utils'; + +const IMAGE_QUALITY = 0.8; + +export const parseImage = async (bytes: Uint8Array): Promise => { + const image = await extractFromBytes(bytes); + + return { + width: image.width, + height: image.height, + dataUri: image.dataURL, + keepAspectRatio: true, + id: '00000000-0000-0000-0000-000000000000' + }; +}; + +async function extractFromBytes(bytes: Uint8Array) { + const mymeType = detectMimeType(bytes); + const url = URL.createObjectURL(new Blob([bytes])); + + const image = await new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => resolve(img); + img.onerror = () => reject(); + img.src = url; + }); + + const canvas = new OffscreenCanvas(image.width, image.height); + const context = canvas.getContext('2d'); + + if (!context) { + throw new Error('Could not create canvas context'); + } + + context.drawImage(image, 0, 0); + + const dataURL = await canvas + .convertToBlob({ type: mymeType, quality: IMAGE_QUALITY }) + .then(blob => { + return new Promise(resolve => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result as string); + reader.readAsDataURL(blob); + }); + }); + + return { + dataURL, + width: image.width, + height: image.height + }; +} diff --git a/ui-src/types/penpotDocument.ts b/ui-src/types/penpotDocument.ts index 3a6f945..c809ed7 100644 --- a/ui-src/types/penpotDocument.ts +++ b/ui-src/types/penpotDocument.ts @@ -1,10 +1,9 @@ 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; + images: Record; }; diff --git a/ui-src/utils/detectMimeType.ts b/ui-src/utils/detectMimeType.ts new file mode 100644 index 0000000..a8ff58a --- /dev/null +++ b/ui-src/utils/detectMimeType.ts @@ -0,0 +1,25 @@ +export const detectMimeType = (bytes: Uint8Array): string | undefined => { + const length = 4; + + if (bytes.length >= length) { + const signatureArr = new Array(length); + + for (let index = 0; index < length; index++) { + signatureArr[index] = bytes[index].toString(16); + } + + const signature = signatureArr.join('').toUpperCase(); + + switch (signature) { + case '89504E47': + return 'image/png'; + case '47494638': + return 'image/gif'; + case 'FFD8FFDB': + case 'FFD8FFE0': + return 'image/jpeg'; + default: + return; + } + } +}; diff --git a/ui-src/utils/index.ts b/ui-src/utils/index.ts new file mode 100644 index 0000000..d317401 --- /dev/null +++ b/ui-src/utils/index.ts @@ -0,0 +1 @@ +export * from './detectMimeType';