diff --git a/README.md b/README.md
index 834d412..b612d47 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,13 @@ npm install empf-generator
# Example
```js
-const {EmpfGenerator, InkModeEnum, PrintBedEnum} = require("empf-generator");
+const {
+ CraftModeEnum,
+ EmpfExportFormatEnum,
+ EmpfGenerator,
+ InkModeEnum,
+ PrintBedEnum
+} = require("empf-generator");
const fs = require("fs");
const imagePath = "./image.png";
@@ -22,6 +28,8 @@ const outPath = "./output.empf";
(async () => {
const image = fs.readFileSync(imagePath);
const generator = new EmpfGenerator({
+ craftMode: CraftModeEnum.reliefTexture,
+ exportFormat: EmpfExportFormatEnum.studioEncrypted,
printBed: PrintBedEnum.standardFlatbed,
projectName: "Test Project"
});
@@ -60,6 +68,8 @@ Creates a new EmpfGenerator.
| [options.printBed] | PrintBedEnum | PrintBedEnum.standardFlatbed | The print bed to use. |
| [options.projectName] | string | "Untitled Design" | The name of the project. |
| [options.canvasBackground] | string | "#ffffff" | The canvas background color, as a hex code. |
+| [options.craftMode] | CraftModeEnum | CraftModeEnum.flat | The craft mode to mark in the project metadata. |
+| [options.exportFormat] | EmpfExportFormatEnum | EmpfExportFormatEnum.zip | The EMPF container format to export. |
@@ -91,7 +101,7 @@ Adds an image to the canvas.
-### empfGenerator.export(outPath)
+### empfGenerator.export(outPath, [options])
Exports the canvas to a .empf file.
**Kind**: instance method of [EmpfGenerator](#EmpfGenerator)
@@ -99,6 +109,47 @@ Exports the canvas to a .empf file.
| Param | Type | Description |
| --- | --- | --- |
| outPath | string | The output path to export to. |
+| [options] | Object | Export options. |
+| [options.exportFormat] | EmpfExportFormatEnum | Override the EMPF container format for this export. |
+
+
+
+## CraftModeEnum : enum
+Enum for eufyMake craft modes.
+
+**Kind**: global enum
+**Read only**: true
+**Properties**
+
+| Name | Type | Default |
+| --- | --- | --- |
+| flat | number | -1 |
+| reliefTexture | number | 0 |
+| texture | number | 1 |
+| textureRelief | number | 2 |
+| brushStrokes | number | 3 |
+| poster | number | 4 |
+| sticker | number | 5 |
+| gild | number | 6 |
+| raised | number | 7 |
+
+
+
+## EmpfExportFormatEnum : enum
+Enum for EMPF container formats.
+
+**Kind**: global enum
+**Read only**: true
+**Properties**
+
+| Name | Type | Default |
+| --- | --- | --- |
+| zip | string | "zip" |
+| studioEncrypted | string | "studioEncrypted" |
+
+`studioEncrypted` wraps the generated ZIP in the `eufyMake` AES-GCM envelope
+observed in current eufyMake Studio exports. Older Studio builds may still
+accept the default plain ZIP-style EMPF files.
@@ -133,4 +184,4 @@ Enum for print beds.
| Name | Type | Default |
| --- | --- | --- |
| standardFlatbed | string | "standardFlatbed" |
-| miniFlatbed | string | "miniFlatbed" |
\ No newline at end of file
+| miniFlatbed | string | "miniFlatbed" |
diff --git a/src/classes/EmpfGenerator.js b/src/classes/EmpfGenerator.js
index 57c8ad6..cd88c65 100644
--- a/src/classes/EmpfGenerator.js
+++ b/src/classes/EmpfGenerator.js
@@ -26,6 +26,8 @@ const JSZip = require("jszip");
const {v4: uuidv4} = require("uuid");
const CanvasObjectEnum = require("../enums/CanvasObjectEnum.js");
+const CraftModeEnum = require("../enums/CraftModeEnum.js");
+const EmpfExportFormatEnum = require("../enums/EmpfExportFormatEnum.js");
const InkModeEnum = require("../enums/InkModeEnum.js");
const PrintBedConstants = require("../constants/PrintBedConstants.js");
const PrintBedEnum = require("../enums/PrintBedEnum.js");
@@ -35,6 +37,25 @@ const mmToE1Units = require("../util/mmToE1Units.js");
// For ease of development, all units are kept in mm until export, when they are converted to E1 units.
+const STUDIO_AES_GCM_KEY = Buffer.from("ab24ba760a896cd89eb9e15a9caec7fa");
+const STUDIO_AES_GCM_NONCE_LENGTH = 12;
+const STUDIO_WRAPPER_FIELDS = [
+ [1, Buffer.from([1])],
+ [3, Buffer.from([1])],
+ [4, Buffer.concat([Buffer.from("offline"), Buffer.alloc(33)])],
+ [6, Buffer.from("application/zip")],
+ [5, Buffer.from("file")]
+];
+
+const CraftModeTagType = {
+ [CraftModeEnum.flat]: 0,
+ [CraftModeEnum.texture]: 1,
+ [CraftModeEnum.reliefTexture]: 2,
+ [CraftModeEnum.textureRelief]: 3,
+ [CraftModeEnum.raised]: 4,
+ [CraftModeEnum.gild]: 8
+};
+
/**
* Class for generating .empf files
*/
@@ -46,14 +67,20 @@ class EmpfGenerator {
* @param {PrintBedEnum} [options.printBed=PrintBedEnum.standardFlatbed] - The print bed to use.
* @param {string} [options.projectName=Untitled Design] - The name of the project.
* @param {string} [options.canvasBackground=#ffffff] - The canvas background color, as a hex code.
+ * @param {CraftModeEnum} [options.craftMode=CraftModeEnum.flat] - The craft mode to mark in the project metadata.
+ * @param {EmpfExportFormatEnum} [options.exportFormat=EmpfExportFormatEnum.zip] - The EMPF container format to export.
*/
constructor({
printBed = PrintBedEnum.standardFlatbed,
projectName = "Untitled Design",
- canvasBackground = "#ffffff"
+ canvasBackground = "#ffffff",
+ craftMode = CraftModeEnum.flat,
+ exportFormat = EmpfExportFormatEnum.zip
}) {
this._printBed = printBed;
this._projectName = projectName;
+ this._craftMode = craftMode;
+ this._exportFormat = exportFormat;
this._version = "5.3.0";
this._canvasObjects = [];
@@ -146,15 +173,26 @@ class EmpfGenerator {
/**
* Exports the canvas to a .empf file.
* @param {string} outPath - The output path to export to.
+ * @param {Object} [options] - Export options.
+ * @param {EmpfExportFormatEnum} [options.exportFormat] - Override the EMPF container format for this export.
*/
- async export(outPath) {
+ async export(outPath, options = {}) {
+ const exportFormat = options.exportFormat ?? this._exportFormat;
+ const zipBuf = await this._generateZipBuffer();
+ const outputBuf = exportFormat === EmpfExportFormatEnum.studioEncrypted
+ ? encryptStudioPayload(zipBuf)
+ : zipBuf;
+
+ await fs.writeFile(outPath, outputBuf);
+ }
+
+ async _generateZipBuffer() {
const zip = new JSZip();
zip.file("Asset/font/font_mapping.json", "{}");
zip.file(`Asset/project_file/canvas_${this._canvasID}.json`, JSON.stringify(this._generateCanvasJSON()));
zip.file("Metadata/project_info.json", JSON.stringify(this._generateProjectInfoJSON()));
- const zipBuf = await zip.generateAsync({type: "nodebuffer"});
- await fs.writeFile(outPath, zipBuf);
+ return zip.generateAsync({type: "nodebuffer"});
}
get _printBedData() {
@@ -245,6 +283,7 @@ class EmpfGenerator {
_generateProjectInfoJSON() {
const currentTimestamp = Math.floor(Date.now() / 1000);
+ const tagType = craftModeToTagType(this._craftMode);
return {
canvases: [{
base_map: this._printBedData.e1Data.base_map,
@@ -257,7 +296,8 @@ class EmpfGenerator {
is_standard_product: this._printBedData.e1Data.is_standard_product,
create_time: currentTimestamp,
update_time: currentTimestamp,
- extra: '{"cutData":"","appCavas":"","pcCavas":"","canvasShape":null,"bleedingLine":null}',
+ tag_type: tagType,
+ extra: '{"cutData":"","appCavas":"","pcCavas":"","canvasShape":null,"bleedingLine":null,"new_func_tag":"00"}',
material_list: [],
model_link: "",
project_id: this._projectID,
@@ -265,6 +305,7 @@ class EmpfGenerator {
printModel: 2,
imgQuality: 300,
printLayerData: [],
+ subPrintType: this._craftMode,
format_size_w: Math.round(e1UnitsToMM(this._printBedData.e1Data.base_map_width)),
format_size_h: Math.round(e1UnitsToMM(this._printBedData.e1Data.base_map_height)),
format_size_w_non: Math.round(e1UnitsToMM(this._printBedData.e1Data.base_map_width)),
@@ -293,7 +334,7 @@ class EmpfGenerator {
works_status: 0,
project_desc: "",
project_type: 1,
- tag_type: 0,
+ tag_type: tagType,
thumb_file: null // this is inconsistent with eufyMake Studio, hopefully it doesn't break things
},
canvasesIndex: 0
@@ -301,4 +342,35 @@ class EmpfGenerator {
}
}
-module.exports = EmpfGenerator;
\ No newline at end of file
+function craftModeToTagType(craftMode) {
+ return CraftModeTagType[craftMode] ?? 0;
+}
+
+function encryptStudioPayload(zipBuf) {
+ const nonce = crypto.randomBytes(STUDIO_AES_GCM_NONCE_LENGTH);
+ const cipher = crypto.createCipheriv("aes-256-gcm", STUDIO_AES_GCM_KEY, nonce);
+ const ciphertext = Buffer.concat([cipher.update(zipBuf), cipher.final()]);
+ return Buffer.concat([
+ createStudioWrapperHeader(),
+ nonce,
+ ciphertext,
+ cipher.getAuthTag()
+ ]);
+}
+
+function createStudioWrapperHeader() {
+ const fieldBuffers = STUDIO_WRAPPER_FIELDS.map(([tag, value]) => {
+ const field = Buffer.alloc(3 + value.length);
+ field.writeUInt8(tag, 0);
+ field.writeUInt16BE(value.length, 1);
+ value.copy(field, 3);
+ return field;
+ });
+ const headerLength = 12 + fieldBuffers.reduce((sum, field) => sum + field.length, 0);
+ const header = Buffer.alloc(12);
+ header.write("eufyMake", 0, "ascii");
+ header.writeUInt32BE(headerLength, 8);
+ return Buffer.concat([header, ...fieldBuffers]);
+}
+
+module.exports = EmpfGenerator;
diff --git a/src/enums/CraftModeEnum.js b/src/enums/CraftModeEnum.js
new file mode 100644
index 0000000..99e57de
--- /dev/null
+++ b/src/enums/CraftModeEnum.js
@@ -0,0 +1,22 @@
+"use strict";
+
+// Corresponds to subPrintType in Metadata/project_info.json canvas print_param.
+
+/**
+ * Enum for eufyMake craft modes.
+ * @readonly
+ * @enum {number}
+ */
+const CraftModeEnum = {
+ flat: -1,
+ reliefTexture: 0,
+ texture: 1,
+ textureRelief: 2,
+ brushStrokes: 3,
+ poster: 4,
+ sticker: 5,
+ gild: 6,
+ raised: 7
+};
+
+module.exports = CraftModeEnum;
diff --git a/src/enums/EmpfExportFormatEnum.js b/src/enums/EmpfExportFormatEnum.js
new file mode 100644
index 0000000..9ddf445
--- /dev/null
+++ b/src/enums/EmpfExportFormatEnum.js
@@ -0,0 +1,13 @@
+"use strict";
+
+/**
+ * Enum for EMPF container formats.
+ * @readonly
+ * @enum {string}
+ */
+const EmpfExportFormatEnum = {
+ zip: "zip",
+ studioEncrypted: "studioEncrypted"
+};
+
+module.exports = EmpfExportFormatEnum;
diff --git a/src/index.js b/src/index.js
index a32d0c3..014a319 100644
--- a/src/index.js
+++ b/src/index.js
@@ -21,6 +21,8 @@ along with this program. If not, see .
module.exports = {
EmpfGenerator: require("./classes/EmpfGenerator.js"),
+ CraftModeEnum: require("./enums/CraftModeEnum.js"),
+ EmpfExportFormatEnum: require("./enums/EmpfExportFormatEnum.js"),
InkModeEnum: require("./enums/InkModeEnum.js"),
PrintBedEnum: require("./enums/PrintBedEnum.js")
-};
\ No newline at end of file
+};