@things-factory/integration-headless 7.0.53 → 7.0.55

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/integration-headless",
3
- "version": "7.0.53",
3
+ "version": "7.0.55",
4
4
  "main": "dist-server/index.js",
5
5
  "things-factory": true,
6
6
  "author": "heartyoh <heartyoh@hatiolab.com>",
@@ -22,12 +22,12 @@
22
22
  "clean": "npm run clean:server"
23
23
  },
24
24
  "dependencies": {
25
- "@things-factory/attachment-base": "^7.0.49",
26
- "@things-factory/board-service": "^7.0.49",
27
- "@things-factory/integration-base": "^7.0.49",
28
- "@things-factory/shell": "^7.0.49",
25
+ "@things-factory/attachment-base": "^7.0.55",
26
+ "@things-factory/board-service": "^7.0.55",
27
+ "@things-factory/integration-base": "^7.0.55",
28
+ "@things-factory/shell": "^7.0.55",
29
29
  "ejs": "^3.1.10",
30
30
  "pdf-lib": "^1.17.1"
31
31
  },
32
- "gitHead": "96b4308aff1d312343c647a80823658e78b03824"
32
+ "gitHead": "20eb64e468407b2044e76d9c7019a837281141ad"
33
33
  }
@@ -1,5 +1,4 @@
1
1
  import { TaskRegistry } from '@things-factory/integration-base'
2
-
3
2
  import { PDFCaptureUtil, getCommonParameterSpec } from './pdf-capture-util'
4
3
  import { BoardFunc } from '@things-factory/board-service'
5
4
 
