@things-factory/integration-headless 7.0.39 → 7.0.41

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.
Files changed (50) hide show
  1. package/dist-server/engine/connector/headless-connector.d.ts +1 -0
  2. package/dist-server/engine/connector/headless-connector.js +3 -0
  3. package/dist-server/engine/connector/headless-connector.js.map +1 -1
  4. package/dist-server/engine/task/headless-pdf-capture-board.d.ts +1 -0
  5. package/dist-server/engine/task/headless-pdf-capture-board.js +311 -0
  6. package/dist-server/engine/task/headless-pdf-capture-board.js.map +1 -0
  7. package/dist-server/engine/task/headless-pdf-capture.js +78 -19
  8. package/dist-server/engine/task/headless-pdf-capture.js.map +1 -1
  9. package/dist-server/engine/task/headless-pdf-open.js +246 -18
  10. package/dist-server/engine/task/headless-pdf-open.js.map +1 -1
  11. package/dist-server/engine/task/headless-pdf-save.js +43 -110
  12. package/dist-server/engine/task/headless-pdf-save.js.map +1 -1
  13. package/dist-server/engine/task/index.d.ts +1 -0
  14. package/dist-server/engine/task/index.js +1 -0
  15. package/dist-server/engine/task/index.js.map +1 -1
  16. package/dist-server/tsconfig.tsbuildinfo +1 -1
  17. package/helps/integration/connector/headless-connector.ja.md +71 -0
  18. package/helps/integration/connector/headless-connector.ko.md +72 -0
  19. package/helps/integration/connector/headless-connector.md +71 -0
  20. package/helps/integration/connector/headless-connector.ms.md +73 -0
  21. package/helps/integration/connector/headless-connector.zh.md +71 -0
  22. package/helps/integration/task/headless-pdf-capture-board.ja.md +51 -0
  23. package/helps/integration/task/headless-pdf-capture-board.ko.md +51 -0
  24. package/helps/integration/task/headless-pdf-capture-board.md +51 -0
  25. package/helps/integration/task/headless-pdf-capture-board.ms.md +53 -0
  26. package/helps/integration/task/headless-pdf-capture-board.zh.md +51 -0
  27. package/helps/integration/task/headless-pdf-capture.ja.md +45 -0
  28. package/helps/integration/task/headless-pdf-capture.ko.md +45 -0
  29. package/helps/integration/task/headless-pdf-capture.md +45 -0
  30. package/helps/integration/task/headless-pdf-capture.ms.md +47 -0
  31. package/helps/integration/task/headless-pdf-capture.zh.md +45 -0
  32. package/helps/integration/task/headless-pdf-open.ja.md +55 -0
  33. package/helps/integration/task/headless-pdf-open.ko.md +55 -0
  34. package/helps/integration/task/headless-pdf-open.md +55 -0
  35. package/helps/integration/task/headless-pdf-open.ms.md +57 -0
  36. package/helps/integration/task/headless-pdf-open.zh.md +55 -0
  37. package/helps/integration/task/headless-pdf-save.ja.md +23 -0
  38. package/helps/integration/task/headless-pdf-save.ko.md +25 -0
  39. package/helps/integration/task/headless-pdf-save.md +23 -0
  40. package/helps/integration/task/headless-pdf-save.ms.md +23 -0
  41. package/helps/integration/task/headless-pdf-save.zh.md +23 -0
  42. package/package.json +5 -3
  43. package/server/engine/connector/headless-connector.ts +4 -0
  44. package/server/engine/task/headless-pdf-capture-board.ts +363 -0
  45. package/server/engine/task/headless-pdf-capture.ts +85 -20
  46. package/server/engine/task/headless-pdf-open.ts +277 -18
  47. package/server/engine/task/headless-pdf-save.ts +53 -124
  48. package/server/engine/task/index.ts +1 -0
  49. package/translations/en.json +1 -0
  50. package/translations/ko.json +1 -0
