Skip to content

Commit

Permalink
Images (#89)
Browse files Browse the repository at this point in the history
* wip

* wip

* fix something?

* wip

* wip

* wip

* fixes

* fixes

* stroke image

* wip

* fixes

* fixes

* remove old code

* fix translate fills

* remove penpot public uri

* remove old code

* fix return undefineds

* updated packages

* finish refactor

---------

Co-authored-by: Jordi Sala Morales <jordism91@gmail.com>
  • Loading branch information
Cenadros and jordisala1991 committed May 9, 2024
1 parent ca540d0 commit dddc457
Show file tree
Hide file tree
Showing 30 changed files with 1,944 additions and 1,546 deletions.
5 changes: 5 additions & 0 deletions .changeset/little-drinks-obey.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"penpot-exporter": minor
---

Basic support for image fills
2,698 changes: 1,545 additions & 1,153 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion plugin-src/transformers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ export * from './transformDocumentNode';
export * from './transformEllipseNode';
export * from './transformFrameNode';
export * from './transformGroupNode';
export * from './transformImageNode';
export * from './transformPageNode';
export * from './transformPathNode';
export * from './transformRectangleNode';
Expand Down
6 changes: 3 additions & 3 deletions plugin-src/transformers/partials/transformFills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import { translateFills } from '@plugin/translators';

import { ShapeAttributes } from '@ui/lib/types/shapes/shape';

export const transformFills = (
export const transformFills = async (
node: MinimalFillsMixin & DimensionAndPositionMixin
): Partial<ShapeAttributes> => {
): Promise<Partial<ShapeAttributes>> => {
return {
fills: translateFills(node.fills, node.width, node.height)
fills: await translateFills(node.fills, node.width, node.height)
};
};
6 changes: 3 additions & 3 deletions plugin-src/transformers/partials/transformStrokes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ const hasFillGeometry = (node: GeometryMixin): boolean => {
return node.fillGeometry.length > 0;
};

export const transformStrokes = (
export const transformStrokes = async (
node: GeometryMixin | (GeometryMixin & IndividualStrokesMixin)
): Partial<ShapeAttributes> => {
): Promise<Partial<ShapeAttributes>> => {
return {
strokes: translateStrokes(
strokes: await translateStrokes(
node,
hasFillGeometry(node),
isVectorLike(node) ? node.vectorNetwork : undefined,
Expand Down
6 changes: 3 additions & 3 deletions plugin-src/transformers/partials/transformText.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { translateGrowType, translateVerticalAlign } from '@plugin/translators/t

import { TextShape } from '@ui/lib/types/shapes/textShape';

export const transformText = (node: TextNode): Partial<TextShape> => {
export const transformText = async (node: TextNode): Promise<Partial<TextShape>> => {
const styledTextSegments = node.getStyledTextSegments([
'fontName',
'fontSize',
Expand All @@ -28,9 +28,9 @@ export const transformText = (node: TextNode): Partial<TextShape> => {
children: [
{
type: 'paragraph',
children: translateStyleTextSegments(node, styledTextSegments),
children: await translateStyleTextSegments(node, styledTextSegments),
...(styledTextSegments.length ? transformTextStyle(node, styledTextSegments[0]) : {}),
...transformFills(node)
...(await transformFills(node))
}
]
}
Expand Down
8 changes: 4 additions & 4 deletions plugin-src/transformers/transformEllipseNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,17 @@ import {

import { CircleShape } from '@ui/lib/types/shapes/circleShape';

export const transformEllipseNode = (
export const transformEllipseNode = async (
node: EllipseNode,
baseX: number,
baseY: number
): CircleShape => {
): Promise<CircleShape> => {
return {
type: 'circle',
name: node.name,
...transformFills(node),
...(await transformFills(node)),
...transformEffects(node),
...transformStrokes(node),
...(await transformStrokes(node)),
...transformDimensionAndPosition(node, baseX, baseY),
...transformSceneNode(node),
...transformBlend(node),
Expand Down
4 changes: 2 additions & 2 deletions plugin-src/transformers/transformFrameNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,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
...transformStrokes(node),
...(await transformStrokes(node)),
// @see: https://forum.figma.com/t/add-a-blendmode-property-for-sectionnode/58560
...transformBlend(node),
...transformProportion(node),
Expand All @@ -41,7 +41,7 @@ export const transformFrameNode = async (
type: 'frame',
name: node.name,
showContent: isSectionNode(node) ? true : !node.clipsContent,
...transformFills(node),
...(await transformFills(node)),
...frameSpecificAttributes,
...(await transformChildren(node, baseX + node.x, baseY + node.y)),
...transformDimensionAndPosition(node, baseX, baseY),
Expand Down
28 changes: 0 additions & 28 deletions plugin-src/transformers/transformImageNode.ts

This file was deleted.

8 changes: 4 additions & 4 deletions plugin-src/transformers/transformPathNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ const hasFillGeometry = (node: VectorNode | StarNode | LineNode | PolygonNode):
return 'fillGeometry' in node && node.fillGeometry.length > 0;
};

export const transformPathNode = (
export const transformPathNode = async (
node: VectorNode | StarNode | LineNode | PolygonNode,
baseX: number,
baseY: number
): PathShape => {
): Promise<PathShape> => {
return {
name: node.name,
...(hasFillGeometry(node) ? transformFills(node) : []),
...transformStrokes(node),
...(hasFillGeometry(node) ? await transformFills(node) : []),
...(await transformStrokes(node)),
...transformEffects(node),
...transformVectorPaths(node, baseX, baseY),
...transformDimensionAndPosition(node, baseX, baseY),
Expand Down
8 changes: 4 additions & 4 deletions plugin-src/transformers/transformRectangleNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,17 @@ import {

import { RectShape } from '@ui/lib/types/shapes/rectShape';

export const transformRectangleNode = (
export const transformRectangleNode = async (
node: RectangleNode,
baseX: number,
baseY: number
): RectShape => {
): Promise<RectShape> => {
return {
type: 'rect',
name: node.name,
...transformFills(node),
...(await transformFills(node)),
...transformEffects(node),
...transformStrokes(node),
...(await transformStrokes(node)),
...transformDimensionAndPosition(node, baseX, baseY),
...transformSceneNode(node),
...transformBlend(node),
Expand Down
24 changes: 4 additions & 20 deletions plugin-src/transformers/transformSceneNode.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import { calculateAdjustment } from '@plugin/utils';

import { PenpotNode } from '@ui/lib/types/penpotNode';

import {
transformEllipseNode,
transformFrameNode,
transformGroupNode,
transformImageNode,
transformPathNode,
transformRectangleNode,
transformTextNode
Expand All @@ -17,36 +14,23 @@ export const transformSceneNode = async (
baseX: number = 0,
baseY: number = 0
): Promise<PenpotNode | undefined> => {
// @TODO: when penpot 2.0, manage image as fills for the basic types
if (
'fills' in node &&
node.fills !== figma.mixed &&
node.fills.find(fill => fill.type === 'IMAGE')
) {
// If the nested frames extended the bounds of the rasterized image, we need to
// account for this both in position on the canvas and the calculated width and
// height of the image.
const [adjustedX, adjustedY] = calculateAdjustment(node);
return await transformImageNode(node, baseX + adjustedX, baseY + adjustedY);
}

switch (node.type) {
case 'RECTANGLE':
return transformRectangleNode(node, baseX, baseY);
return await transformRectangleNode(node, baseX, baseY);
case 'ELLIPSE':
return transformEllipseNode(node, baseX, baseY);
return await transformEllipseNode(node, baseX, baseY);
case 'SECTION':
case 'FRAME':
return await transformFrameNode(node, baseX, baseY);
case 'GROUP':
return await transformGroupNode(node, baseX, baseY);
case 'TEXT':
return transformTextNode(node, baseX, baseY);
return await transformTextNode(node, baseX, baseY);
case 'STAR':
case 'POLYGON':
case 'VECTOR':
case 'LINE':
return transformPathNode(node, baseX, baseY);
return await transformPathNode(node, baseX, baseY);
}

console.error(`Unsupported node type: ${node.type}`);
Expand Down
10 changes: 7 additions & 3 deletions plugin-src/transformers/transformTextNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,20 @@ import {

import { TextShape } from '@ui/lib/types/shapes/textShape';

export const transformTextNode = (node: TextNode, baseX: number, baseY: number): TextShape => {
export const transformTextNode = async (
node: TextNode,
baseX: number,
baseY: number
): Promise<TextShape> => {
return {
type: 'text',
name: node.name,
...transformText(node),
...(await transformText(node)),
...transformDimensionAndPosition(node, baseX, baseY),
...transformEffects(node),
...transformSceneNode(node),
...transformBlend(node),
...transformProportion(node),
...transformStrokes(node)
...(await transformStrokes(node))
};
};
21 changes: 13 additions & 8 deletions plugin-src/translators/text/translateStyleTextSegments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@ import {

import { TextNode as PenpotTextNode, TextStyle } from '@ui/lib/types/shapes/textShape';

export const translateStyleTextSegments = (
export const translateStyleTextSegments = async (
node: TextNode,
segments: StyleTextSegment[]
): PenpotTextNode[] => {
const partials = segments.map(segment => ({
textNode: translateStyleTextSegment(node, segment),
segment
}));
): Promise<PenpotTextNode[]> => {
const partials = await Promise.all(
segments.map(async segment => ({
textNode: await translateStyleTextSegment(node, segment),
segment
}))
);

return translateParagraphProperties(node, partials);
};
Expand All @@ -42,9 +44,12 @@ export const transformTextStyle = (
};
};

const translateStyleTextSegment = (node: TextNode, segment: StyleTextSegment): PenpotTextNode => {
const translateStyleTextSegment = async (
node: TextNode,
segment: StyleTextSegment
): Promise<PenpotTextNode> => {
return {
fills: translateFills(segment.fills, node.width, node.height),
fills: await translateFills(segment.fills, node.width, node.height),
text: segment.characters,
...transformTextStyle(node, segment)
};
Expand Down
52 changes: 45 additions & 7 deletions plugin-src/translators/translateFills.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,38 @@
import { rgbToHex } from '@plugin/utils';
import { detectMimeType, rgbToHex } from '@plugin/utils';
import { calculateLinearGradient } from '@plugin/utils/calculateLinearGradient';

import { Fill } from '@ui/lib/types/utils/fill';
import { ImageColor } from '@ui/lib/types/utils/imageColor';

export const translateFill = (fill: Paint, width: number, height: number): Fill | undefined => {
export const translateFill = async (
fill: Paint,
width: number,
height: number
): Promise<Fill | undefined> => {
switch (fill.type) {
case 'SOLID':
return translateSolidFill(fill);
case 'GRADIENT_LINEAR':
return translateGradientLinearFill(fill, width, height);
case 'IMAGE':
return await translateImageFill(fill);
}

console.error(`Unsupported fill type: ${fill.type}`);
};

export const translateFills = (
export const translateFills = async (
fills: readonly Paint[] | typeof figma.mixed,
width: number,
height: number
): Fill[] => {
): Promise<Fill[]> => {
const figmaFills = fills === figma.mixed ? [] : fills;
const penpotFills: Fill[] = [];

for (const fill of figmaFills) {
const penpotFill = translateFill(fill, width, height);
const penpotFill = await translateFill(fill, width, height);
if (penpotFill) {
// colors are applied in reverse order in Figma, that's why we unshift
// fills are applied in reverse order in Figma, that's why we unshift
penpotFills.unshift(penpotFill);
}
}
Expand All @@ -42,6 +49,37 @@ export const translatePageFill = (fill: Paint): string | undefined => {
console.error(`Unsupported page fill type: ${fill.type}`);
};

const translateImage = async (imageHash: string | null): Promise<ImageColor | undefined> => {
if (!imageHash) return;

const image = figma.getImageByHash(imageHash);
if (!image) return;

const bytes = await image.getBytesAsync();
const size = await image.getSizeAsync();
const b64 = figma.base64Encode(bytes);
const mimeType = detectMimeType(b64);
const dataUri = `data:${mimeType};base64,${b64}`;

return {
width: size.width,
height: size.height,
mtype: mimeType,
keepAspectRatio: true,
dataUri: dataUri
};
};

const translateImageFill = async (fill: ImagePaint): Promise<Fill | undefined> => {
const fillImage = await translateImage(fill.imageHash);
if (!fillImage) return;

return {
fillOpacity: !fill.visible ? 0 : fill.opacity,
fillImage: fillImage
};
};

const translateSolidFill = (fill: SolidPaint): Fill => {
return {
fillColor: rgbToHex(fill.color),
Expand Down Expand Up @@ -73,6 +111,6 @@ const translateGradientLinearFill = (fill: GradientPaint, width: number, height:
}
]
},
fillOpacity: fill.visible === false ? 0 : undefined
fillOpacity: !fill.visible ? 0 : fill.opacity
};
};

0 comments on commit dddc457

Please sign in to comment.