@@ -76,7 +75,7 @@ export async function HeadlessPDFCaptureBoard(step, context) {
76
75
 
77
76
  // PDF 생성 및 추가
78
77
  const htmlContent = '' // 보드에서 직접 렌더링되므로 HTML 컨텐츠가 비어있음
79
- await pdfUtil.processPageAndGeneratePDF({ ...step.params, width, height }, htmlContent)
78
+ await pdfUtil.processPageAndGeneratePDF(pageOptions, htmlContent) // pageOptions 사용
80
79
 
81
80
  return {
82
81
  data: context.__headless_pdf
@@ -1,243 +1,64 @@
1
- import * as ejs from 'ejs'
2
1
  import { PDFDocument } from 'pdf-lib'
3
2
  import { TaskRegistry } from '@things-factory/integration-base'
4
- import { ConnectionManager } from '@things-factory/integration-base'
5
3
  import { access } from '@things-factory/utils'
4
+ import { PDFCaptureUtil, getCommonParameterSpec } from './pdf-capture-util'
6
5
 
7
6
  async function HeadlessPDFOpen(step, context) {
8
- var { connection: connectionName, params: stepOptions } = step
9
- var {
10
- accessor,
11
- coverPage,
12
- lastPage,
13
- header,
14
- footer,
15
- watermark,
16
- fileName,
17
- format,
18
- width,
19
- height,
20
- marginTop,
21
- marginBottom,
22
- marginLeft,
23
- marginRight,
24
- scale,
25
- printBackground,
26
- landscape,
27
- preferCSSPageSize
28
- } = stepOptions || {}
29
- const { data, logger } = context
7
+ const { connection: connectionName, params: stepOptions } = step
8
+ const { accessor, coverPage, lastPage, header, footer, watermark, fileName } = stepOptions || {}
30
9
 
31
- const headlessPool = ConnectionManager.getConnectionInstanceByName(context.domain, connectionName)
32
- let browser
10
+ const { data } = context
33
11
 
34
12
  // Create a new PDF document using pdf-lib
35
13
  const pdfDoc = await PDFDocument.create()
36
14
 
37
- try {
38
- browser = await headlessPool.acquire()
39
- const page = await browser.newPage()
15
+ const pdfInfo = {
16
+ pdfDoc,
17
+ watermark,
18
+ fileName,
19
+ pageCount: 0
20
+ } as any
40
21
 
41
- const templateInput = access(accessor, data)
22
+ context.__headless_pdf = pdfInfo
42
23
 
43
- const renderTemplateSafely = (template, data) => {
44
- try {
45
- return ejs.render(template, data)
46
- } catch (error) {
47
- logger.warn(`Template rendering error: ${error.message}`)
48
- return template
49
- }
50
- }
24
+ const pdfUtil = new PDFCaptureUtil(context)
25
+ await pdfUtil.initBrowser(connectionName)
26
+
27
+ try {
28
+ const templateInput = access(accessor, data)
51
29
 
52
30
  // Convert Cover Page to PDF and add it to the document (if provided)
53
31
  if (coverPage) {
54
- const renderedCoverPage = renderTemplateSafely(coverPage, templateInput)
55
-
56
- await page.setContent(renderedCoverPage)
57
-
58
- // Apply header, footer, and watermark using Puppeteer
59
- // Apply header, footer, and watermark using Puppeteer
60
- if (header || footer || watermark) {
61
- await page.evaluate(
62
- ({ header, footer, watermark, isLandscape }) => {
63
- const setPositioning = (element, position) => {
64
- element.style.position = 'fixed'
65
- element.style.left = '0'
66
- element.style.width = '100%'
67
- element.style.textAlign = 'center'
68
- element.style.fontSize = '12px'
69
- if (position === 'header') {
70
- element.style.top = '0'
71
- } else if (position === 'footer') {
72
- element.style.bottom = '0'
73
- }
74
- }
75
-
76
- // if (header) {
77
- // const headerElement = document.createElement('div')
78
- // headerElement.innerHTML = header
79
- // setPositioning(headerElement, 'header')
80
- // document.body.appendChild(headerElement)
81
- // }
82
-
83
- // if (footer) {
84
- // const footerElement = document.createElement('div')
85
- // footerElement.innerHTML = footer
86
- // setPositioning(footerElement, 'footer')
87
- // document.body.appendChild(footerElement)
88
- // }
89
-
90
- if (watermark) {
91
- const watermarkElement = document.createElement('div')
92
- watermarkElement.innerHTML = watermark
93
- watermarkElement.style.position = 'fixed'
94
- watermarkElement.style.top = isLandscape ? '40%' : '50%'
95
- watermarkElement.style.left = '50%'
96
- watermarkElement.style.transform = 'translate(-50%, -50%) rotate(-45deg)'
97
- watermarkElement.style.opacity = '0.2'
98
- watermarkElement.style.fontSize = '50px'
99
- watermarkElement.style.color = 'red'
100
- watermarkElement.style.pointerEvents = 'none'
101
- watermarkElement.style.zIndex = '9999'
102
- document.body.appendChild(watermarkElement)
103
- }
104
- },
105
- { header, footer, watermark, isLandscape: landscape }
106
- )
107
- }
108
-
109
- const pageOptions = {
110
- format,
111
- width,
112
- height,
113
- margin: {
114
- top: marginTop,
115
- right: marginRight,
116
- bottom: marginBottom,
117
- left: marginLeft
118
- },
119
- scale,
120
- printBackground,
121
- landscape,
122
- displayHeaderFooter: false, // Handled via Puppeteer directly
123
- preferCSSPageSize
124
- }
125
-
126
- const coverPageBuffer = await page.pdf(pageOptions)
127
- const coverPageDoc = await PDFDocument.load(coverPageBuffer)
128
- const coverPages = await pdfDoc.copyPages(coverPageDoc, coverPageDoc.getPageIndices())
129
- coverPages.forEach(page => pdfDoc.addPage(page))
32
+ const renderedCoverPage = pdfUtil.renderTemplate(coverPage, templateInput)
33
+ await pdfUtil.processPageAndGeneratePDF(stepOptions, renderedCoverPage)
130
34
  }
131
35
 
132
36
  var lastPageBuffer
133
37
 
134
38
  // Process Last Page (if provided) but add it later in the save task
135
39
  if (lastPage) {
136
- const renderedLastPage = renderTemplateSafely(lastPage, templateInput)
137
- await page.setContent(renderedLastPage)
138
-
139
- // Apply header, footer, and watermark using Puppeteer
140
- if (header || footer || watermark) {
141
- await page.evaluate(
142
- ({ header, footer, watermark, isLandscape }) => {
143
- const setPositioning = (element, position) => {
144
- element.style.position = 'fixed'
145
- element.style.left = '0'
146
- element.style.width = '100%'
147
- element.style.textAlign = 'center'
148
- element.style.fontSize = '12px'
149
- if (position === 'header') {
150
- element.style.top = '0'
151
- } else if (position === 'footer') {
152
- element.style.bottom = '0'
153
- }
154
- }
155
-
156
- // if (header) {
157
- // const headerElement = document.createElement('div')
158
- // headerElement.innerHTML = header
159
- // setPositioning(headerElement, 'header')
160
- // document.body.appendChild(headerElement)
161
- // }
162
-
163
- // if (footer) {
164
- // const footerElement = document.createElement('div')
165
- // footerElement.innerHTML = footer
166
- // setPositioning(footerElement, 'footer')
167
- // document.body.appendChild(footerElement)
168
- // }
40
+ const renderedLastPage = pdfUtil.renderTemplate(lastPage, templateInput)
41
+ lastPageBuffer = await pdfUtil.generateLastPageBuffer(stepOptions, renderedLastPage)
42
+ }
169
43
 
170
- if (watermark) {
171
- const watermarkElement = document.createElement('div')
172
- watermarkElement.innerHTML = watermark
173
- watermarkElement.style.position = 'fixed'
174
- watermarkElement.style.top = isLandscape ? '40%' : '50%'
175
- watermarkElement.style.left = '50%'
176
- watermarkElement.style.transform = 'translate(-50%, -50%) rotate(-45deg)'
177
- watermarkElement.style.opacity = '0.2'
178
- watermarkElement.style.fontSize = '50px'
179
- watermarkElement.style.color = 'red'
180
- watermarkElement.style.pointerEvents = 'none'
181
- watermarkElement.style.zIndex = '9999'
182
- document.body.appendChild(watermarkElement)
183
- }
184
- },
185
- { header, footer, watermark, isLandscape: landscape }
186
- )
187
- }
44
+ await pdfUtil.closeBrowser()
188
45
 
189
- const pageOptions = {
190
- format,
191
- width,
192
- height,
193
- margin: {
194
- top: marginTop,
195
- right: marginRight,
196
- bottom: marginBottom,
197
- left: marginLeft
198
- },
199
- scale,
200
- printBackground,
201
- landscape,
202
- displayHeaderFooter: false, // Handled via Puppeteer directly
203
- preferCSSPageSize
204
- }
46
+ pdfInfo.lastPageBuffer = lastPageBuffer
47
+ pdfInfo.pageCount = pdfDoc.getPageCount()
48
+ pdfInfo.header = header
49
+ pdfInfo.footer = footer
205
50
 
206
- lastPageBuffer = await page.pdf(pageOptions)
51
+ return {
52
+ data: pdfInfo
207
53
  }
208
-
209
- await page.close()
210
- headlessPool.release(browser)
211
54
  } catch (error) {
212
- if (browser) {
213
- await browser.close()
214
- }
55
+ await pdfUtil.closeBrowser()
215
56
  throw error
216
57
  }
217
-
218
- const pdfInfo = {
219
- pdfDoc,
220
- lastPageBuffer,
221
- header,
222
- footer,
223
- watermark,
224
- fileName,
225
- pageCount: pdfDoc.getPageCount()
226
- }
227
-
228
- context.__headless_pdf = pdfInfo
229
-
230
- return {
231
- data: pdfInfo
232
- }
233
58
  }
234
59
 
235
60
  HeadlessPDFOpen.parameterSpec = [
236
- {
237
- type: 'scenario-step-input',
238
- name: 'accessor',
239
- label: 'accessor'
240
- },
61
+ ...getCommonParameterSpec(),
241
62
  {
242
63
  type: 'textarea',
243
64
  name: 'coverPage',
@@ -269,91 +90,6 @@ HeadlessPDFOpen.parameterSpec = [
269
90
  type: 'string',
270
91
  name: 'fileName',
271
92
  label: 'filename'
272
- },
273
- {
274
- type: 'select',
275
- name: 'format',
276
- label: 'page-format',
277
- property: {
278
- options: [
279
- { display: '', value: '' },
280
- { display: 'A4', value: 'A4' },
281
- { display: 'A3', value: 'A3' },
282
- { display: 'Letter', value: 'Letter' },
283
- { display: 'Legal', value: 'Legal' }
284
- ]
285
- },
286
- description: 'Select the paper format for the PDF'
287
- },
288
- {
289
- type: 'string',
290
- name: 'width',
291
- label: 'page-width',
292
- placeholder: '(e.g., "8.5in", "21cm", "600px")',
293
- description: 'Specify the width of the page (e.g., "8.5in", "21cm", "600px")'
294
- },
295
- {
296
- type: 'string',
297
- name: 'height',
298
- label: 'page-height',
299
- placeholder: '(e.g., "11in", "29.7cm", "800px")',
300
- description: 'Specify the height of the page (e.g., "11in", "29.7cm", "800px")'
301
- },
302
- {
303
- type: 'string',
304
- name: 'marginTop',
305
- label: 'page-margin-top',
306
- placeholder: '(e.g., "0.5in", "1cm", "100px")',
307
- description: 'Set the top margin for the page'
308
- },
309
- {
310
- type: 'string',
311
- name: 'marginBottom',
312
- label: 'page-margin-bottom',
313
- placeholder: '(e.g., "0.5in", "1cm", "100px")',
314
- description: 'Set the bottom margin for the page'
315
- },
316
- {
317
- type: 'string',
318
- name: 'marginLeft',
319
- label: 'page-margin-left',
320
- placeholder: '(e.g., "0.5in", "1cm", "100px")',
321
- description: 'Set the left margin for the page'
322
- },
323
- {
324
- type: 'string',
325
- name: 'marginRight',
326
- label: 'page-margin-right',
327
- placeholder: '(e.g., "0.5in", "1cm", "100px")',
328
- description: 'Set the right margin for the page'
329
- },
330
- {
331
- type: 'number',
332
- name: 'scale',
333
- label: 'page-scale',
334
- defaultValue: 1,
335
- description: 'Set the scale of the page content (default is 1)'
336
- },
337
- {
338
- type: 'boolean',
339
- name: 'printBackground',
340
- label: 'print-background',
341
- defaultValue: true,
342
- description: 'Include background graphics when printing the page'
343
- },
344
- {
345
- type: 'boolean',
346
- name: 'landscape',
347
- label: 'landscape',
348
- defaultValue: false,
349
- description: 'Print the PDF in landscape orientation'
350
- },
351
- {
352
- type: 'boolean',
353
- name: 'preferCSSPageSize',
354
- label: 'prefer-css-page-size',
355
- defaultValue: false,
356
- description: 'Whether to prefer the CSS-defined page size over the given width and height'
357
93
  }
358
94
  ]
359
95
 
@@ -1,12 +1,11 @@
1
1
  import { PDFDocument } from 'pdf-lib'
2
- const { Readable } = require('stream')
3
-
2
+ import { Readable } from 'stream'
4
3
  import { TaskRegistry } from '@things-factory/integration-base'
5
4
  import { Attachment, createAttachment } from '@things-factory/attachment-base'
6
5
  import { getRepository } from '@things-factory/shell'
7
6
 
8
7
  async function HeadlessPDFSave(step, context) {
9
- var { domain, user, __headless_pdf } = context
8
+ const { domain, user, __headless_pdf } = context
10
9
 
11
10
  if (!__headless_pdf || !__headless_pdf.pdfDoc) {
12
11
  throw new Error(
@@ -18,16 +17,39 @@ async function HeadlessPDFSave(step, context) {
18
17
 
19
18
  // Add last page if it exists
20
19
  if (__headless_pdf.lastPageBuffer) {
21
- const lastPageDoc = await PDFDocument.load(__headless_pdf.lastPageBuffer)
22
- const copiedLastPages = await pdfDoc.copyPages(lastPageDoc, lastPageDoc.getPageIndices())
23
- copiedLastPages.forEach(page => pdfDoc.addPage(page))
20
+ await appendLastPageToPDF(pdfDoc, __headless_pdf.lastPageBuffer)
24
21
  }
25
22
 
26
23
  const pdfBytes = await pdfDoc.save()
24
+ const finalFileName = generateFileName(__headless_pdf.fileName)
25
+
26
+ const savedAttachment = await savePDFToFile(pdfBytes, finalFileName, domain, user)
27
+ const attachment = await getAttachmentDetails(savedAttachment.id)
27
28
 
28
- const finalFileName = __headless_pdf.fileName || `document-${Date.now()}.pdf`
29
+ return {
30
+ data: {
31
+ id: attachment.id,
32
+ name: attachment.name,
33
+ path: attachment.path,
34
+ mimetype: attachment.mimetype,
35
+ refBy: attachment.refBy,
36
+ fullpath: attachment.fullpath
37
+ }
38
+ }
39
+ }
40
+
41
+ async function appendLastPageToPDF(pdfDoc, lastPageBuffer) {
42
+ const lastPageDoc = await PDFDocument.load(lastPageBuffer)
43
+ const copiedLastPages = await pdfDoc.copyPages(lastPageDoc, lastPageDoc.getPageIndices())
44
+ copiedLastPages.forEach(page => pdfDoc.addPage(page))
45
+ }
29
46
 
30
- const saved = await createAttachment(
47
+ function generateFileName(fileName) {
48
+ return fileName || `document-${Date.now()}.pdf`
49
+ }
50
+
51
+ async function savePDFToFile(pdfBytes, finalFileName, domain, user) {
52
+ return await createAttachment(
31
53
  null,
32
54
  {
33
55
  attachment: {
@@ -51,21 +73,12 @@ async function HeadlessPDFSave(step, context) {
51
73
  state: { domain, user }
52
74
  }
53
75
  )
76
+ }
54
77
 
55
- const attachment = await getRepository(Attachment).findOne({
56
- where: { id: saved.id }
78
+ async function getAttachmentDetails(attachmentId) {
79
+ return await getRepository(Attachment).findOne({
80
+ where: { id: attachmentId }
57
81
  })
58
-
59
- return {
60
- data: {
61
- id: attachment.id,
62
- name: attachment.name,
63
- path: attachment.path,
64
- mimetype: attachment.mimetype,
65
- refBy: attachment.refBy,
66
- fullpath: attachment.fullpath
67
- }
68
- }
69
82
  }
70
83
 
71
84
  HeadlessPDFSave.parameterSpec = []
@@ -58,8 +58,7 @@ export class PDFCaptureUtil {
58
58
  }
59
59
  }
60
60
 
61
- async generatePDFContent(page: Page, htmlContent: string, pageOptions: any): Promise<PDFDocument> {
62
- await page.setContent(htmlContent)
61
+ async generatePDFContent(page: Page, pageOptions: any): Promise<PDFDocument> {
63
62
  const contentBuffer = await page.pdf(pageOptions)
64
63
  const contentPdfDoc = await PDFDocument.load(contentBuffer)
65
64
  return contentPdfDoc
@@ -76,15 +75,19 @@ export class PDFCaptureUtil {
76
75
 
77
76
  const page = await this.browser.newPage()
78
77
 
78
+ // 페이지 컨텐츠 설정
79
+ await page.setContent(htmlContent, { waitUntil: 'networkidle0' }) // HTML 컨텐츠가 완전히 로드될 때까지 대기
80
+
81
+ // 페이지가 완전히 로드된 후에 워터마크와 머릿글 적용
79
82
  const { __headless_pdf } = this.context
80
83
  const { header, footer, watermark, landscape } = __headless_pdf
81
84
 
82
85
  if (header || footer || watermark) {
83
- await this.applyHeaderFooterWatermark(page, { header, footer, watermark, landscape })
86
+ await this.applyHeaderFooterWatermark(page, { header, footer, watermark, landscape, data: __headless_pdf })
84
87
  }
85
88
 
86
89
  const pageOptions = this.buildPageOptions(stepOptions)
87
- const contentPdfDoc = await this.generatePDFContent(page, htmlContent, pageOptions)
90
+ const contentPdfDoc = await this.generatePDFContent(page, pageOptions)
88
91
  await this.appendPDFContentToDocument(__headless_pdf.pdfDoc, contentPdfDoc)
89
92
  await page.close()
90
93
  }
@@ -127,10 +130,16 @@ export class PDFCaptureUtil {
127
130
 
128
131
  async applyHeaderFooterWatermark(
129
132
  page: Page,
130
- { header, footer, watermark, landscape }: { header: string; footer: string; watermark: string; landscape: boolean }
133
+ {
134
+ header,
135
+ footer,
136
+ watermark,
137
+ landscape,
138
+ data
139
+ }: { header: string; footer: string; watermark: string; landscape: boolean; data: any }
131
140
  ) {
132
141
  await page.evaluate(
133
- ({ header, footer, watermark, landscape }) => {
142
+ ({ header, footer, watermark, landscape, data }) => {
134
143
  const setPositioning = (element: HTMLElement, position: 'header' | 'footer') => {
135
144
  element.style.position = 'fixed'
136
145
  element.style.left = '0'
@@ -139,6 +148,8 @@ export class PDFCaptureUtil {
139
148
  element.style.fontSize = '12px'
140
149
  element.style.margin = '0'
141
150
  element.style.padding = '0'
151
+ element.style.zIndex = '1000'
152
+
142
153
  if (position === 'header') {
143
154
  element.style.top = '0'
144
155
  } else if (position === 'footer') {
@@ -175,9 +186,49 @@ export class PDFCaptureUtil {
175
186
  document.body.appendChild(watermarkElement)
176
187
  }
177
188
  },
178
- { header, footer, watermark, landscape }
189
+ {
190
+ header: header && this.renderTemplate(header, data),
191
+ footer: footer && this.renderTemplate(footer, data),
192
+ watermark: watermark && this.renderTemplate(watermark, data),
193
+ landscape,
194
+ data
195
+ }
179
196
  )
180
197
  }
198
+
199
+ /**
200
+ * Generates a PDF buffer for the last page.
201
+ * @param stepOptions - Options used for generating the PDF.
202
+ * @param htmlContent - HTML content to be converted to PDF.
203
+ * @returns {Promise<Uint8Array>} - The generated PDF as a buffer.
204
+ */
205
+ async generateLastPageBuffer(stepOptions: StepOptions, htmlContent: string): Promise<Uint8Array> {
206
+ if (!this.browser) throw new Error('Browser not initialized. Call initBrowser() first.')
207
+
208
+ const page = await this.browser.newPage()
209
+
210
+ // 페이지에 HTML 콘텐츠를 설정합니다.
211
+ await page.setContent(htmlContent, { waitUntil: 'networkidle0' })
212
+
213
+ // 페이지가 완전히 로드된 후에 워터마크와 머릿글 적용
214
+ const { __headless_pdf } = this.context
215
+ const { header, footer, watermark, landscape } = __headless_pdf
216
+
217
+ if (header || footer || watermark) {
218
+ await this.applyHeaderFooterWatermark(page, { header, footer, watermark, landscape, data: __headless_pdf })
219
+ }
220
+
221
+ // 페이지 옵션 설정
222
+ const pageOptions = this.buildPageOptions(stepOptions)
223
+
224
+ // PDF를 생성하고 버퍼를 반환합니다.
225
+ const lastPageBuffer = await page.pdf(pageOptions)
226
+
227
+ // 페이지 닫기
228
+ await page.close()
229
+
230
+ return lastPageBuffer
231
+ }
181
232
  }
182
233
 
183
234
  export function getCommonParameterSpec() {