Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: export frame with vector/group/text images #1607

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions crates/dc_bundle/src/legacy_definition/view/node_style.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ pub struct NodeStyle {
pub aspect_ratio: Number,
pub pointer_events: PointerEvents,
pub meter_data: Option<MeterData>,

pub img_replacement_res_name: Option<String>,
}

impl Default for NodeStyle {
Expand Down Expand Up @@ -151,6 +153,7 @@ impl Default for NodeStyle {
aspect_ratio: Number::default(),
pointer_events: PointerEvents::default(),
meter_data: None,
img_replacement_res_name: None,
}
}
}
4 changes: 4 additions & 0 deletions crates/dc_bundle/src/legacy_definition/view/view_style.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,10 @@ impl ViewStyle {
if self.node_style.meter_data != other.node_style.meter_data {
delta.node_style.meter_data = other.node_style.meter_data.clone();
}
if self.node_style.img_replacement_res_name != other.node_style.img_replacement_res_name {
delta.node_style.img_replacement_res_name =
other.node_style.img_replacement_res_name.clone();
}
delta
}
}
7 changes: 7 additions & 0 deletions crates/figma_import/src/transform_flexbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1468,6 +1468,13 @@ fn visit_node(
}
}

// Check to see if there is additional plugin data to render the node use a resource drawable.
if let Some(res_data) = plugin_data {
if let Some(res_name) = res_data.get("image_replacement_res_name") {
style.node_style.img_replacement_res_name = Some(res_name.into());
}
}

// Figure out the ViewShape from the node type.
let view_shape = match &node.data {
figma_schema::NodeData::BooleanOperation { vector, .. }
Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,10 @@ internal fun DesignFrame(
}
)
}
val imageReplacement = loadDrawable(style.node_style.img_replacement_res_name, LocalContext.current)
if (customImage == null) {
customImage = imageReplacement
}

// Get the modeValues used to resolve variable values
val modeValues = VariableManager.currentModeValues(view.explicit_variable_modes)
Expand Down Expand Up @@ -294,7 +298,9 @@ internal fun DesignFrame(
horizontalArrangement = layoutInfo.arrangement,
verticalAlignment = layoutInfo.alignment
) {
content()
if (imageReplacement == null) {
content()
}
}
}
}
Expand Down Expand Up @@ -365,7 +371,9 @@ internal fun DesignFrame(
verticalArrangement = layoutInfo.arrangement,
horizontalAlignment = layoutInfo.alignment
) {
content()
if (imageReplacement == null) {
content()
}
}
}
}
Expand Down Expand Up @@ -690,7 +698,9 @@ internal fun DesignFrame(
m = m.then(layoutInfo.selfModifier)
DesignVariableExplicitModeValues(modeValues) {
DesignFrameLayout(m, view, layoutId, rootLayoutId, layoutState, designScroll) {
content()
if (imageReplacement == null) {
content()
}
}
}
}
Expand Down
53 changes: 32 additions & 21 deletions designcompose/src/main/java/com/android/designcompose/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,11 @@ internal fun mergeStyles(base: ViewStyle, override: ViewStyle): ViewStyle {
} else {
base.node_style.meter_data
}
nodeStyle.img_replacement_res_name = if (override.node_style.img_replacement_res_name.isPresent) {
override.node_style.img_replacement_res_name
} else {
base.node_style.img_replacement_res_name
}
style.layout_style = layoutStyle.build()
style.node_style = nodeStyle.build()
return style.build()
Expand Down Expand Up @@ -749,6 +754,7 @@ internal fun NodeStyle.asBuilder(): NodeStyle.Builder {
builder.aspect_ratio = aspect_ratio
builder.pointer_events = pointer_events
builder.meter_data = meter_data
builder.img_replacement_res_name = img_replacement_res_name
return builder
}

