├── index.js ├── .npmignore ├── images ├── a.png ├── b.png ├── c.png ├── d.png ├── e.png ├── image.png ├── full_wf.png ├── gotenberg.png ├── sample_wf.png └── gotenberg_no_owrite.png ├── .gitignore ├── .vscode └── extensions.json ├── nodes ├── PdfMerge │ ├── PdfMerge.node.json │ ├── merge.svg │ ├── PdfMergeUtils.ts │ └── PdfMerge.node.ts └── CarboneNode │ ├── CarboneNode.node.json │ ├── fileword.svg │ ├── CarboneUtils.ts │ └── CarboneNode.node.ts ├── .editorconfig ├── .eslintrc.prepublish.js ├── gulpfile.js ├── carbone.d.ts ├── .github └── workflows │ └── npm-publish.yml ├── tsconfig.json ├── LICENSE_N8N.md ├── .prettierrc.js ├── CHANGELOG.md ├── .eslintrc.js ├── package.json ├── tslint.json ├── CODE_OF_CONDUCT.md ├── LICENSE_CARBONE.md └── README.md /index.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.tsbuildinfo 3 | -------------------------------------------------------------------------------- /images/a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jreyesr/n8n-nodes-carbonejs/HEAD/images/a.png -------------------------------------------------------------------------------- /images/b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jreyesr/n8n-nodes-carbonejs/HEAD/images/b.png -------------------------------------------------------------------------------- /images/c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jreyesr/n8n-nodes-carbonejs/HEAD/images/c.png -------------------------------------------------------------------------------- /images/d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jreyesr/n8n-nodes-carbonejs/HEAD/images/d.png -------------------------------------------------------------------------------- /images/e.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jreyesr/n8n-nodes-carbonejs/HEAD/images/e.png -------------------------------------------------------------------------------- /images/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jreyesr/n8n-nodes-carbonejs/HEAD/images/image.png -------------------------------------------------------------------------------- /images/full_wf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jreyesr/n8n-nodes-carbonejs/HEAD/images/full_wf.png -------------------------------------------------------------------------------- /images/gotenberg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jreyesr/n8n-nodes-carbonejs/HEAD/images/gotenberg.png -------------------------------------------------------------------------------- /images/sample_wf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jreyesr/n8n-nodes-carbonejs/HEAD/images/sample_wf.png -------------------------------------------------------------------------------- /images/gotenberg_no_owrite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jreyesr/n8n-nodes-carbonejs/HEAD/images/gotenberg_no_owrite.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | .tmp 4 | tmp 5 | dist 6 | npm-debug.log* 7 | yarn.lock 8 | .vscode/launch.json 9 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "EditorConfig.EditorConfig", 5 | "esbenp.prettier-vscode", 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /nodes/PdfMerge/PdfMerge.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "node": "n8n-nodes-carbonejs.pdfMerge", 3 | "nodeVersion": "1.0", 4 | "codexVersion": "1.0", 5 | "categories": ["Development", "Developer Tools"], 6 | "resources": { 7 | "primaryDocumentation": [ 8 | { 9 | "url": "https://pdf-lib.js.org/" 10 | } 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /nodes/CarboneNode/CarboneNode.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "node": "n8n-nodes-carbonejs.carboneNode", 3 | "nodeVersion": "1.0", 4 | "codexVersion": "1.0", 5 | "categories": ["Development", "Developer Tools"], 6 | "resources": { 7 | "primaryDocumentation": [ 8 | { 9 | "url": "https://carbone.io/" 10 | } 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = tab 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [package.json] 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | 18 | [*.yml] 19 | indent_style = space 20 | indent_size = 2 21 | -------------------------------------------------------------------------------- /.eslintrc.prepublish.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('@types/eslint').ESLint.ConfigData} 3 | */ 4 | module.exports = { 5 | extends: "./.eslintrc.js", 6 | 7 | overrides: [ 8 | { 9 | files: ['package.json'], 10 | plugins: ['eslint-plugin-n8n-nodes-base'], 11 | rules: { 12 | 'n8n-nodes-base/community-package-json-name-still-default': 'error', 13 | }, 14 | }, 15 | ], 16 | }; 17 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { task, src, dest } = require('gulp'); 3 | 4 | task('build:icons', copyIcons); 5 | 6 | function copyIcons() { 7 | const nodeSource = path.resolve('nodes', '**', '*.{png,svg}'); 8 | const nodeDestination = path.resolve('dist', 'nodes'); 9 | 10 | src(nodeSource).pipe(dest(nodeDestination)); 11 | 12 | const credSource = path.resolve('credentials', '**', '*.{png,svg}'); 13 | const credDestination = path.resolve('dist', 'credentials'); 14 | 15 | return src(credSource).pipe(dest(credDestination)); 16 | } 17 | -------------------------------------------------------------------------------- /nodes/PdfMerge/merge.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /carbone.d.ts: -------------------------------------------------------------------------------- 1 | // Overwrite an old @types/carbone declaration: 2 | // carbone.convert() no longer is convert(data, convertTo, options?, callback) 3 | // but convert(data, options?, callback), similar to the render() function 4 | // convertTo is moved into the options param 5 | // Can be deleted when PR https://github.com/DefinitelyTyped/DefinitelyTyped/pull/65614 is merged on DefinitelyTyped 6 | // When deleting, also remove L29 on tsconfig.json, which includes the carbone.d.ts file 7 | 8 | import type { RenderOptions, ConvertCallback } from 'carbone'; 9 | 10 | declare module 'carbone' { 11 | export function convert( 12 | data: Buffer, 13 | options: RenderOptions & { extension: string }, 14 | callback: ConvertCallback, 15 | ): void; 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: Release on NPM 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: actions/setup-node@v3 13 | with: 14 | node-version: 16 15 | - run: npm ci 16 | # - run: npm test 17 | 18 | release: 19 | needs: build 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v3 23 | - uses: actions/setup-node@v3 24 | with: 25 | node-version: 16 26 | registry-url: 'https://registry.npmjs.org' 27 | - run: npm ci && npm run build 28 | - name: Publish package on NPM 📦 29 | run: npm publish 30 | env: 31 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 32 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "target": "es2019", 7 | "lib": ["es2019", "es2020", "es2022.error"], 8 | "removeComments": true, 9 | "useUnknownInCatchVariables": false, 10 | "forceConsistentCasingInFileNames": true, 11 | "noImplicitAny": true, 12 | "noImplicitReturns": true, 13 | "noUnusedLocals": true, 14 | "strictNullChecks": true, 15 | "preserveConstEnums": true, 16 | "esModuleInterop": true, 17 | "resolveJsonModule": true, 18 | "incremental": true, 19 | "declaration": true, 20 | "sourceMap": true, 21 | "skipLibCheck": true, 22 | "outDir": "./dist/", 23 | }, 24 | "include": [ 25 | "credentials/**/*", 26 | "nodes/**/*", 27 | "nodes/**/*.json", 28 | "package.json", 29 | "README.md", 30 | "images/*.png", 31 | "carbone.d.ts", 32 | ], 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE_N8N.md: -------------------------------------------------------------------------------- 1 | Copyright 2022 n8n 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | /** 3 | * https://prettier.io/docs/en/options.html#semicolons 4 | */ 5 | semi: true, 6 | 7 | /** 8 | * https://prettier.io/docs/en/options.html#trailing-commas 9 | */ 10 | trailingComma: 'all', 11 | 12 | /** 13 | * https://prettier.io/docs/en/options.html#bracket-spacing 14 | */ 15 | bracketSpacing: true, 16 | 17 | /** 18 | * https://prettier.io/docs/en/options.html#tabs 19 | */ 20 | useTabs: true, 21 | 22 | /** 23 | * https://prettier.io/docs/en/options.html#tab-width 24 | */ 25 | tabWidth: 2, 26 | 27 | /** 28 | * https://prettier.io/docs/en/options.html#arrow-function-parentheses 29 | */ 30 | arrowParens: 'always', 31 | 32 | /** 33 | * https://prettier.io/docs/en/options.html#quotes 34 | */ 35 | singleQuote: true, 36 | 37 | /** 38 | * https://prettier.io/docs/en/options.html#quote-props 39 | */ 40 | quoteProps: 'as-needed', 41 | 42 | /** 43 | * https://prettier.io/docs/en/options.html#end-of-line 44 | */ 45 | endOfLine: 'lf', 46 | 47 | /** 48 | * https://prettier.io/docs/en/options.html#print-width 49 | */ 50 | printWidth: 100, 51 | }; 52 | -------------------------------------------------------------------------------- /nodes/PdfMerge/PdfMergeUtils.ts: -------------------------------------------------------------------------------- 1 | import { IBinaryData } from 'n8n-workflow'; 2 | import { PDFDocument } from 'pdf-lib'; 3 | 4 | const isPDFDocument = (data: IBinaryData) => data.mimeType === 'application/pdf'; 5 | 6 | const mergePdfs = async (document1: Buffer, document2: Buffer): Promise => { 7 | // This code is blessed by the author of pdf-lib himself, 8 | // see https://github.com/Hopding/pdf-lib/issues/252#issuecomment-566063380 9 | // Only change is to provide the input docs as buffers, not as fs.readFileSync calls 10 | 11 | const mergedPdf = await PDFDocument.create(); 12 | 13 | const pdfA = await PDFDocument.load(document1); 14 | const pdfB = await PDFDocument.load(document2); 15 | 16 | const copiedPagesA = await mergedPdf.copyPages(pdfA, pdfA.getPageIndices()); 17 | copiedPagesA.forEach((page) => mergedPdf.addPage(page)); 18 | 19 | const copiedPagesB = await mergedPdf.copyPages(pdfB, pdfB.getPageIndices()); 20 | copiedPagesB.forEach((page) => mergedPdf.addPage(page)); 21 | 22 | const mergedPdfFile = await mergedPdf.save(); 23 | 24 | return Buffer.from(mergedPdfFile); 25 | }; 26 | 27 | export { isPDFDocument, mergePdfs }; 28 | -------------------------------------------------------------------------------- /nodes/CarboneNode/fileword.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v1.2.0 [2025-04-18] 2 | 3 | - Remove the limitation on rendering only DOCX files (allow also XLSX and PPTX templates) 4 | 5 | ## v1.1.2 [2024-09-17] 6 | 7 | - Fix issue #7: sometimes, under unclear circumstances, rendering failed with an error `NodeOperationError: Unknown input file type`. 8 | Thanks to [@JV300381](https://github.com/JV300381) for reporting the issue and helping confirm that it was solved! 9 | 10 | ## v1.1.1 [2024-02-02] 11 | 12 | - Fix issue #4: if the template binary file had been stored to disk by N8N (as opposed to keeping it in memory), the Render operation would fail with the error `ERROR: The "data" argument must be of type string or an instance of Buffer, TypedArray, or DataView. Received an instance of Promise`. This was due to [a breaking change in N8N v1.9.0](https://github.com/n8n-io/n8n/blob/master/packages/cli/BREAKING-CHANGES.md#what-changed-3). Thanks to [@altvk88](https://github.com/altvk88) for reporting the issue and helping diagnose it! 13 | 14 | ## v1.1.0 [2023-08-25] 15 | 16 | - Add the ability to set [render options](https://carbone.io/api-reference.html#options). Thanks, [@mrtnblv](https://github.com/mrtnblv)! 17 | 18 | ## v1.0.1 [2023-05-30] 19 | 20 | - Add docs suggesting alternative for PDF rendering when running N8N on Docker (i.e. Gotenberg) 21 | 22 | ## v1.0.0 [2023-50-27] 23 | 24 | - Initial release 25 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('@types/eslint').ESLint.ConfigData} 3 | */ 4 | module.exports = { 5 | root: true, 6 | 7 | env: { 8 | browser: true, 9 | es6: true, 10 | node: true, 11 | }, 12 | 13 | parser: '@typescript-eslint/parser', 14 | 15 | parserOptions: { 16 | project: ['./tsconfig.json'], 17 | sourceType: 'module', 18 | extraFileExtensions: ['.json'], 19 | }, 20 | 21 | ignorePatterns: ['.eslintrc.js', '**/*.js', '**/node_modules/**', '**/dist/**'], 22 | 23 | overrides: [ 24 | { 25 | files: ['package.json'], 26 | plugins: ['eslint-plugin-n8n-nodes-base'], 27 | extends: ['plugin:n8n-nodes-base/community'], 28 | rules: { 29 | 'n8n-nodes-base/community-package-json-name-still-default': 'off', 30 | 'n8n-nodes-base/community-package-json-license-not-default': 'off' // We can't use straight MIT, since Carbone is licensed under something weird 31 | }, 32 | }, 33 | { 34 | files: ['./credentials/**/*.ts'], 35 | plugins: ['eslint-plugin-n8n-nodes-base'], 36 | extends: ['plugin:n8n-nodes-base/credentials'], 37 | rules: { 38 | 'n8n-nodes-base/cred-class-field-documentation-url-missing': 'off', 39 | 'n8n-nodes-base/cred-class-field-documentation-url-miscased': 'off', 40 | }, 41 | }, 42 | { 43 | files: ['./nodes/**/*.ts'], 44 | plugins: ['eslint-plugin-n8n-nodes-base'], 45 | extends: ['plugin:n8n-nodes-base/nodes'], 46 | rules: { 47 | 'n8n-nodes-base/node-execute-block-missing-continue-on-fail': 'off', 48 | 'n8n-nodes-base/node-resource-description-filename-against-convention': 'off', 49 | 'n8n-nodes-base/node-param-fixed-collection-type-unsorted-items': 'off', 50 | }, 51 | }, 52 | ], 53 | }; 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "n8n-nodes-carbonejs", 3 | "version": "1.2.0", 4 | "description": "A Carbone JS node that renders Word templates on n8n.io", 5 | "keywords": [ 6 | "n8n-community-node-package" 7 | ], 8 | "license": "SEE LICENSE IN LICENSE_CARBONE.md AND SEE LICENSE IN LICENSE_N8N.md", 9 | "homepage": "https://github.com/jreyesr/n8n-nodes-carbonejs", 10 | "author": { 11 | "name": "jreyesr", 12 | "url": "https://github.com/jreyesr" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/jreyesr/n8n-nodes-carbonejs.git" 17 | }, 18 | "main": "index.js", 19 | "scripts": { 20 | "build": "tsc && gulp build:icons", 21 | "dev": "tsc --watch", 22 | "format": "prettier nodes --write", 23 | "lint": "eslint nodes package.json", 24 | "lintfix": "eslint nodes package.json --fix", 25 | "prepublishOnly": "npm run build && npm run lint -c .eslintrc.prepublish.js nodes package.json" 26 | }, 27 | "files": [ 28 | "dist", 29 | "images" 30 | ], 31 | "n8n": { 32 | "n8nNodesApiVersion": 1, 33 | "nodes": [ 34 | "dist/nodes/CarboneNode/CarboneNode.node.js", 35 | "dist/nodes/PdfMerge/PdfMerge.node.js" 36 | ] 37 | }, 38 | "devDependencies": { 39 | "@types/carbone": "^3.2.1", 40 | "@types/express": "^4.17.6", 41 | "@types/request-promise-native": "~1.0.15", 42 | "@typescript-eslint/parser": "~5.45", 43 | "eslint-plugin-n8n-nodes-base": "^1.11.0", 44 | "gulp": "^4.0.2", 45 | "n8n-core": "*", 46 | "n8n-workflow": "*", 47 | "prettier": "^2.7.1", 48 | "typescript": "~4.8.4" 49 | }, 50 | "dependencies": { 51 | "carbone": "^3.5.5", 52 | "pdf-lib": "^1.17.1" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "linterOptions": { 3 | "exclude": [ 4 | "node_modules/**/*" 5 | ] 6 | }, 7 | "defaultSeverity": "error", 8 | "jsRules": {}, 9 | "rules": { 10 | "array-type": [ 11 | true, 12 | "array-simple" 13 | ], 14 | "arrow-return-shorthand": true, 15 | "ban": [ 16 | true, 17 | { 18 | "name": "Array", 19 | "message": "tsstyle#array-constructor" 20 | } 21 | ], 22 | "ban-types": [ 23 | true, 24 | [ 25 | "Object", 26 | "Use {} instead." 27 | ], 28 | [ 29 | "String", 30 | "Use 'string' instead." 31 | ], 32 | [ 33 | "Number", 34 | "Use 'number' instead." 35 | ], 36 | [ 37 | "Boolean", 38 | "Use 'boolean' instead." 39 | ] 40 | ], 41 | "class-name": true, 42 | "curly": [ 43 | true, 44 | "ignore-same-line" 45 | ], 46 | "forin": true, 47 | "jsdoc-format": true, 48 | "label-position": true, 49 | "indent": [ 50 | true, 51 | "tabs", 52 | 2 53 | ], 54 | "member-access": [ 55 | true, 56 | "no-public" 57 | ], 58 | "new-parens": true, 59 | "no-angle-bracket-type-assertion": true, 60 | "no-any": true, 61 | "no-arg": true, 62 | "no-conditional-assignment": true, 63 | "no-construct": true, 64 | "no-debugger": true, 65 | "no-default-export": true, 66 | "no-duplicate-variable": true, 67 | "no-inferrable-types": true, 68 | "ordered-imports": [ 69 | true, 70 | { 71 | "import-sources-order": "any", 72 | "named-imports-order": "case-insensitive" 73 | } 74 | ], 75 | "no-namespace": [ 76 | true, 77 | "allow-declarations" 78 | ], 79 | "no-reference": true, 80 | "no-string-throw": true, 81 | "no-unused-expression": true, 82 | "no-var-keyword": true, 83 | "object-literal-shorthand": true, 84 | "only-arrow-functions": [ 85 | true, 86 | "allow-declarations", 87 | "allow-named-functions" 88 | ], 89 | "prefer-const": true, 90 | "radix": true, 91 | "semicolon": [ 92 | true, 93 | "always", 94 | "ignore-bound-class-methods" 95 | ], 96 | "switch-default": true, 97 | "trailing-comma": [ 98 | true, 99 | { 100 | "multiline": { 101 | "objects": "always", 102 | "arrays": "always", 103 | "functions": "always", 104 | "typeLiterals": "ignore" 105 | }, 106 | "esSpecCompliant": true 107 | } 108 | ], 109 | "triple-equals": [ 110 | true, 111 | "allow-null-check" 112 | ], 113 | "use-isnan": true, 114 | "quotes": [ 115 | "error", 116 | "single" 117 | ], 118 | "variable-name": [ 119 | true, 120 | "check-format", 121 | "ban-keywords", 122 | "allow-leading-underscore", 123 | "allow-trailing-underscore" 124 | ] 125 | }, 126 | "rulesDirectory": [] 127 | } 128 | -------------------------------------------------------------------------------- /nodes/CarboneNode/CarboneUtils.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises'; 2 | import os from 'os'; 3 | import path from 'path'; 4 | 5 | import carbone from 'carbone'; 6 | 7 | import type {Readable} from 'stream'; 8 | import {IBinaryData, IExecuteFunctions} from 'n8n-workflow'; 9 | 10 | // These two functions come straight from https://advancedweb.hu/secure-tempfiles-in-nodejs-without-dependencies/#solution, 11 | // plus typing. These should be safe (from pesky hackers and race conditions), and require no third-party dependencies 12 | const withTempFile = (fn: (fileName: string) => T) => 13 | withTempDir((dir: string) => fn(path.join(dir, 'file'))); 14 | 15 | const withTempDir = async (fn: (dirPath: string) => T): Promise => { 16 | const dir = await fs.mkdtemp((await fs.realpath(os.tmpdir())) + path.sep); 17 | try { 18 | return await fn(dir); 19 | } finally { 20 | await fs.rm(dir, {recursive: true}); 21 | } 22 | }; 23 | 24 | const isOfficeDocument = (data: IBinaryData) => 25 | [ 26 | 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 27 | 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 28 | 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 29 | ].includes(data.mimeType); 30 | 31 | const buildOptions = (node: IExecuteFunctions, index: number): object => { 32 | const additionalFields = node.getNodeParameter('options', index); 33 | // console.debug(additionalFields); 34 | 35 | let options: any = {}; 36 | if (additionalFields.timezone) options.timezone = additionalFields.timezone; 37 | if (additionalFields.lang) options.lang = additionalFields.lang; 38 | if (additionalFields.variableStr) options.variableStr = additionalFields.variableStr; 39 | if (additionalFields.complement) options.complement = JSON.parse(additionalFields.complement as string); 40 | if (additionalFields.enum) options.enum = JSON.parse(additionalFields.enum as string); 41 | if (additionalFields.translations) options.translations = JSON.parse(additionalFields.translations as string); 42 | 43 | // console.debug(options) 44 | return options; 45 | }; 46 | 47 | const renderDocument = async ( 48 | document: Buffer | Readable, 49 | context: any, 50 | options: object, 51 | ): Promise => { 52 | return withTempFile(async (file) => { 53 | await fs.writeFile(file, document); // Save the template to temp dir, since Carbone needs to read from disk 54 | 55 | return await new Promise((resolve, reject) => { 56 | carbone.render(file, context, options, function (err, result) { 57 | if (err) { 58 | reject(err); 59 | } 60 | if (typeof result === 'string') { 61 | // manually cast result to Buffer first 62 | resolve(Buffer.from(result, 'utf-8')); 63 | } else { 64 | // result must be Buffer, and TS is happy again 65 | resolve(result); 66 | } 67 | }); 68 | }); 69 | }); 70 | }; 71 | 72 | const convertDocumentToPdf = async (document: Buffer): Promise => { 73 | var options = { 74 | convertTo: 'pdf', 75 | extension: 'docx', 76 | }; 77 | 78 | return await new Promise((resolve, reject) => { 79 | carbone.convert(document, options, function (err, result) { 80 | if (err) { 81 | reject(err); 82 | } 83 | 84 | resolve(result); 85 | }); 86 | }); 87 | }; 88 | 89 | export { 90 | withTempFile, 91 | withTempDir, 92 | isOfficeDocument, 93 | buildOptions, 94 | renderDocument, 95 | convertDocumentToPdf, 96 | }; 97 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at jan@n8n.io. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /nodes/PdfMerge/PdfMerge.node.ts: -------------------------------------------------------------------------------- 1 | import { IExecuteFunctions } from 'n8n-core'; 2 | import { 3 | INodeExecutionData, 4 | INodeProperties, 5 | INodeType, 6 | INodeTypeDescription, 7 | IPairedItemData, 8 | NodeOperationError, 9 | } from 'n8n-workflow'; 10 | import { BINARY_ENCODING } from 'n8n-workflow'; 11 | import { isPDFDocument, mergePdfs } from './PdfMergeUtils'; 12 | 13 | const nodeOperationOptions: INodeProperties[] = [ 14 | { 15 | displayName: 'Property Name 1', 16 | name: 'dataPropertyName1', 17 | type: 'string', 18 | default: 'data', 19 | description: 20 | 'Name of the binary property for the first input which holds the document to be used', 21 | }, 22 | { 23 | displayName: 'Property Name 2', 24 | name: 'dataPropertyName2', 25 | type: 'string', 26 | default: 'data', 27 | description: 28 | 'Name of the binary property for the second input which holds the document to be used', 29 | }, 30 | { 31 | displayName: 'Property Name Out', 32 | name: 'dataPropertyNameOut', 33 | type: 'string', 34 | default: 'data', 35 | description: 'Name of the binary property where the combined document will be output', 36 | }, 37 | ]; 38 | 39 | export class PdfMerge implements INodeType { 40 | description: INodeTypeDescription = { 41 | displayName: 'Merge PDF', 42 | name: 'pdfMerge', 43 | icon: 'file:merge.svg', 44 | group: ['transform'], 45 | version: 1, 46 | description: 'Merges two PDF documents', 47 | defaults: { 48 | name: 'Merge PDF', 49 | }, 50 | inputs: ['main', 'main'], 51 | inputNames: ['Document 1', 'Document 2'], 52 | outputs: ['main'], 53 | properties: [...nodeOperationOptions], 54 | }; 55 | 56 | async execute(this: IExecuteFunctions): Promise { 57 | const items1 = this.getInputData(0); 58 | const items2 = this.getInputData(1); 59 | 60 | if (items1.length !== items2.length) { 61 | throw new NodeOperationError( 62 | this.getNode(), 63 | `Invalid inut lengths! Both inputs should have the same number of items, but had ${items1.length} and ${items2.length} items, respectively.`, 64 | ); 65 | } 66 | 67 | const returnData: INodeExecutionData[] = []; 68 | 69 | for (let itemIndex = 0; itemIndex < items1.length; itemIndex++) { 70 | const dataPropertyName1 = this.getNodeParameter('dataPropertyName1', itemIndex) as string; 71 | const dataPropertyName2 = this.getNodeParameter('dataPropertyName2', itemIndex) as string; 72 | const dataPropertyNameOut = this.getNodeParameter('dataPropertyNameOut', itemIndex) as string; 73 | const item1 = items1[itemIndex]; 74 | const item2 = items2[itemIndex]; 75 | 76 | try { 77 | const binaryData1 = this.helpers.assertBinaryData(itemIndex, dataPropertyName1); 78 | const binaryData2 = this.helpers.assertBinaryData(itemIndex, dataPropertyName2); 79 | if (!isPDFDocument(binaryData1)) { 80 | // Sanity check: only allow PDFs 81 | throw new NodeOperationError( 82 | this.getNode(), 83 | `Input 1 (on binary property "${dataPropertyName2}") should be a PDF file, was ${binaryData2.mimeType} instead`, 84 | { itemIndex }, 85 | ); 86 | } 87 | if (!isPDFDocument(binaryData2)) { 88 | // Sanity check: only allow PDFs 89 | throw new NodeOperationError( 90 | this.getNode(), 91 | `Input 2 (on binary property "${dataPropertyName2}") should be a PDF file, was ${binaryData2.mimeType} instead`, 92 | { itemIndex }, 93 | ); 94 | } 95 | let fileContent1: Buffer; 96 | if (binaryData1.id) { 97 | fileContent1 = await this.helpers.binaryToBuffer( 98 | this.helpers.getBinaryStream(binaryData1.id), 99 | ); 100 | } else { 101 | fileContent1 = Buffer.from(binaryData1.data, BINARY_ENCODING); 102 | } 103 | let fileContent2: Buffer; 104 | if (binaryData2.id) { 105 | fileContent2 = await this.helpers.binaryToBuffer( 106 | this.helpers.getBinaryStream(binaryData2.id), 107 | ); 108 | } else { 109 | fileContent2 = Buffer.from(binaryData2.data, BINARY_ENCODING); 110 | } 111 | 112 | const merged = await mergePdfs(fileContent1, fileContent2); 113 | 114 | // Add the rendered file in a new property 115 | returnData.push({ 116 | json: { 117 | ...item1.json, 118 | ...item2.json, 119 | }, 120 | binary: { 121 | ...item1.binary, 122 | ...item2.binary, 123 | [dataPropertyNameOut]: await this.helpers.prepareBinaryData( 124 | merged, 125 | dataPropertyNameOut + '.pdf', 126 | 'application/pdf', 127 | ), 128 | }, 129 | pairedItem: [item1.pairedItem as IPairedItemData, item2.pairedItem as IPairedItemData], 130 | }); 131 | } catch (error) { 132 | if (this.continueOnFail()) { 133 | // Carry on with the data that was provided as input (short-circuit the node) 134 | returnData.push({ 135 | json: { 136 | ...item1.json, 137 | ...item2.json, 138 | }, 139 | binary: { 140 | ...item1.binary, 141 | ...item2.binary, 142 | }, 143 | pairedItem: [item1.pairedItem as IPairedItemData, item2.pairedItem as IPairedItemData], 144 | }); 145 | } else { 146 | // Adding `itemIndex` allows other workflows to handle this error 147 | if (error.context) { 148 | // If the error thrown already contains the context property, 149 | // only append the itemIndex 150 | error.context.itemIndex = itemIndex; 151 | throw error; 152 | } 153 | throw new NodeOperationError(this.getNode(), error, { 154 | itemIndex, 155 | }); 156 | } 157 | } 158 | } 159 | 160 | return [returnData]; 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /nodes/CarboneNode/CarboneNode.node.ts: -------------------------------------------------------------------------------- 1 | import {IExecuteFunctions} from 'n8n-core'; 2 | import { 3 | INodeExecutionData, 4 | INodeProperties, 5 | INodePropertyOptions, 6 | INodeType, 7 | INodeTypeDescription, 8 | NodeOperationError, 9 | } from 'n8n-workflow'; 10 | import {convertDocumentToPdf, isOfficeDocument, renderDocument, buildOptions} from './CarboneUtils'; 11 | 12 | const nodeOperations: INodePropertyOptions[] = [ 13 | { 14 | name: 'Render', 15 | value: 'render', 16 | description: 17 | 'Fills a DOCX template with the contents of a JSON document, to generate a filled "report"', 18 | action: 'Render template', 19 | }, 20 | { 21 | name: 'Convert to PDF', 22 | value: 'toPdf', 23 | description: 'Converts a document into a PDF, using LibreOffice', 24 | action: 'Convert to PDF', 25 | }, 26 | ]; 27 | 28 | const nodeOperationOptions: INodeProperties[] = [ 29 | { 30 | displayName: 'Context', 31 | name: 'context', 32 | type: 'json', 33 | default: '{}', 34 | description: 'This data will be used to fill the template', 35 | displayOptions: { 36 | show: {operation: ['render']}, 37 | }, 38 | }, 39 | { 40 | displayName: 'Property Name', 41 | name: 'dataPropertyName', 42 | type: 'string', 43 | default: 'data', 44 | description: 'Name of the binary property which holds the document to be used', 45 | displayOptions: { 46 | show: {operation: ['render', 'toPdf']}, 47 | }, 48 | }, 49 | { 50 | displayName: 'Property Name Out', 51 | name: 'dataPropertyNameOut', 52 | type: 'string', 53 | default: 'data', 54 | description: 'Name of the binary property which will hold the converted document', 55 | displayOptions: { 56 | show: {operation: ['render', 'toPdf']}, 57 | }, 58 | }, 59 | ]; 60 | 61 | const nodeOptions: INodeProperties[] = [ 62 | { 63 | displayName: 'Options', 64 | name: 'options', 65 | type: 'collection', 66 | placeholder: 'Add Option', 67 | default: {}, 68 | options: [ 69 | { 70 | displayName: 'Timezone', 71 | name: 'timezone', 72 | type: 'string', 73 | default: 'Europe/Paris', 74 | description: 75 | 'Convert document dates to a timezone. The date must be chained with the `:formatD` formatter. See https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List, in the column "TZ identifier"', 76 | }, 77 | { 78 | displayName: 'Locale', 79 | name: 'lang', 80 | type: 'string', 81 | default: 'en', 82 | description: 83 | 'Locale of the generated document, it will used for translation `{t()}`, formatting numbers with `:formatN`, and currencies `:formatC`. See https://github.com/carboneio/carbone/blob/master/formatters/_locale.js.', 84 | }, 85 | { 86 | displayName: 'Complement', 87 | name: 'complement', 88 | type: 'json', 89 | default: '{}', 90 | description: 'Extra data accessible in the template with {c.} instead of {d.}', 91 | }, 92 | { 93 | displayName: 'Alias', 94 | name: 'variableStr', 95 | type: 'string', 96 | default: '', 97 | placeholder: 'e.g. {#def = d.id}', // eslint-disable-line n8n-nodes-base/node-param-placeholder-miscased-id 98 | description: 'Predefined alias. See https://carbone.io/documentation.html#alias.', 99 | }, 100 | { 101 | displayName: 'Enums', 102 | name: 'enum', 103 | type: 'json', 104 | default: '', 105 | placeholder: 'e.g. {"ORDER_STATUS": ["open", "close"]}', 106 | description: 'Object with enumerations, use it in reports with `convEnum` formatters', 107 | }, 108 | { 109 | displayName: 'Translations', 110 | name: 'translations', 111 | type: 'json', 112 | default: '', 113 | placeholder: 'e.g. {"es-es": {"one": "uno"}}', 114 | description: 115 | 'When the report is generated, all text between `{t( )}` is replaced with the corresponding translation. The `lang` option is required to select the correct translation. See https://carbone.io/documentation.html#translations', 116 | }, 117 | ], 118 | displayOptions: { 119 | show: {operation: ['render']}, 120 | }, 121 | }, 122 | ]; 123 | 124 | export class CarboneNode implements INodeType { 125 | description: INodeTypeDescription = { 126 | displayName: 'Carbone', 127 | name: 'carboneNode', 128 | icon: 'file:fileword.svg', 129 | group: ['transform'], 130 | version: 1, 131 | subtitle: '={{ $parameter["operation"]}}', 132 | description: 'Operations with the Carbone document generator', 133 | defaults: { 134 | name: 'Carbone', 135 | }, 136 | inputs: ['main'], 137 | outputs: ['main'], 138 | properties: [ 139 | { 140 | displayName: 'Operation', 141 | name: 'operation', 142 | type: 'options', 143 | noDataExpression: true, 144 | options: nodeOperations, 145 | default: 'render', 146 | }, 147 | { 148 | displayName: 149 | 'This operation requires LibreOffice to be installed! If using Docker, see this link for a suggested alternative.', 150 | name: 'notice', 151 | type: 'notice', 152 | default: '', 153 | displayOptions: { 154 | show: {operation: ['toPdf']}, 155 | }, 156 | }, 157 | ...nodeOperationOptions, 158 | ...nodeOptions, 159 | ], 160 | }; 161 | 162 | async execute(this: IExecuteFunctions): Promise { 163 | const items = this.getInputData(); 164 | const returnData: INodeExecutionData[] = []; 165 | 166 | for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { 167 | try { 168 | const operation = this.getNodeParameter('operation', itemIndex); 169 | const dataPropertyName = this.getNodeParameter('dataPropertyName', itemIndex) as string; 170 | const dataPropertyNameOut = this.getNodeParameter( 171 | 'dataPropertyNameOut', 172 | itemIndex, 173 | ) as string; 174 | const item = items[itemIndex]; 175 | const newItem: INodeExecutionData = { 176 | json: {}, 177 | binary: {}, 178 | pairedItem: {item: itemIndex}, 179 | }; 180 | 181 | if (operation === 'render') { 182 | const context = JSON.parse(this.getNodeParameter('context', itemIndex, '') as string); 183 | 184 | const binaryData = this.helpers.assertBinaryData(itemIndex, dataPropertyName); 185 | if (!isOfficeDocument(binaryData)) { 186 | throw new NodeOperationError( 187 | this.getNode(), 188 | `Binary property "${dataPropertyName}" should be a DOCX (Word), XLSX (Excel) or PPTX (Powerpoint) file, was ${binaryData.mimeType} instead`, 189 | { 190 | itemIndex, 191 | }, 192 | ); 193 | } 194 | let fileContent = await this.helpers.getBinaryDataBuffer(itemIndex, dataPropertyName); 195 | console.debug("content =", fileContent.subarray(0, 30).toString("base64") + "...") 196 | 197 | const options = buildOptions(this, itemIndex); 198 | const rendered = await renderDocument(fileContent, context, options); 199 | console.debug("rendered =", rendered.subarray(0, 30).toString("base64") + "...") 200 | 201 | newItem.json = context; // Present the used context as the node's JSON output 202 | 203 | // Add the rendered file in a new property 204 | newItem.binary![dataPropertyNameOut] = await this.helpers.prepareBinaryData( 205 | rendered, 206 | item.binary![dataPropertyName].fileName, 207 | item.binary![dataPropertyName].mimeType, 208 | ); 209 | } else if (operation === 'toPdf') { 210 | this.helpers.assertBinaryData(itemIndex, dataPropertyName); 211 | 212 | let fileContent = await this.helpers.getBinaryDataBuffer(itemIndex, dataPropertyName); 213 | console.debug("content =", fileContent.subarray(0, 30).toString("base64") + "...") 214 | 215 | const converted = await convertDocumentToPdf(fileContent); 216 | console.debug("converted =", converted.subarray(0, 30).toString("base64") + "...") 217 | 218 | // Add the converted file in a new property 219 | newItem.binary![dataPropertyNameOut] = await this.helpers.prepareBinaryData( 220 | converted, 221 | item.binary![dataPropertyName].fileName?.replace('.docx', '.pdf') ?? "out.pdf", 222 | 'application/pdf', 223 | ); 224 | } 225 | 226 | returnData.push(newItem); 227 | } catch (error) { 228 | if (this.continueOnFail()) { 229 | // Carry on with the data that was provided as input (short-circuit the node) 230 | returnData.push({ 231 | json: this.getInputData(itemIndex)[0].json, 232 | binary: this.getInputData(itemIndex)[0].binary, 233 | error, 234 | pairedItem: itemIndex, 235 | }); 236 | } else { 237 | // Adding `itemIndex` allows other workflows to handle this error 238 | if (error.context) { 239 | // If the error thrown already contains the context property, 240 | // only append the itemIndex 241 | error.context.itemIndex = itemIndex; 242 | throw error; 243 | } 244 | throw new NodeOperationError(this.getNode(), error, { 245 | itemIndex, 246 | }); 247 | } 248 | } 249 | } 250 | 251 | return this.prepareOutputData(returnData); 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /LICENSE_CARBONE.md: -------------------------------------------------------------------------------- 1 | > Roughly speaking, as long as you are not offering Carbone Community Edition Software as a hosted 2 | Document-Generator-as-a-Service like [Carbone Cloud](https://carbone.io/pricing.html), you can use all Community 3 | features for free. For any questions, please contact contact@carbone.io . This license is inspired by the Timescale 4 | License Agreement. 5 | 6 | -------------- 7 | 8 | Effective Date: February 14, 2023 9 | 10 | # CARBONE COMMUNITY LICENSE AGREEMENT 11 | 12 | PLEASE READ CAREFULLY THIS CARBONE COMMUNITY LICENSE AGREEMENT ("CCL Agreement"), WHICH CONSTITUTES A LEGALLY BINDING 13 | AGREEMENT AND GOVERNS USE OF THE CARBONEIO DOCUMENT GENERATOR SOFTWARE AND RELATED SOFTWARE THAT IS PROVIDED SUBJECT TO 14 | THIS CCL AGREEMENT. BY INSTALLING OR USING SUCH SOFTWARE, YOU AGREE THAT YOU HAVE READ AND AGREE TO BE BOUND BY THE 15 | TERMS AND CONDITIONS OF THIS CCL AGREEMENT. IF YOU DO NOT AGREE WITH SUCH TERMS AND CONDITIONS, YOU MAY NOT INSTALL OR 16 | USE SUCH SOFTWARE. IF YOU ARE INSTALLING OR USING SUCH SOFTWARE ON BEHALF OF A LEGAL ENTITY, YOU REPRESENT AND WARRANT 17 | THAT YOU HAVE THE AUTHORITY TO AGREE TO THE TERMS AND CONDITIONS OF THIS CCL AGREEMENT ON BEHALF OF THAT LEGAL ENTITY 18 | AND THE RIGHT TO BIND THAT LEGAL ENTITY TO THIS CCL AGREEMENT. 19 | 20 | This CCL Agreement is entered into by and between CarboneIO SAS, registered under N°899 106 785 in the Register of 21 | companies of La Roche-sur-Yon, France ("CarboneIO") and you or the legal entity on whose behalf you are accepting this CCL 22 | Agreement ("You"). 23 | 24 | ## 0. BACKGROUND 25 | 26 | The CarboneIO Document Generator software and related software is offered as "open core" code. This means that only the 27 | core of the software is available for inspection and download at https://github.com/carboneio/carbone. The CarboneIO 28 | Document Generator is distributed under two versions: 29 | 30 | - Carbone Community Edition (referred to herein as the Carbone Community Edition Software, as defined below) is an open 31 | source software available freely under this **CCL Agreement** . 32 | - Carbone Enterprise Edition (hosted and on-premise) is a closed-source software with more features than the Carbone 33 | Community Edition Software, available only by a subscription at https://carbone.io. 34 | 35 | ## 1. GOVERNING LICENSES 36 | 37 | **1.1 Source Code.** The source code of Carbone Community Edition Software is made publicly available by CarboneIO at 38 | https://github.com/carboneio/carbone. The use of this software is governed by this CCL Agreement. 39 | 40 | **1.2 License Rights to Your Customers**. As set forth in Section 2.1 below, the use by Your customers of the Carbone 41 | Community Edition Software as part of any Value Added Products or Services that You distribute will be subject to the 42 | most current version of this CCL Agreement. 43 | 44 | ## 2. GRANT OF LICENSES 45 | 46 | **2.1 Grant**. Conditioned upon compliance with all of the terms and conditions of this CCL Agreement, CarboneIO grants 47 | to You at no charge the following limited, non-exclusive, non-transferable, fully paid up, worldwide licenses, 48 | without the right to grant or authorize sublicenses (except as set forth in Section 2.3): 49 | 50 | - **(a) Internal Use**. A license to copy, compile, install, and use the Carbone Community Edition Software and 51 | Derivative Works solely for Your own internal business purposes in a manner that does not expose or give access to, 52 | directly or indirectly (e.g., via a wrapper), the Carbone Community Edition Software feature to any person or 53 | entity other than You or Your employees and Contractors working on Your behalf. 54 | 55 | - **(b) Value Added Products or Services**. A license (i) to copy, compile, install, and use the Carbone Community 56 | Edition Software, Derivative Works, or parts thereof to develop and maintain Your Value Added Products or 57 | Services, (ii) to utilize (in the case of services) copies of the Carbone Community Edition Software, Derivative 58 | Works, or parts thereof solely as incorporated into or utilized with Your Value Added Products or Services, and 59 | (iii) to distribute (in the case of products that are distributed to Your customers) copies of the Carbone 60 | Community Edition Software binaries or of Derivative Works, and both solely as incorporated into or utilized with 61 | Your Value Added Products or Services; provided that You notify Your customers that use of such Carbone Community 62 | Edition Software or Derivative Works is subject to this CCL Agreement and You provide to each such customer a copy 63 | of the most current version of this CCL Agreement or a URL from which the most current version of this CCL 64 | Agreement may be obtained. 65 | 66 | - **(c) Distribution of Source Code or Binaries in Standalone Form**. Subject to the prohibitions in Section 2.2 below, 67 | a license to copy and distribute the Carbone Community Edition Software source code and binaries solely in 68 | unmodified standalone form and subject to the terms and conditions of the most current version of this CCL 69 | Agreement. 70 | 71 | - **(d) Derivative Works**. A license (i) to prepare, compile, and test Derivative Works of the Carbone Community 72 | Edition Software; (ii) to use Derivative Works for Internal Use solely as expressly permitted in Section 2.1(a); 73 | (iii) to utilize Derivative Works with Your Value Added Products or Services solely as expressly permitted in 74 | Section 2.1(b); (iv) to distribute Derivative Works with Your Value Added Products or Services solely as expressly 75 | permitted in Section 2.1(b); and (v) to distribute Derivative Works back to CarboneIO under CarboneIO's Contributor 76 | Agreement for potential incorporation into CarboneIO's maintained code base at its sole discretion. 77 | 78 | **2.2 Prohibitions.** Notwithstanding any other provision in this CCL Agreement, You are prohibited from (i) using any 79 | Carbone Community Edition Software to provide document-generator-as-a-service services, or to provide any form of 80 | software-as-a-service or service offering in which the Carbone Community Edition Software is offered or made 81 | available to third parties to provide Document Generator functions or operations, other than as part of Your Value 82 | Added Products or Services, or (ii) copying or distributing any Carbone Community Edition Software for use in any of 83 | the foregoing ways. In addition, You agree not to, except as expressly permitted in Section 2.1(d), prepare 84 | Derivative Works of any Carbone Community Edition Software or, except as expressly permitted herein, transfer, sell, 85 | rent, lease, sublicense, loan, or otherwise transfer or make available any Carbone Community Edition Software, 86 | whether in source code or binary executable form. 87 | 88 | **2.3 Affiliates and Contractors.** You may permit Your Contractors and Affiliates to exercise the licenses set forth in 89 | Section 2.1, provided that such exercise by Contractors must be solely for your benefit and/or the benefit of Your 90 | Affiliates, and You shall be responsible for all acts and omissions of such Contractors and Affiliates in connection 91 | with such exercise of the licenses, including but not limited to breach of any terms of this CCL Agreement. 92 | 93 | **2.4 Reservation of Rights.** Except as expressly set forth in Section 2.1, no other license or rights to the Carbone 94 | Community Edition Software are granted to You under this CCL Agreement, whether by implication, estoppel, or 95 | otherwise. 96 | 97 | ## 3. DEFINITIONS 98 | 99 | In addition to other terms defined elsewhere in this CCL Agreement, the terms below have the following meanings: 100 | 101 | **3.1 "Affiliate"** means, if You are a legal entity, any legal entity that controls, is controlled by, or which is 102 | under common control with, You, where "control" means ownership of at least fifty percent (50%) of the outstanding 103 | voting shares of the legal entity, or the contractual right to establish policy for, and manage the operations of, 104 | the legal entity. 105 | 106 | **3.2 "Contractor"** means a person or entity engaged as a consultant or contractor to perform work on Your behalf, but 107 | only to the extent such person or entity is performing such work on Your behalf. 108 | 109 | **3.3 "Derivative Work"** means any modification or enhancement made by You to the Carbone Community Edition Software, 110 | whether in source code, binary executable, intermediate, or other form. 111 | 112 | **3.4 "Document Generator"** means a tool that generates documents of any form and any type. 113 | 114 | **3.7 "Carbone Community Edition"** means those portions of the Carbone Enterprise Edition Software that CarboneIO makes 115 | publicly available for distribution from time to time as open source software under the terms of this **CCL 116 | Agreement**. 117 | 118 | **3.8 "Carbone Enterprise Edition"** means the full-feature Document Generator closed-source software and related 119 | software made available by CarboneIO on https://carbone.io. 120 | 121 | **3.9 "Value Added Products or Services"** means products or services developed by or for You that utilize (for example, 122 | as a back-end function or part of a software stack) all or parts of the Carbone Community Edition Software to provide 123 | Document Generator functions in support of larger value-added products or services (for example, an ERP software or 124 | vertical-specific application) with respect to which all of the following are true: 125 | 126 | - (i) such value-added products or services are not primarily Document Generator products or services; 127 | 128 | - (ii) such value-added products or services add substantial value of a different nature to the Document Generator 129 | provided by CarboneIO and are the key functions upon which such products or services are offered and marketed. 130 | 131 | 132 | ## 4. TERMINATION 133 | 134 | This CCL Agreement will automatically terminate, whether or not You receive notice of such termination from CarboneIO, 135 | in the event You breach any of its terms or conditions. In accordance with Section 6 below, CarboneIO shall have no 136 | liability for any damage, loss, or expense of any kind, whether consequential, indirect, or direct, suffered or 137 | incurred by You arising from or incident to the termination of this CCL Agreement, whether or not CarboneIO has been 138 | advised or is aware of any such potential damage, loss, or expense. 139 | 140 | ## 5. DISCLAIMER OF WARRANTIES 141 | 142 | TO THE MAXIMUM EXTENT PERMITTED UNDER APPLICABLE LAW, ALL CARBONEIO SOFTWARE PROVIDED UNDER THIS CCL AGREEMENT ARE 143 | PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND AND CARBONEIO DISCLAIMS ALL SUCH WARRANTIES, WHETHER EXPRESS, STATUTORY, 144 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, TITLE, FITNESS FOR A PARTICULAR PURPOSE, OR 145 | NON-INFRINGEMENT, AND ANY IMPLIED WARRANTIES ARISING FROM USAGE OF TRADE, COURSE OF DEALING, OR COURSE OF PERFORMANCE. 146 | WITHOUT LIMITING THE FOREGOING, CARBONEIO MAKES NO WARRANTY OR REPRESENTATION AS TO THE RELIABILITY, TIMELINESS, 147 | QUALITY, SUITABILITY, PROFITABILITY, SUPPORT, PERFORMANCE, LOSS OF USE OR LOSS OF DATA, AVAILABILITY, OR ACCURACY OF 148 | THE CARBONE COMMUNITY EDITION SOFTWARE. YOU ACKNOWLEDGE THAT CHANGES MADE BY CARBONEIO TO THE CARBONE COMMUNITY EDITION 149 | SOFTWARE MAY DISRUPT INTEROPERATION WITH YOUR VALUE ADDED PRODUCTS OR SERVICES. CARBONEIO AND ITS LICENSORS DO NOT 150 | WARRANT THAT THE CARBONE COMMUNITY EDITION SOFTWARE, OR ANY PORTION THEREOF, IS ERROR FREE OR WILL OPERATE WITHOUT 151 | INTERRUPTION, OR THAT ANY VALUE ADDED PRODUCT OR SERVICE INTEROPERATING WITH THE CARBONE COMMUNITY EDITION SOFTWARE 152 | WILL NOT EXPERIENCE LOSS OF USE OR LOSS OF DATA. YOU ACKNOWLEDGE THAT IN ENTERING INTO THIS CCL AGREEMENT, YOU HAVE NOT 153 | RELIED ON ANY PROMISE, WARRANTY, OR REPRESENTATION NOT EXPRESSLY SET FORTH IN THIS AGREEMENT. 154 | 155 | ## 6. LIMITATION OF LIABILITY 156 | 157 | TO THE MAXIMUM EXTENT PERMITTED UNDER APPLICABLE LAW, IN NO EVENT SHALL CARBONEIO OR ITS LICENSORS BE LIABLE TO YOU OR 158 | ANY THIRD PARTY FOR ANY DIRECT OR INDIRECT DAMAGES, INCLUDING BUT NOT LIMITED TO ANY LOSS OF PROFITS OR REVENUE, LOSS 159 | OF USE, BUSINESS INTERRUPTION, LOSS OF DATA, COST OF COVER OR SUBSTITUTE GOODS OR SERVICES, OR FOR ANY SPECIAL, 160 | INCIDENTAL, CONSEQUENTIAL, PUNITIVE, OR EXEMPLARY DAMAGES OF ANY KIND, HOWEVER CAUSED, RELATED TO, OR ARISING OUT OF 161 | THIS CCL AGREEMENT, ITS TERMINATION OR THE PERFORMANCE OR FAILURE TO PERFORM THIS CCL AGREEMENT, OR THE USE OR 162 | INABILITY TO USE THE CARBONE COMMUNITY EDITION SOFTWARE, WHETHER ALLEGED AS A BREACH OF CONTRACT, BREACH OF WARRANTY, 163 | TORTIOUS CONDUCT, INCLUDING NEGLIGENCE, OR ANY OTHER LEGAL THEORY, EVEN IF CARBONEIO HAS BEEN ADVISED OR IS AWARE OF 164 | THE POSSIBILITY OF SUCH DAMAGES. 165 | 166 | ## 7. GENERAL 167 | 168 | **7.1 Complete Agreement.** This CCL Agreement completely and exclusively states the entire agreement of the parties 169 | regarding the subject matter hereof and supersedes all prior proposals, agreements, or other communications between 170 | the parties, oral or written, regarding such subject matter. 171 | 172 | **7.2 Modification.** This CCL Agreement may be modified by CarboneIO from time to time, and any such modifications will 173 | be effective upon the "Posted Date" set forth at the top of the modified agreement. The modified agreement shall 174 | govern any new version of the Carbone Community Edition Software (and all its constituent source code and binaries) 175 | that is officially released as a complete version release by CarboneIO on or after such Posted Date. Except as set 176 | forth in this Section 7.2, this CCL Agreement may not be amended except by a writing executed by both parties. 177 | 178 | **7.3 Governing Law.** This CCL Agreement shall be governed by and construed solely under the laws of France. 179 | 180 | **7.4 Unenforceability.** If any provision of this CCL Agreement is held unenforceable, the remaining provisions of this 181 | CCL Agreement shall remain in effect and the unenforceable provision shall be replaced by an enforceable provision 182 | that best reflects the original intent of the parties. 183 | 184 | **7.5 Injunctive Relief.** You acknowledge that a breach or threatened breach of any provision of this CCL Agreement 185 | will cause irreparable harm to CarboneIO for which damages at law will not provide adequate relief, and CarboneIO 186 | shall therefore be entitled to injunctive relief against such breach or threatened breach. 187 | 188 | **7.6 Assignment.** You may not assign this CCL Agreement, including by operation of law in connection with a merger or 189 | acquisition or otherwise, in whole or in part, without the prior written consent of CarboneIO, which CarboneIO may 190 | grant or withhold in its sole and absolute discretion. Any assignment in violation of the preceding sentence is 191 | void. 192 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # n8n-nodes-carbonejs 2 | 3 | > [!TIP] 4 | > If you're interested in generating documents using N8N from a Word/Excel/Powerpoint template, you may also be 5 | > interested [in the `n8n-nodes-docxtemplater` node](https://github.com/jreyesr/n8n-nodes-docxtemplater), which 6 | > uses [Docxtemplater](https://docxtemplater.com/) as the rendering engine. 7 | > 8 | > Docxtemplater has a different syntax for filters/formatters and its node has more configurable options than this one. 9 | > It allows you to write your own Transforms (the equivalent to 10 | > Carbone's [Formatters](https://carbone.io/documentation/design/formatters/overview.html)) by using 11 | > [N8N's tool-calling functionality, commonly used on LLMs](https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/). 12 | > It also allows you to 13 | > define [custom data sources](https://docxtemplater.com/docs/async/#code-for-async-data-resolution) that are called 14 | > _while_ the document is rendering, as opposed to the standard JSON object that must be available before the document 15 | > is even read. It also supports [Docxtemplater Modules](https://docxtemplater.com/modules/), which add functionality 16 | > such as image rendering or more ways to generate tables, and can be bought from Docxtemplater or developed separately. 17 | 18 | This is an n8n community node. It lets you use [the Carbone JS library](https://carbone.io/) in your n8n workflows. 19 | 20 | Carbone is a report generator that lets you render JSON data into DOCX, PDF, XLSX and more formats: 21 | 22 | ![a diagram of the dataflow in Carbone](https://carbone.io/img/doc/carboneWorkflow.svg) 23 | 24 | [n8n](https://n8n.io/) is a [fair-code licensed](https://docs.n8n.io/reference/license/) workflow automation platform. 25 | 26 | > **NOTE:** Carbone is licensed under 27 | > the [Carbone Community License (CCL)](https://github.com/carboneio/carbone/blob/master/LICENSE.md), which says that " 28 | > Roughly speaking, as long as you are not offering Carbone Community Edition Software as a hosted 29 | > Document-Generator-as-a-Service like Carbone Cloud, you can use all Community features for free.". AFAICT, this means 30 | > that this plugin must also be distributed under CCL, and that you can't install it in a N8N instance and then use it 31 | > to 32 | > provide document generation as a service. You can use it for "your own internal business purposes" and "value-added 33 | > products or services", as long as they aren't primarily document generation services. 34 | 35 | [Installation](#installation) 36 | [Operations](#operations) 37 | [Compatibility](#compatibility) 38 | [Usage](#usage) 39 | [Resources](#resources) 40 | 41 | ## Installation 42 | 43 | Follow the [installation guide](https://docs.n8n.io/integrations/community-nodes/installation/) in the n8n community 44 | nodes documentation. 45 | 46 | ## Operations 47 | 48 | ### Render Document 49 | 50 | Must receive an input item with both `$json` and `$binary` keys. The `$json` key may be used to compose the "context", 51 | which will be provided to the templating engine. The `$binary` key should contain a DOCX document that contains a valid 52 | Carbone template. 53 | 54 | This operation can take "advanced options", which are passed directly to Carbone's rendering engine. 55 | See [Carbone's docs](https://carbone.io/api-reference.html#options) for information about each option. They appear in 56 | the Options dropdown, at the bottom of the Render operation: 57 | 58 | ![a screenshot of the advanced options](images/image.png) 59 | 60 | ### Convert to PDF 61 | 62 | > **NOTE:** This operation requires LibreOffice to be installed. If using the native NPM install, you should install 63 | > LibreOffice system-wide. If using the Docker images, this operation doesn't seem to work :( 64 | 65 | This node must receive items with a binary property containing a DOCX document. The selected document will be rendered 66 | into a PDF file using the LibreOffice renderer, 67 | since [according to one of the Carbone authors](https://github.com/carboneio/carbone/issues/41#issuecomment-528573164), " 68 | I tried to avoid LibreOffice because I wanted a tool really light and highly performant. But after many researches, I 69 | have never found a solution to convert a document more reliable than LibreOffice and which can run everywhere." 70 | 71 | ### Merge PDFs 72 | 73 | This node takes two inputs, like 74 | the [Merge node](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.merge/). Unlike the merge node, both 75 | inputs _must_ have the same amount of elements. The node will output a stream with the same amount of items, where every 76 | output item is the result of merging one element from `Input 1` and one element from `Input 2`, by position (i.e., first 77 | with first, second with second, and so on). 78 | 79 | ## Compatibility 80 | 81 | This plugin has been developed and tested on N8N version 0.228.2. It should work with older versions too, but it hasn't 82 | been tested. 83 | 84 | ## Usage 85 | 86 | ### Basic usage: render to a DOCX file 87 | 88 | The `Render` node needs to receive data items with _both_ JSON 89 | and [binary data](https://docs.n8n.io/courses/level-two/chapter-2/#binary-data). The binary data is the template, and 90 | the JSON data will be the context passed to the template. 91 | 92 | To generate these data items, you'll probably want to use 93 | a [Merge Node](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.merge), with Mode set to **Combine** 94 | and Combination Mode set to **Multiplex**. This generates a cross-join of all data in Input A and Input B (i.e., every 95 | possible combination of items). If you only input _one_ item in the template, you'll get one item out for each different 96 | JSON context. 97 | 98 | See below for an example: 99 | 100 | ![a sample workflow that generates sample JSON data, then reads a template from disk, then merges the JSON documents with the template, and then renders each template+JSON combo with the Carbone node](./images/sample_wf.png) 101 | 102 | The Merge node receives 3 items in its Input A, and 1 item in its input B. Every possible combination yields 3*1 = 3 103 | output combinations, each with the same template but different JSON content. 104 | 105 | The data items output by the node will also have JSON and binary data. The JSON data is the context that was used for 106 | rendering the document (which is not necessarily the same as the node's input `$json` key), and the binary data is 107 | another DOCX document, rendered from the input Template. 108 | 109 | ### Complicated usage: render, convert to PDF, then add a static "cover page" 110 | 111 | ![a sample workflow that renders a template into a set of DOCX files, then converts them into PDFs, and finally adds a cover page (taken from a PDF file) to the beginning of each PDF document](./images/full_wf.png) 112 | 113 | The workflow above is a more complicated version of the workflow, which uses everything that this node provides: 114 | 115 | 1. The rendering part is the same as above: the JSON contexts are generated somehow, the template is read from disk and 116 | replicated on every context, and then the Carbone node is used to render the documents 117 | 1. Every rendered document is converted to PDF 118 | 1. A "cover page" (a static PDF document) is read from disk and replicated on every PDF document (using the same Merge 119 | pattern as in the Simple Example above) 120 | 1. The Merge PDF node is used to add the cover letter at the start of every PDF document 121 | 122 | Here's how the data looks at different points in the path (look for the yellow notes with letters): 123 | 124 | A. The original template, which refers to a property in the context called `a`. Imagine that it's, say, the name of a 125 | user. 126 | 127 | ![a Word document containing a placeholder for the property "a"](./images/a.png) 128 | 129 | B. The document after being rendered, here with the context `{"a": 1}` 130 | 131 | ![a Word document that says "Hello, 1"](./images/b.png) 132 | 133 | C. The same document, converted to a PDF file 134 | 135 | ![a PDF document that says "Hello, 1"](./images/c.png) 136 | 137 | D. The "cover page" 138 | 139 | ![a PDF document with a single page that says "Sample Report"](./images/d.png) 140 | 141 | E. The document, now with the cover page added. Note that it has two pages, coming from two different documents 142 | 143 | ![a PDF document with two pages, the first one says "Sample Report" and the second one says "Hello, 1"](./images/e.png) 144 | 145 | ## Resources 146 | 147 | * [n8n community nodes documentation](https://docs.n8n.io/integrations/community-nodes/) 148 | * [General Carbone docs](https://carbone.io/documentation.html) 149 | * [A short tutorial on template design](https://help.carbone.io/en-us/article/how-to-create-a-template-nm284z) 150 | * [Detailed docs on template design](https://carbone.io/documentation.html#design-your-first-template) 151 | 152 | ## Usage with Docker 153 | 154 | > **NOTE:** As of now, I haven't been able to make Carbone work with LibreOffice in Docker, so it probably won't work 155 | > with N8N Docker deployments. If you manage to make it work, please open an issue! 156 | > 157 | > As of now, if you need to convert rendered documents to PDF, I recommend either a) using native (NPM) deployments for 158 | > N8N, o b) using a standalone Docker container that exposes some sort of REST API, deploying it alongside the N8N 159 | > container, and using 160 | > the [HTTP Request node](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.httprequest/) to interface 161 | > with it. 162 | > 163 | > May I suggest [Gotenberg](https://github.com/gotenberg/gotenberg), just for the awesome portrait of a Gutenberg 164 | > gopher? Note that I can't vouch for the security of that service, but it seems legit and active. 165 | 166 | Using the node with a native N8N instance (i.e., [one installed with 167 | `npm`](https://docs.n8n.io/hosting/installation/npm/)) is relatively easy: install LibreOffice using the OS's utilities, 168 | if required (e.g. `apt install libreoffice-common` on Ubuntu), then install the node with `npm` or using the N8N UI, and 169 | then use it. 170 | 171 | [Docker deployments](https://docs.n8n.io/hosting/installation/docker/) add some complexity, since you can't necessarily 172 | rely on the NPM packages persisting across container restarts, ~~and you need to install the LibreOffice package, if 173 | required, in the container image, not on your host OS~~. 174 | 175 | Here's a checklist of changes that are required to make the node work on Docker deployments: 176 | 177 | - [ ] Ensure that the `/home/node/.n8n` directory (on the container) is mapped to a volume. If using plain Docker, use 178 | `-v host_dir_or_vol_name:/home/node/.n8n` as part of the `docker run` command (note that 179 | N8N's [Docker quickstart](https://docs.n8n.io/hosting/installation/docker/#starting-n8n) already does this, and maps 180 | that folder to `~/.n8n` on your host device). If using Docker Compose, ensure that you have an entry under the 181 | `volumes` array that has `- host_dir_or_vol_name:/home/node/.n8n` as its value 182 | - [ ] Install the node as normal (i.e. by going to the `Settings>Community Nodes` page and searching for 183 | `n8n-nodes-carbonejs`). Ensure that the node appears in the host-mapped volume, under the `node_modules` directory. 184 | - [ ] ~~If you wish to use the Convert to PDF action, you'll additionally need to add the LibreOffice system library to 185 | the Docker image and recompile it.~~ 186 | - [ ] ~~Create a `Dockerfile` with the following contents:~~ 187 | ```Dockerfile 188 | FROM n8nio/n8n:latest 189 | 190 | RUN apk --update --no-cache --purge add libreoffice-common 191 | ``` 192 | - [ ] ~~If using plain Docker, run `docker build . -t n8nio/n8n:latest-libreoffice`~~ 193 | - [ ] ~~From there on, run N8N as `docker run <...> n8nio/n8n:latest-libreoffice`~~ 194 | - [ ] ~~If using Docker Compose, change the service declaration:~~ 195 | ```yml 196 | services: 197 | n8n: 198 | image: n8nio/n8n:latest-libreoffice # Declare a new tag 199 | build: . # Add this line 200 | ``` 201 | - [ ] ~~From there on, run `docker-compose build` before running `docker-compose up`, or use `docker-compose up -b`~~ 202 | - [ ] ~~Whenever you need to update the N8N version, remember to run `docker pull n8nio/n8n:latest` first, as 203 | otherwise the build process will use a cached base image~~ 204 | 205 | ### A workaround for converting DOCX files to PDF on Docker 206 | 207 | Since the LibreOffice-Carbone-N8N-Alpine stack is currently not working, I'd recommend using a standalone dedicated 208 | container to do DOCX→PDF conversions, in the spirit of small, dedicated microservices and such. 209 | 210 | A cursory Google search turns up [Gotenberg](https://github.com/gotenberg/gotenberg), "A Docker-powered stateless API 211 | for PDF files." As usual, do your own research, deploying untrusted containers may make your lunch disappear, and all 212 | the usual warnings. 213 | 214 | See [their docs](https://gotenberg.dev/docs/get-started/docker-compose) for Docker Compose information: 215 | 216 | ```yml 217 | services: 218 | # Your other services (i.e. N8N) 219 | 220 | gotenberg: 221 | image: gotenberg/gotenberg:7 222 | ``` 223 | 224 | Then, use [the LibreOffice module](https://gotenberg.dev/docs/modules/libreoffice#route) to convert files: 225 | 226 | 1. Create a [HTTP Request node](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.httprequest/) in the 227 | N8N workflow 228 | 1. Set the method to `POST` 229 | 1. Set the URL to `http://gotenberg:3000/forms/libreoffice/convert` 230 | 1. Enable the `Send Body` toggle 231 | 1. Set the Body Content Type to `Form-Data` 232 | 1. Set the Parameter Type of the first Body parameter to `n8n Binary Data` 233 | 1. Set the Name to `files` (it looks like it could also be something else, but set it to `files` just to match the 234 | Gotenberg docs) 235 | 1. Set the Input Data Field Name to the name of the binary field holding the DOCX file (could be `data`) 236 | 237 | See below for a screenshot of a working configuration: 238 | 239 | ![](./images/gotenberg.png) 240 | 241 | By default, this configuration will override the incoming file with the response's (PDF) file. If you wish to preserve 242 | both files: 243 | 244 | 1. In the HTTP Request node, click the Add Option button at the bottom, then click the Response option 245 | 1. In the new section that appears, set the Response Format to `File` 246 | 1. In the new Put Output in Field textfield that appears, set it to a name that is different to the name of the input 247 | file (e.g., if the input file is in `data`, set it to `data_pdf` or something) 248 | 249 | ![](./images/gotenberg_no_owrite.png) 250 | 251 | ## Development 252 | 253 | More information [here](https://docs.n8n.io/integrations/creating-nodes/test/run-node-locally/). 254 | 255 | You must have a local (non-Docker) installation of N8N. 256 | 257 | 1. Clone this repo 258 | 1. `npm i` 259 | 1. Make changes as required 260 | 1. `npm run build` 261 | 1. `npm link` 262 | 1. Go to N8N's install dir (`~/.n8n/custom/` on Linux), then run `npm link n8n-nodes-carbonejs` 263 | 1. `n8n start`. If you need to start the N8N instance on another port, `N8N_PORT=5679 n8n start` 264 | 1. There's no need to visit the web UI to install the node: it's already installed since it lives in the correct 265 | directory 266 | 1. After making changes in the code and rebuilding, you'll need to stop N8N (Ctrl+C) and restart it (`n8n start`) 267 | 1. For faster changes, instead of rebuilding the code each time, run `npm run dev`. This will start the TypeScript 268 | compiler in watch mode, which will recompile the code on every change. You'll still need to restart N8N manually, 269 | though. 270 | 271 | ### Small-scale release for single person 272 | 273 | Use when someone has reported an issue, to give that person a way to test the fix 274 | without having to release a version that may not fix their issue. Especially useful 275 | when the issue can't be reproduced locally. 276 | 277 | 1. Temporarily edit the `package.json` file to another version, e.g. `v1.2.3-bugfix123` 278 | 2. Run `npm ci && npm run build && npm publish --tag prerelease` 279 | 3. This will build and upload a new release to NPM, _without marking it_ as the latest release! This is important 280 | because otherwise other users of the node will start getting prompts to update their installations, which isn't 281 | correct 282 | 4. Tell the user that may be interested in testing the changes to force-install that new version 283 | 5. If/when the user confirms that the version fixes the issue, release for everyone: [see below](#releasing-changes) 284 | 285 | ### Releasing changes 286 | 287 | - [ ] Bump the version in `package.json`. We use [SemVer](https://semver.org/). 288 | - [ ] Add an entry to the top of `CHANGELOG.md` describing the changes. 289 | - [ ] Push changes, open a PR and merge it to master branch (if developing on another branch) 290 | - [ ] Create a release. This will kick off the CI which will build and publish the package on NPM 291 | --------------------------------------------------------------------------------