@things-factory/integration-headless 7.0.53 → 7.0.54

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.54",
4
4
  "main": "dist-server/index.js",
5
5
  "things-factory": true,
6
6
  "author": "heartyoh <heartyoh@hatiolab.com>",
@@ -29,5 +29,5 @@
29
29
  "ejs": "^3.1.10",
30
30
  "pdf-lib": "^1.17.1"
31
31
  },
32
- "gitHead": "96b4308aff1d312343c647a80823658e78b03824"
32
+ "gitHead": "14a3320fb61bd0a47c147d4c3ff8310de6172f61"
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,
7
+ const { connection: connectionName, params: stepOptions } = step
8
+ const { accessor, coverPage, lastPage, header, footer, watermark, fileName } = stepOptions || {}
9
+
10
+ const { data } = context
11
+
12
+ // Create a new PDF document using pdf-lib
13
+ const pdfDoc = await PDFDocument.create()
14
+
15
+ const pdfInfo = {
16
+ pdfDoc,
13
17
  header,
14
18
  footer,
15
19
  watermark,
16
20
  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
21
+ pageCount: 0
22
+ } as any
30
23
 
31
- const headlessPool = ConnectionManager.getConnectionInstanceByName(context.domain, connectionName)
32
- let browser
24
+ context.__headless_pdf = pdfInfo
33
25
 
34
- // Create a new PDF document using pdf-lib
35
- const pdfDoc = await PDFDocument.create()
26
+ const pdfUtil = new PDFCaptureUtil(context)
27
+ await pdfUtil.initBrowser(connectionName)
36
28
 
37
29
  try {
38
- browser = await headlessPool.acquire()
39
- const page = await browser.newPage()
40
-
41
30
  const templateInput = access(accessor, data)
42
31
 
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
- }
51
-
52
32
  // Convert Cover Page to PDF and add it to the document (if provided)
53
33
  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))
34
+ const renderedCoverPage = pdfUtil.renderTemplate(coverPage, templateInput)
35
+ await pdfUtil.processPageAndGeneratePDF(stepOptions, renderedCoverPage)
130
36
  }
131
37
 
132
38
  var lastPageBuffer
133
39
 
134
40
  // Process Last Page (if provided) but add it later in the save task
135
41
  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
- // }
42
+ const renderedLastPage = pdfUtil.renderTemplate(lastPage, templateInput)
43
+ lastPageBuffer = await pdfUtil.generateLastPageBuffer(stepOptions, renderedLastPage)
44
+ }
169
45
 
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
- }
46
+ await pdfUtil.closeBrowser()
188
47
 
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
- }
48
+ pdfInfo.lastPageBuffer = lastPageBuffer
49
+ pdfInfo.pageCount = pdfDoc.getPageCount()
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,41 @@ 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 pageOptions = this.buildPageOptions(stepOptions)
215
+
216
+ // PDF를 생성하고 버퍼를 반환합니다.
217
+ const lastPageBuffer = await page.pdf(pageOptions)
218
+
219
+ // 페이지 닫기
220
+ await page.close()
221
+
222
+ return lastPageBuffer
223
+ }
181
224
  }
182
225
 
183
226
  export function getCommonParameterSpec() {