├── 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 |
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 | 
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 | 
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 | 
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 | 
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 | 
128 |
129 | B. The document after being rendered, here with the context `{"a": 1}`
130 |
131 | 
132 |
133 | C. The same document, converted to a PDF file
134 |
135 | 
136 |
137 | D. The "cover page"
138 |
139 | 
140 |
141 | E. The document, now with the cover page added. Note that it has two pages, coming from two different documents
142 |
143 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------