Expand Down Expand Up @@ -795,6 +801,7 @@ internal fun defaultNodeStyle(): NodeStyle.Builder {
builder.aspect_ratio = com.android.designcompose.serdegen.Number.Undefined()
builder.pointer_events = PointerEvents.Auto()
builder.meter_data = Optional.empty()
builder.img_replacement_res_name = Optional.empty()
builder.hyperlink = Optional.empty()
return builder
}
Expand Down Expand Up @@ -1343,27 +1350,17 @@ internal fun Background.asBrush(
is Background.Image -> {
val backgroundImage = this
val imageTransform = backgroundImage.transform.asSkiaMatrix()
if (DebugNodeManager.getUseLocalRes().value) {
backgroundImage.res_name.orElse(null)?.let {
val resId =
appContext.resources.getIdentifier(it, "drawable", appContext.packageName)
if (resId != Resources.ID_NULL) {
val bitmap =
BitmapFactoryWithCache.loadResource(appContext.resources, resId)
return Pair(
RelativeImageFill(
image = bitmap,
imageDensity = density,
displayDensity = density,
imageTransform = imageTransform,
scaleMode = backgroundImage.scale_mode
),
backgroundImage.opacity
)
} else {
Log.w(TAG, "No drawable resource $it found")
}
}
loadDrawable(backgroundImage.res_name, appContext)?.let {
return Pair(
RelativeImageFill(
image = it,
imageDensity = density,
displayDensity = density,
imageTransform = imageTransform,
scaleMode = backgroundImage.scale_mode
),
backgroundImage.opacity
)
}
val imageFillAndDensity =
backgroundImage.key.orElse(null)?.let { document.image(it.value, density) }
Expand Down Expand Up @@ -1481,6 +1478,20 @@ internal fun Background.asBrush(
return null
}

internal fun loadDrawable(resName: Optional<String>, appContext: Context): Bitmap? {
if (DebugNodeManager.getUseLocalRes().value) {
resName.orElse(null)?.let {
val resId = appContext.resources.getIdentifier(it, "drawable", appContext.packageName)
if (resId != Resources.ID_NULL) {
return BitmapFactoryWithCache.loadResource(appContext.resources, resId)
} else {
Log.w(TAG, "No drawable resource $it found")
}
}
}
return null
}

internal fun com.android.designcompose.serdegen.Path.asPath(
density: Float,
scaleX: Float,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import com.android.designcompose.annotation.DesignDoc
// TEST Image Update Test. After this loads, rename #Stage in the Figma doc. After the app
// updates,
// rename it back to #Stage. The image should reload correctly.
@DesignDoc(id = "oQw7kiy94fvdVouCYBC9T0")
@DesignDoc(id = "6jg63z15XG1GdIt3F62Ojy")
interface ImageUpdateTest {
@DesignComponent(node = "#Stage") fun Main() {}
}
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion support-figma/extended-layout-plugin/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
{ "name": "Generate String Resource", "command": "localization" },
{ "name": "Clear String Resource", "command": "clear-localization" },
{ "name": "Export Images", "command": "export-images" },
{ "name": "Clear Image Resource", "command": "clear-image-res" }
{ "name": "Clear Image Resource", "command": "clear-image-res" },
{ "name": "Export Vectors", "command": "export-vectors" },
{ "name": "Clear Vector Resource", "command": "clear-vector-res" }
]
}
],
Expand Down
11 changes: 11 additions & 0 deletions support-figma/extended-layout-plugin/src/code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import * as Utils from "./utils";
import * as Localization from "./localization-module";
import * as DesignSpecs from "./design-spec-module";
import * as ImageRes from "./image-res-module";
import * as VectorRes from "./vector-res-module";

// Warning component.
interface ClippyWarningRun {
Expand Down Expand Up @@ -564,6 +565,16 @@ if (figma.command === "sync") {
};
} else if (figma.command === "clear-image-res") {
ImageRes.clear();
} else if (figma.command === "export-vectors") {
VectorRes.exportAllVectorsAsync();
figma.ui.onmessage = (msg) => {
if (msg.msg === "show-node") {
Utils.showNode(msg.node);
} else if (msg.msg === "update-vector-res-name") {
}
}
} else if (figma.command === "clear-vector-res") {

} else if (figma.command === "move-plugin-data") {
function movePluginDataWithKey(node: BaseNode, key: string) {
// Read the private plugin data, write to shared
Expand Down
9 changes: 9 additions & 0 deletions support-figma/extended-layout-plugin/src/image-res-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,3 +242,12 @@ function parseCachedImageHashToResMap(imageHashToResNameData?: string) {
);
return cachedImageHashToResMap;
}

export function loadImageDrawableResNames(): Array<string> {
let imageResNames = new Array<string>();
let cachedHashToResMap = loadExportedImages();
cachedHashToResMap.forEach((resName, _) => imageResNames.push(resName));
let cachedHashToResMapExcluded = loadNonExportedImages();
cachedHashToResMapExcluded.forEach((resName, _) => imageResNames.push(resName));
return imageResNames;
}
104 changes: 94 additions & 10 deletions support-figma/extended-layout-plugin/src/ui.html
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,18 @@
</div>
<!-- END OF IMAGE RES PLUGIN UI -->

<!-- BEGIN OF VECTOR RES PLUGIN UI -->
<div id="vectorExport" class="page page-padding-large">
<div style="border-bottom: solid #000000; font-weight: bold;">Vector Frames:</div>
<div style="font-size: 14; margin-top: 8px;"><li>Vector frames will be exported as png files, supporting ldpi, mdpi, hdpi, xhdpi and xxhdpi densities.</li></div>
<hr/>
<div id="vectorInfo"></div>
<button id="downloadVectorButton" class="styled-button" style="width: 200px; margin-top: 16px;">
Download
</button>
</div>
<!-- END OF VECTOR RES PLUGIN UI -->

<div id="toast"></div>

<script>
Expand Down Expand Up @@ -1341,17 +1353,17 @@

// jpeg and png will be downloaded as png. Gif will be downloaded as gif. Other
// image types are not supported.
async function processImage(imageHash, imageBytes) {
async function processImage(imageBytes) {
const imageType = getImageType(imageBytes);
switch(imageType) {
case 'png':
case 'jpeg':
const fileFormat = 'png';
const canvas = document.createElement('canvas');
canvas.style.marginTop = '8px';
const ctx = canvas.getContext('2d');
const newBytes = await convert(canvas, ctx, imageBytes);
return {element: canvas, outputImgBytes: newBytes, imageFormat: fileFormat};
case 'png':
case 'gif':
default:
const blob = new Blob([imageBytes], { type: `image/${imageType}` });
Expand Down Expand Up @@ -1407,7 +1419,7 @@
resNameElement.appendChild(input);
imageInfo.appendChild(resNameElement);

let {element, outputImgBytes, imageFormat} = await processImage(imageHash, imageBytes);
let {element, outputImgBytes, imageFormat} = await processImage(imageBytes);
imageInfo.appendChild(element);

if (exportedImageHashArray.includes(imageHash)) {
Expand Down Expand Up @@ -1460,8 +1472,7 @@
link.className = 'button button--primary';
link.href = blobURL;
link.download = "drawable.zip"
link.click()
link.setAttribute('download', name + '.zip');
link.click();

parent.postMessage({
pluginMessage: {
Expand All @@ -1472,12 +1483,85 @@
});
}

// Display the vector frames
async function createVectorImageOutput(vectorResArray) {
const vectorInfo = document.getElementById('vectorInfo');
vectorInfo.innerHTML = "";

const vectorResNameMap = new Map(vectorResArray);
const jszip = new JSZip();
for (const [vectorResName, vectorRes] of vectorResNameMap) {
const resNameElement = document.createElement('div');
resNameElement.style.marginTop = '8px';
resNameElement.textContent = 'Image res name: ';
const resName = vectorResName;
const input = document.createElement("input");
input.addEventListener('change', function (evt) {
const newValue = this.value;
parent.postMessage({
pluginMessage: {
msg: 'update-vector-res-name',
resName: newValue,
node: vectorRes["nodeId"],
}
}, '*');
});
input.setAttribute("type", "text");
input.setAttribute("value", resName);
resNameElement.appendChild(input);
vectorInfo.appendChild(resNameElement);

let {element, outputImgBytes, imageFormat} = await processImage(vectorRes["mdpiBytes"]);
vectorInfo.appendChild(element);

const nodeIdElement = document.createElement('div');
nodeIdElement.style.marginTop = '8px';
nodeIdElement.textContent = "Node id: ";
nodeIdElement.appendChild(createNodeIdSpan(vectorRes["nodeId"]));
vectorInfo.appendChild(nodeIdElement);

const divider = document.createElement('hr');
vectorInfo.appendChild(divider);

jszip.file(`drawable/${resName}.${imageFormat}`, vectorRes["ldpiBytes"], {base64: true});
jszip.file(`drawable-mdpi/${resName}.${imageFormat}`, outputImgBytes, {base64: true});
jszip.file(`drawable-hdpi/${resName}.${imageFormat}`, vectorRes["hdpiBytes"], {base64: true});
jszip.file(`drawable-xhdpi/${resName}.${imageFormat}`, vectorRes["xhdpiBytes"], {base64: true});
jszip.file(`drawable-xxhdpi/${resName}.${imageFormat}`, vectorRes["xxhdpiBytes"], {base64: true});
jszip.file(`drawable-xxxhdpi/${resName}.${imageFormat}`, vectorRes["xxxhdpiBytes"], {base64: true});
}

const downloadButton = document.getElementById('downloadVectorButton');

downloadButton.addEventListener('click', ()=>{

jszip.generateAsync({ type: 'blob' })
.then((content) => {
const blobURL = window.URL.createObjectURL(content);
const link = document.createElement('a');
link.className = 'button button--primary';
link.href = blobURL;
link.download = "res.zip";
link.click();

parent.postMessage({
pluginMessage: {
msg: 'close-plugin'
}
}, '*');
});
});

}

jsonInputFile.addEventListener('change', jsonInputFileChanged, false);
stringsXmlInputFile.addEventListener('change', stringsXmlInputFileChanged, false);

// Update the form from a selection change.
window.onmessage = async function (event) {
let msg = event.data.pluginMessage;
// JSZip also posts messages but not plugin messages. Add a check to avoid errors in logging.
if (!msg) return;

if (msg.msg == 'selection-cleared') {
currentSelection = null;
Expand Down Expand Up @@ -1602,10 +1686,8 @@
}
else if (msg.msg == 'meters-selection') {
setMeterData(msg);
}

if (msg.msg == 'localization') {
openPage('localizationOptions')
} else if (msg.msg == 'localization') {
openPage('localizationOptions');
} else if (msg.msg == 'localization-output') {
loadOutputStringData(msg.output);
createOutputStringTable('outputStrings');
Expand All @@ -1618,7 +1700,9 @@
} else if (msg.msg == 'image-export') {
openPage('imageExport');
createImageOutput(msg.imageNodesArray, msg.imageResNameArray, msg.imageBytesArray, msg.exportedImageHashArray);
} else if (msg.msg == 'vector-export') {
openPage('vectorExport');
createVectorImageOutput(msg.vectorResArray, msg.existingImageDrawableResNames);
}

}
</script>
Loading