@@ -2,39 +2,208 @@ import * as ejs from 'ejs'
2
2
  import { PDFDocument } from 'pdf-lib'
3
3
  import { TaskRegistry } from '@things-factory/integration-base'
4
4
  import { ConnectionManager } from '@things-factory/integration-base'
5
+ import { access } from '@things-factory/utils'
5
6
 
6
7
  async function HeadlessPDFOpen(step, context) {
7
8
  var { connection: connectionName, params: stepOptions } = step
8
9
  var {
10
+ accessor,
9
11
  coverPage,
10
12
  lastPage,
11
13
  header,
12
14
  footer,
13
15
  watermark,
14
- fileName // 추가된 파일 이름
16
+ fileName,
17
+ format,
18
+ width,
19
+ height,
20
+ marginTop,
21
+ marginBottom,
22
+ marginLeft,
23
+ marginRight,
24
+ scale,
25
+ printBackground,
26
+ landscape,
27
+ preferCSSPageSize
15
28
  } = stepOptions || {}
16
-
17
- const pdf = await PDFDocument.create()
29
+ const { data, logger } = context
18
30
 
19
31
  const headlessPool = ConnectionManager.getConnectionInstanceByName(context.domain, connectionName)
20
32
  let browser
21
33
 
34
+ // Create a new PDF document using pdf-lib
35
+ const pdfDoc = await PDFDocument.create()
36
+
22
37
  try {
23
38
  browser = await headlessPool.acquire()
24
39
  const page = await browser.newPage()
25
40
 
26
- // Cover Page를 PDF로 변환
41
+ const templateInput = access(accessor, data)
42
+
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
+ // Convert Cover Page to PDF and add it to the document (if provided)
27
53
  if (coverPage) {
28
- await page.setContent(coverPage)
29
- const coverPageBuffer = await page.pdf()
30
- coverPage = coverPageBuffer
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))
31
130
  }
32
131
 
33
- // Last Page를 PDF로 변환
132
+ var lastPageBuffer
133
+
134
+ // Process Last Page (if provided) but add it later in the save task
34
135
  if (lastPage) {
35
- await page.setContent(lastPage)
36
- const lastPageBuffer = await page.pdf()
37
- lastPage = lastPageBuffer
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
+ // }
169
+
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
+ }
188
+
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
+ }
205
+
206
+ lastPageBuffer = await page.pdf(pageOptions)
38
207
  }
39
208
 
40
209
  await page.close()
@@ -46,25 +215,29 @@ async function HeadlessPDFOpen(step, context) {
46
215
  throw error
47
216
  }
48
217
 
49
- const data = {
50
- pdf,
51
- coverPage,
52
- lastPage,
218
+ const pdfInfo = {
219
+ pdfDoc,
220
+ lastPageBuffer,
53
221
  header,
54
222
  footer,
55
223
  watermark,
56
224
  fileName,
57
- pageCount: 0
225
+ pageCount: pdfDoc.getPageCount()
58
226
  }
59
227
 
60
- context.__headless_pdf = data
228
+ context.__headless_pdf = pdfInfo
61
229
 
62
230
  return {
63
- data
231
+ data: pdfInfo
64
232
  }
65
233
  }
66
234
 
