@things-factory/integration-headless 7.0.49 → 7.0.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,250 +1,36 @@
1
- import * as ejs from 'ejs'
2
1
  import { TaskRegistry } from '@things-factory/integration-base'
3
- import { ConnectionManager } from '@things-factory/integration-base'
4
2
  import { access } from '@things-factory/utils'
5
- import { PDFDocument } from 'pdf-lib'
6
3
 
7
- async function HeadlessPDFCapture(step, context) {
8
- var { connection: connectionName, params: stepOptions } = step
9
- var {
10
- accessor,
11
- htmlContent,
12
- format,
13
- width,
14
- height,
15
- marginTop,
16
- marginBottom,
17
- marginLeft,
18
- marginRight,
19
- scale,
20
- printBackground,
21
- landscape,
22
- preferCSSPageSize
23
- } = stepOptions || {}
24
- var { domain, data, logger, __headless_pdf } = context
4
+ import { PDFCaptureUtil, getCommonParameterSpec } from './pdf-capture-util'
25
5
 
26
- if (!__headless_pdf) {
27
- throw new Error('It requires headless-pdf-open to be performed first')
28
- }
29
-
30
- var { pdfDoc, header, footer, watermark } = __headless_pdf
31
-
32
- var headlessPool = ConnectionManager.getConnectionInstanceByName(domain, connectionName)
33
- let browser
6
+ export async function HeadlessPDFCapture(step, context) {
7
+ const pdfUtil = new PDFCaptureUtil(context)
8
+ await pdfUtil.initBrowser(step.connection)
34
9
 
35
10
  try {
36
- browser = await headlessPool.acquire()
37
- const page = await browser.newPage()
38
-
39
- const templateInput = access(accessor, data)
40
-
41
- const renderTemplateSafely = (template, data) => {
42
- try {
43
- return ejs.render(template, data)
44
- } catch (error) {
45
- logger.warn(`Template rendering error: ${error.message}`)
46
- return template
47
- }
48
- }
49
-
50
- const renderedHtmlContent = renderTemplateSafely(htmlContent, templateInput)
51
- await page.setContent(renderedHtmlContent)
52
-
53
- header = renderTemplateSafely(header, __headless_pdf)
54
- footer = renderTemplateSafely(footer, __headless_pdf)
55
-
56
- // Apply header, footer, and watermark using Puppeteer
57
- if (header || footer || watermark) {
58
- await page.evaluate(
59
- ({ header, footer, watermark, isLandscape }) => {
60
- const setPositioning = (element, position) => {
61
- element.style.position = 'fixed'
62
- element.style.left = '0'
63
- element.style.width = '100%'
64
- element.style.textAlign = 'center'
65
- element.style.fontSize = '12px'
66
- element.style.margin = '0'
67
- element.style.padding = '0'
68
- if (position === 'header') {
69
- element.style.top = '0'
70
- } else if (position === 'footer') {
71
- element.style.bottom = '0'
72
- }
73
- }
74
-
75
- if (header) {
76
- const headerElement = document.createElement('div')
77
- headerElement.innerHTML = header
78
- setPositioning(headerElement, 'header')
79
- document.body.appendChild(headerElement)
80
- }
81
-
82
- if (footer) {
83
- const footerElement = document.createElement('div')
84
- footerElement.innerHTML = footer
85
- setPositioning(footerElement, 'footer')
86
- document.body.appendChild(footerElement)
87
- }
88
-
89
- if (watermark) {
90
- const watermarkElement = document.createElement('div')
91
- watermarkElement.innerHTML = watermark
92
- watermarkElement.style.position = 'fixed'
93
- watermarkElement.style.top = isLandscape ? '40%' : '50%'
94
- watermarkElement.style.left = '50%'
95
- watermarkElement.style.transform = 'translate(-50%, -50%) rotate(-45deg)'
96
- watermarkElement.style.opacity = '0.2'
97
- watermarkElement.style.fontSize = '50px'
98
- watermarkElement.style.color = 'red'
99
- watermarkElement.style.pointerEvents = 'none'
100
- watermarkElement.style.zIndex = '9999'
101
- document.body.appendChild(watermarkElement)
102
- }
103
- },
104
- { header, footer, watermark, isLandscape: landscape }
105
- )
106
- }
107
-
108
- if (preferCSSPageSize) {
109
- width = undefined
110
- height = undefined
111
- }
112
-
113
- const pageOptions = {
114
- format,
115
- width,
116
- height,
117
- margin: {
118
- top: marginTop,
119
- right: marginRight,
120
- bottom: marginBottom,
121
- left: marginLeft
122
- },
123
- scale,
124
- printBackground,
125
- landscape,
126
- displayHeaderFooter: false, // Handled via Puppeteer directly
127
- preferCSSPageSize
128
- }
11
+ const { accessor, htmlContent } = step.params
12
+ const templateInput = access(accessor, context.data)
129
13
 
130
- const contentBuffer = await page.pdf(pageOptions)
131
- const contentPdfDoc = await PDFDocument.load(contentBuffer)
132
- const copiedPages = await pdfDoc.copyPages(contentPdfDoc, contentPdfDoc.getPageIndices())
14
+ const renderedHtmlContent = pdfUtil.renderTemplate(htmlContent, templateInput)
133
15
 
134
- copiedPages.forEach(page => pdfDoc.addPage(page))
135
-
136
- __headless_pdf.pageCount += copiedPages.length
137
-
138
- await page.close()
139
- headlessPool.release(browser)
16
+ await pdfUtil.processPageAndGeneratePDF(step.params, renderedHtmlContent)
140
17
 
141
18
  return {
142
- data: __headless_pdf
19
+ data: context.__headless_pdf
143
20
  }
144
21
  } catch (error) {
145
- if (browser) {
146
- await browser.close()
147
- }
148
-
149
22
  throw error
23
+ } finally {
24
+ await pdfUtil.closeBrowser()
150
25
  }
151
26
  }
152
27
 
153
28
  HeadlessPDFCapture.parameterSpec = [
154
- {
155
- type: 'scenario-step-input',
156
- name: 'accessor',
157
- label: 'accessor'
158
- },
29
+ ...getCommonParameterSpec(),
159
30
  {
160
31
  type: 'textarea',
161
32
  name: 'htmlContent',
162
33
  label: 'html-content'
163
- },
164
- {
165
- type: 'select',
166
- name: 'format',
167
- label: 'page-format',
168
- property: {
169
- options: [
170
- { display: '', value: '' },
171
- { display: 'A4', value: 'A4' },
172
- { display: 'A3', value: 'A3' },
173
- { display: 'Letter', value: 'Letter' },
174
- { display: 'Legal', value: 'Legal' }
175
- ]
176
- },
177
- description: 'Select the paper format for the PDF'
178
- },
179
- {
180
- type: 'string',
181
- name: 'width',
182
- label: 'page-width',
183
- placeholder: '(e.g., "8.5in", "21cm", "600px")',
184
- description: 'Specify the width of the page (e.g., "8.5in", "21cm", "600px")'
185
- },
186
- {
187
- type: 'string',
188
- name: 'height',
189
- label: 'page-height',
190
- placeholder: '(e.g., "11in", "29.7cm", "800px")',
191
- description: 'Specify the height of the page (e.g., "11in", "29.7cm", "800px")'
192
- },
193
- {
194
- type: 'string',
195
- name: 'marginTop',
196
- label: 'page-margin-top',
197
- placeholder: '(e.g., "0.5in", "1cm", "100px")',
198
- description: 'Set the top margin for the page'
199
- },
200
- {
201
- type: 'string',
202
- name: 'marginBottom',
203
- label: 'page-margin-bottom',
204
- placeholder: '(e.g., "0.5in", "1cm", "100px")',
205
- description: 'Set the bottom margin for the page'
206
- },
207
- {
208
- type: 'string',
209
- name: 'marginLeft',
210
- label: 'page-margin-left',
211
- placeholder: '(e.g., "0.5in", "1cm", "100px")',
212
- description: 'Set the left margin for the page'
213
- },
214
- {
215
- type: 'string',
216
- name: 'marginRight',
217
- label: 'page-margin-right',
218
- placeholder: '(e.g., "0.5in", "1cm", "100px")',
219
- description: 'Set the right margin for the page'
220
- },
221
- {
222
- type: 'number',
223
- name: 'scale',
224
- label: 'page-scale',
225
- defaultValue: 1,
226
- description: 'Set the scale of the page content (default is 1)'
227
- },
228
- {
229
- type: 'boolean',
230
- name: 'printBackground',
231
- label: 'print-background',
232
- defaultValue: true,
233
- description: 'Include background graphics when printing the page'
234
- },
235
- {
236
- type: 'boolean',
237
- name: 'landscape',
238
- label: 'landscape',
239
- defaultValue: false,
240
- description: 'Print the PDF in landscape orientation'
241
- },
242
- {
243
- type: 'boolean',
244
- name: 'preferCSSPageSize',
245
- label: 'prefer-css-page-size',
246
- defaultValue: false,
247
- description: 'Whether to prefer the CSS-defined page size over the given width and height'
248
34
  }
249
35
  ]
250
36
 
@@ -1,4 +1,5 @@
1
1
  import './headless-pdf-capture'
2
+ import './headless-pdf-capture-markdown'
2
3
  import './headless-pdf-capture-board'
3
4
  import './headless-pdf-open'
4
5
  import './headless-pdf-save'
@@ -0,0 +1,276 @@
1
+ import * as ejs from 'ejs'
2
+ import { ConnectionManager } from '@things-factory/integration-base'
3
+ import { PDFDocument } from 'pdf-lib'
4
+ import { Browser, Page } from 'puppeteer'
5
+ import { Logger } from 'winston'
6
+
7
+ interface StepOptions {
8
+ format?: string
9
+ width?: string
10
+ height?: string
11
+ marginTop?: string
12
+ marginBottom?: string
13
+ marginLeft?: string
14
+ marginRight?: string
15
+ scale?: number
16
+ printBackground?: boolean
17
+ landscape?: boolean
18
+ preferCSSPageSize?: boolean
19
+ }
20
+
21
+ interface PDFContext {
22
+ domain: any
23
+ data: any
24
+ logger: Logger
25
+ __headless_pdf: any
26
+ }
27
+
28
+ export class PDFCaptureUtil {
29
+ private context: PDFContext
30
+ private headlessPool: any
31
+
32
+ public browser: Browser | null = null
33
+
34
+ constructor(context: PDFContext) {
35
+ this.context = context
36
+ }
37
+
38
+ async initBrowser(connectionName: string) {
39
+ const { domain } = this.context
40
+ this.headlessPool = ConnectionManager.getConnectionInstanceByName(domain, connectionName)
41
+ this.browser = await this.headlessPool.acquire()
42
+ }
43
+
44
+ async closeBrowser() {
45
+ if (this.browser) {
46
+ await this.browser.close()
47
+ this.headlessPool.release(this.browser)
48
+ }
49
+ }
50
+
51
+ renderTemplate(template: string, data: any): string {
52
+ const { logger } = this.context
53
+ try {
54
+ return ejs.render(template, data)
55
+ } catch (error) {
56
+ logger.warn(`Template rendering error: ${error.message}`)
57
+ return template
58
+ }
59
+ }
60
+
61
+ async generatePDFContent(page: Page, htmlContent: string, pageOptions: any): Promise<PDFDocument> {
62
+ await page.setContent(htmlContent)
63
+ const contentBuffer = await page.pdf(pageOptions)
64
+ const contentPdfDoc = await PDFDocument.load(contentBuffer)
65
+ return contentPdfDoc
66
+ }
67
+
68
+ async appendPDFContentToDocument(pdfDoc: PDFDocument, contentPdfDoc: PDFDocument) {
69
+ const copiedPages = await pdfDoc.copyPages(contentPdfDoc, contentPdfDoc.getPageIndices())
70
+ copiedPages.forEach(page => pdfDoc.addPage(page))
71
+ this.context.__headless_pdf.pageCount += copiedPages.length
72
+ }
73
+
74
+ async processPageAndGeneratePDF(stepOptions: StepOptions, htmlContent: string) {
75
+ if (!this.browser) throw new Error('Browser not initialized. Call initBrowser() first.')
76
+
77
+ const page = await this.browser.newPage()
78
+
79
+ const { __headless_pdf } = this.context
80
+ const { header, footer, watermark, landscape } = __headless_pdf
81
+
82
+ if (header || footer || watermark) {
83
+ await this.applyHeaderFooterWatermark(page, { header, footer, watermark, landscape })
84
+ }
85
+
86
+ const pageOptions = this.buildPageOptions(stepOptions)
87
+ const contentPdfDoc = await this.generatePDFContent(page, htmlContent, pageOptions)
88
+ await this.appendPDFContentToDocument(__headless_pdf.pdfDoc, contentPdfDoc)
89
+ await page.close()
90
+ }
91
+
92
+ buildPageOptions({
93
+ format,
94
+ width,
95
+ height,
96
+ marginTop,
97
+ marginBottom,
98
+ marginLeft,
99
+ marginRight,
100
+ scale,
101
+ printBackground,
102
+ landscape,
103
+ preferCSSPageSize
104
+ }: StepOptions): any {
105
+ if (preferCSSPageSize) {
106
+ width = undefined
107
+ height = undefined
108
+ }
109
+
110
+ return {
111
+ format,
112
+ width,
113
+ height,
114
+ margin: {
115
+ top: marginTop,
116
+ right: marginRight,
117
+ bottom: marginBottom,
118
+ left: marginLeft
119
+ },
120
+ scale,
121
+ printBackground,
122
+ landscape,
123
+ displayHeaderFooter: false,
124
+ preferCSSPageSize
125
+ }
126
+ }
127
+
128
+ async applyHeaderFooterWatermark(
129
+ page: Page,
130
+ { header, footer, watermark, landscape }: { header: string; footer: string; watermark: string; landscape: boolean }
131
+ ) {
132
+ await page.evaluate(
133
+ ({ header, footer, watermark, landscape }) => {
134
+ const setPositioning = (element: HTMLElement, position: 'header' | 'footer') => {
135
+ element.style.position = 'fixed'
136
+ element.style.left = '0'
137
+ element.style.width = '100%'
138
+ element.style.textAlign = 'center'
139
+ element.style.fontSize = '12px'
140
+ element.style.margin = '0'
141
+ element.style.padding = '0'
142
+ if (position === 'header') {
143
+ element.style.top = '0'
144
+ } else if (position === 'footer') {
145
+ element.style.bottom = '0'
146
+ }
147
+ }
148
+
149
+ if (header) {
150
+ const headerElement = document.createElement('div')
151
+ headerElement.innerHTML = header
152
+ setPositioning(headerElement, 'header')
153
+ document.body.appendChild(headerElement)
154
+ }
155
+
156
+ if (footer) {
157
+ const footerElement = document.createElement('div')
158
+ footerElement.innerHTML = footer
159
+ setPositioning(footerElement, 'footer')
160
+ document.body.appendChild(footerElement)
161
+ }
162
+
163
+ if (watermark) {
164
+ const watermarkElement = document.createElement('div')
165
+ watermarkElement.innerHTML = watermark
166
+ watermarkElement.style.position = 'fixed'
167
+ watermarkElement.style.top = landscape ? '40%' : '50%'
168
+ watermarkElement.style.left = '50%'
169
+ watermarkElement.style.transform = 'translate(-50%, -50%) rotate(-45deg)'
170
+ watermarkElement.style.opacity = '0.2'
171
+ watermarkElement.style.fontSize = '50px'
172
+ watermarkElement.style.color = 'red'
173
+ watermarkElement.style.pointerEvents = 'none'
174
+ watermarkElement.style.zIndex = '9999'
175
+ document.body.appendChild(watermarkElement)
176
+ }
177
+ },
178
+ { header, footer, watermark, landscape }
179
+ )
180
+ }
181
+ }
182
+
183
+ export function getCommonParameterSpec() {
184
+ return [
185
+ {
186
+ type: 'scenario-step-input',
187
+ name: 'accessor',
188
+ label: 'accessor'
189
+ },
190
+ {
191
+ type: 'select',
192
+ name: 'format',
193
+ label: 'page-format',
194
+ property: {
195
+ options: [
196
+ { display: '', value: '' },
197
+ { display: 'A4', value: 'A4' },
198
+ { display: 'A3', value: 'A3' },
199
+ { display: 'Letter', value: 'Letter' },
200
+ { display: 'Legal', value: 'Legal' }
201
+ ]
202
+ },
203
+ description: 'Select the paper format for the PDF'
204
+ },
205
+ {
206
+ type: 'string',
207
+ name: 'width',
208
+ label: 'page-width',
209
+ placeholder: '(e.g., "8.5in", "21cm", "600px")',
210
+ description: 'Specify the width of the page (e.g., "8.5in", "21cm", "600px")'
211
+ },
212
+ {
213
+ type: 'string',
214
+ name: 'height',
215
+ label: 'page-height',
216
+ placeholder: '(e.g., "11in", "29.7cm", "800px")',
217
+ description: 'Specify the height of the page (e.g., "11in", "29.7cm", "800px")'
218
+ },
219
+ {
220
+ type: 'string',
221
+ name: 'marginTop',
222
+ label: 'page-margin-top',
223
+ placeholder: '(e.g., "0.5in", "1cm", "100px")',
224
+ description: 'Set the top margin for the page'
225
+ },
226
+ {
227
+ type: 'string',
228
+ name: 'marginBottom',
229
+ label: 'page-margin-bottom',
230
+ placeholder: '(e.g., "0.5in", "1cm", "100px")',
231
+ description: 'Set the bottom margin for the page'
232
+ },
233
+ {
234
+ type: 'string',
235
+ name: 'marginLeft',
236
+ label: 'page-margin-left',
237
+ placeholder: '(e.g., "0.5in", "1cm", "100px")',
238
+ description: 'Set the left margin for the page'
239
+ },
240
+ {
241
+ type: 'string',
242
+ name: 'marginRight',
243
+ label: 'page-margin-right',
244
+ placeholder: '(e.g., "0.5in", "1cm", "100px")',
245
+ description: 'Set the right margin for the page'
246
+ },
247
+ {
248
+ type: 'number',
249
+ name: 'scale',
250
+ label: 'page-scale',
251
+ defaultValue: 1,
252
+ description: 'Set the scale of the page content (default is 1)'
253
+ },
254
+ {
255
+ type: 'boolean',
256
+ name: 'printBackground',
257
+ label: 'print-background',
258
+ defaultValue: true,
259
+ description: 'Include background graphics when printing the page'
260
+ },
261
+ {
262
+ type: 'boolean',
263
+ name: 'landscape',
264
+ label: 'landscape',
265
+ defaultValue: false,
266
+ description: 'Print the PDF in landscape orientation'
267
+ },
268
+ {
269
+ type: 'boolean',
270
+ name: 'preferCSSPageSize',
271
+ label: 'prefer-css-page-size',
272
+ defaultValue: false,
273
+ description: 'Whether to prefer the CSS-defined page size over the given width and height'
274
+ }
275
+ ]
276
+ }