67
235
  HeadlessPDFOpen.parameterSpec = [
236
+ {
237
+ type: 'scenario-step-input',
238
+ name: 'accessor',
239
+ label: 'accessor'
240
+ },
68
241
  {
69
242
  type: 'textarea',
70
243
  name: 'coverPage',
@@ -96,8 +269,94 @@ HeadlessPDFOpen.parameterSpec = [
96
269
  type: 'string',
97
270
  name: 'fileName',
98
271
  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'
99
357
  }
100
358
  ]
359
+
101
360
  HeadlessPDFOpen.help = 'integration/task/headless-pdf-open'
102
361
 
103
362
  TaskRegistry.registerTaskHandler('headless-pdf-open', HeadlessPDFOpen)
@@ -1,145 +1,74 @@
1
- import * as fs from 'fs'
2
- import * as path from 'path'
3
- import * as ejs from 'ejs'
4
- import { PDFDocument, rgb, degrees } from 'pdf-lib'
1
+ import { PDFDocument } from 'pdf-lib'
2
+ const { Readable } = require('stream')
5
3
 
6
4
  import { TaskRegistry } from '@things-factory/integration-base'
7
-
8
- async function HeadlessPDFRenderHeadersAndFooters(pdf, headerTemplate, footerTemplate, watermark, totalPages) {
9
- const pages = pdf.getPages()
10
-
11
- pages.forEach((page, index) => {
12
- const pageNumber = index + 1
13
- const pageWidth = page.getWidth()
14
-
15
- // 헤더와 푸터 렌더링 (페이지 번호 및 총 페이지 수 포함)
16
- if (headerTemplate || footerTemplate) {
17
- const renderedHeader = headerTemplate
18
- ? ejs.render(headerTemplate, {
19
- pageNumber,
20
- totalPages
21
- })
22
- : ''
23
- const renderedFooter = footerTemplate
24
- ? ejs.render(footerTemplate, {
25
- pageNumber,
26
- totalPages
27
- })
28
- : ''
29
-
30
- const fontSize = 12 // 폰트 크기 설정
31
- const textWidthHeader = fontSize * renderedHeader.length * 0.5
32
- const textWidthFooter = fontSize * renderedFooter.length * 0.5
33
-
34
- // Header/Footer 그리기 (가운데 정렬)
35
- if (renderedHeader) {
36
- page.drawText(renderedHeader, {
37
- x: (pageWidth - textWidthHeader) / 2,
38
- y: page.getHeight() - 30,
39
- size: fontSize,
40
- color: rgb(0, 0, 0)
41
- })
42
- }
43
- if (renderedFooter) {
44
- page.drawText(renderedFooter, {
45
- x: (pageWidth - textWidthFooter) / 2,
46
- y: 30,
47
- size: fontSize,
48
- color: rgb(0, 0, 0)
49
- })
50
- }
51
- }
52
-
53
- // 워터마크 추가
54
- if (watermark) {
55
- const fontSize = 50
56
-
57
- page.drawText(watermark, {
58
- x: pageWidth / 2 - (fontSize * watermark.length) / 4,
59
- y: page.getHeight() / 2 - fontSize / 2,
60
- size: fontSize,
61
- color: rgb(0.75, 0.75, 0.75),
62
- rotate: degrees(-45),
63
- opacity: 0.5
64
- })
65
- }
66
- })
67
- }
5
+ import { Attachment, createAttachment } from '@things-factory/attachment-base'
6
+ import { getRepository } from '@things-factory/shell'
68
7
 
69
8
  async function HeadlessPDFSave(step, context) {
70
- var { params: stepOptions } = step
71
- var { outputPath } = stepOptions || {}
72
- var { domain, data, __headless_pdf } = context
9
+ var { domain, user, __headless_pdf } = context
73
10
 
74
- var { pdf, coverPage, lastPage, watermark, fileName } = __headless_pdf
75
-
76
- const totalPages = pdf.getPages().length
77
- await HeadlessPDFRenderHeadersAndFooters(pdf, __headless_pdf.header, __headless_pdf.footer, watermark, totalPages)
78
-
79
- const finalFileName = fileName || `document-${Date.now()}.pdf`
80
- const savePath = path.join(outputPath || './attachments', finalFileName)
81
-
82
- if (coverPage) {
83
- const coverPageDoc = await PDFDocument.load(coverPage)
84
- const [cover] = await pdf.copyPages(coverPageDoc, [0])
85
-
86
- if (watermark) {
87
- const fontSize = 50
88
- const pageWidth = cover.getWidth()
89
- const pageHeight = cover.getHeight()
90
-
91
- cover.drawText(watermark, {
92
- x: pageWidth / 2 - (fontSize * watermark.length) / 4,
93
- y: pageHeight / 2 - fontSize / 2,
94
- size: fontSize,
95
- color: rgb(0.75, 0.75, 0.75),
96
- rotate: degrees(-45),
97
- opacity: 0.5
98
- })
99
- }
100
-
101
- pdf.insertPage(0, cover)
11
+ if (!__headless_pdf || !__headless_pdf.pdfDoc) {
12
+ throw new Error(
13
+ 'No PDF document found. Ensure that headless-pdf-open and headless-pdf-capture tasks are executed before saving.'
14
+ )
102
15
  }
103
16
 
104
- if (lastPage) {
105
- const lastPageDoc = await PDFDocument.load(lastPage)
106
- const [last] = await pdf.copyPages(lastPageDoc, [0])
17
+ const pdfDoc = __headless_pdf.pdfDoc
107
18
 
108
- if (watermark) {
109
- const fontSize = 50
110
- const pageWidth = last.getWidth()
111
- const pageHeight = last.getHeight()
19
+ // Add last page if it exists
20
+ 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))
24
+ }
112
25
 
113
- last.drawText(watermark, {
114
- x: pageWidth / 2 - (fontSize * watermark.length) / 4,
115
- y: pageHeight / 2 - fontSize / 2,
116
- size: fontSize,
117
- color: rgb(0.75, 0.75, 0.75),
118
- rotate: degrees(-45),
119
- opacity: 0.5
120
- })
26
+ const pdfBytes = await pdfDoc.save()
27
+
28
+ const finalFileName = __headless_pdf.fileName || `document-${Date.now()}.pdf`
29
+
30
+ const saved = await createAttachment(
31
+ null,
32
+ {
33
+ attachment: {
34
+ file: {
35
+ createReadStream: () =>
36
+ new Readable({
37
+ read() {
38
+ this.push(pdfBytes)
39
+ this.push(null)
40
+ }
41
+ }),
42
+ filename: finalFileName,
43
+ mimetype: 'application/pdf',
44
+ encoding: 'binary'
45
+ },
46
+ refType: 'headless-pdf-save',
47
+ refBy: 'pdf-published'
48
+ }
49
+ },
50
+ {
51
+ state: { domain, user }
121
52
  }
53
+ )
122
54
 
123
- pdf.addPage(last)
124
- }
125
-
126
- const pdfBytes = await pdf.save()
127
- fs.writeFileSync(savePath, pdfBytes)
55
+ const attachment = await getRepository(Attachment).findOne({
56
+ where: { id: saved.id }
57
+ })
128
58
 
129
59
  return {
130
60
  data: {
131
- link: savePath
61
+ id: attachment.id,
62
+ name: attachment.name,
63
+ path: attachment.path,
64
+ mimetype: attachment.mimetype,
65
+ refBy: attachment.refBy,
66
+ fullpath: attachment.fullpath
132
67
  }
133
68
  }
134
69
  }
135
70
 
136
- HeadlessPDFSave.parameterSpec = [
137
- {
138
- type: 'string',
139
- name: 'outputPath',
140
- label: 'output-path'
141
- }
142
- ]
71
+ HeadlessPDFSave.parameterSpec = []
143
72
 
144
73
  HeadlessPDFSave.help = 'integration/task/headless-pdf-save'
145
74
 
@@ -1,3 +1,4 @@
1
1
  import './headless-pdf-capture'
2
+ import './headless-pdf-capture-board'
2
3
  import './headless-pdf-open'
3
4
  import './headless-pdf-save'
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "label.display-header/footer": "display header/footer",
3
+ "label.board-draft": "draft board",
3
4
  "label.filename": "filename",
4
5
  "label.footer": "footer template",
5
6
  "label.header": "header template",
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "label.display-header/footer": "머릿글/바닥글 표시여부",
3
+ "label.board-draft": "편집본 대상",
3
4
  "label.filename": "파일이름",
4
5
  "label.footer": "바닥글 템플릿",
5
6
  "label.header": "머릿글 템플릿",