@things-factory/integration-headless 8.0.0-beta.9 → 8.0.0

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 (35) hide show
  1. package/dist-server/engine/connector/headless-connector.d.ts +14 -0
  2. package/dist-server/engine/connector/headless-connector.js +54 -0
  3. package/dist-server/engine/connector/headless-connector.js.map +1 -0
  4. package/dist-server/engine/connector/headless-pool.d.ts +3 -0
  5. package/dist-server/engine/connector/headless-pool.js +63 -0
  6. package/dist-server/engine/connector/headless-pool.js.map +1 -0
  7. package/dist-server/engine/connector/index.d.ts +1 -0
  8. package/dist-server/engine/connector/index.js +4 -0
  9. package/dist-server/engine/connector/index.js.map +1 -0
  10. package/dist-server/engine/index.d.ts +1 -0
  11. package/dist-server/engine/index.js +1 -0
  12. package/dist-server/engine/index.js.map +1 -1
  13. package/dist-server/engine/task/pdf-capture-util.d.ts +1 -1
  14. package/dist-server/engine/task/pdf-capture-util.js +3 -3
  15. package/dist-server/engine/task/pdf-capture-util.js.map +1 -1
  16. package/dist-server/tsconfig.tsbuildinfo +1 -1
  17. package/helps/integration/connector/headless-connector.ja.md +31 -183
  18. package/helps/integration/connector/headless-connector.ko.md +32 -177
  19. package/helps/integration/connector/headless-connector.md +31 -178
  20. package/helps/integration/connector/headless-connector.ms.md +32 -180
  21. package/helps/integration/connector/headless-connector.zh.md +31 -178
  22. package/package.json +6 -6
  23. package/server/engine/connector/headless-connector.ts +68 -0
  24. package/server/engine/connector/headless-pool.ts +69 -0
  25. package/server/engine/connector/index.ts +1 -0
  26. package/server/engine/index.ts +2 -0
  27. package/server/engine/task/headless-pdf-capture-board.ts +182 -0
  28. package/server/engine/task/headless-pdf-capture-markdown.ts +47 -0
  29. package/server/engine/task/headless-pdf-capture.ts +39 -0
  30. package/server/engine/task/headless-pdf-open.ts +98 -0
  31. package/server/engine/task/headless-pdf-save.ts +88 -0
  32. package/server/engine/task/index.ts +9 -0
  33. package/server/engine/task/pdf-capture-util.ts +331 -0
  34. package/server/index.ts +3 -0
  35. package/server/tsconfig.json +10 -0
@@ -0,0 +1,98 @@
1
+ import { PDFDocument } from 'pdf-lib'
2
+ import { TaskRegistry } from '@things-factory/integration-base'
3
+ import { access } from '@things-factory/utils'
4
+ import { PDFCaptureUtil, getCommonParameterSpec } from './pdf-capture-util'
5
+
6
+ async function HeadlessPDFOpen(step, context) {
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
+ try {
16
+ const fileNameEncoded = fileName ? new TextEncoder().encode(access(fileName, data)) : undefined
17
+
18
+ const pdfInfo = {
19
+ pdfDoc,
20
+ watermark,
21
+ fileName: fileNameEncoded,
22
+ pageCount: 0
23
+ } as any
24
+ context.__headless_pdf = pdfInfo
25
+
26
+ var pdfUtil = new PDFCaptureUtil(context)
27
+ await pdfUtil.initBrowser(connectionName)
28
+
29
+ const templateInput = access(accessor, data)
30
+
31
+ // Convert Cover Page to PDF and add it to the document (if provided)
32
+ if (coverPage) {
33
+ const renderedCoverPage = pdfUtil.renderTemplate(coverPage, templateInput)
34
+ await pdfUtil.processPageAndGeneratePDF(stepOptions, renderedCoverPage)
35
+ }
36
+
37
+ var lastPageBuffer
38
+
39
+ // Process Last Page (if provided) but add it later in the save task
40
+ if (lastPage) {
41
+ const renderedLastPage = pdfUtil.renderTemplate(lastPage, templateInput)
42
+ lastPageBuffer = await pdfUtil.generateLastPageBuffer(stepOptions, renderedLastPage)
43
+ }
44
+
45
+ pdfInfo.lastPageBuffer = lastPageBuffer
46
+ pdfInfo.pageCount = pdfDoc.getPageCount()
47
+ pdfInfo.header = header
48
+ pdfInfo.footer = footer
49
+
50
+ return {
51
+ data: pdfInfo
52
+ }
53
+ } catch (error) {
54
+ throw error
55
+ } finally {
56
+ await pdfUtil?.closeBrowser()
57
+ }
58
+ }
59
+
60
+ HeadlessPDFOpen.parameterSpec = [
61
+ ...getCommonParameterSpec(),
62
+ {
63
+ type: 'textarea',
64
+ name: 'coverPage',
65
+ label: 'pdf-cover-page'
66
+ },
67
+ {
68
+ type: 'textarea',
69
+ name: 'lastPage',
70
+ label: 'pdf-last-page'
71
+ },
72
+ {
73
+ type: 'string',
74
+ name: 'header',
75
+ label: 'header',
76
+ placeholder: 'Page <%= pageNumber %> of <%= totalPages %>'
77
+ },
78
+ {
79
+ type: 'string',
80
+ name: 'footer',
81
+ label: 'footer',
82
+ placeholder: 'Page <%= pageNumber %> of <%= totalPages %>'
83
+ },
84
+ {
85
+ type: 'string',
86
+ name: 'watermark',
87
+ label: 'watermark'
88
+ },
89
+ {
90
+ type: 'string',
91
+ name: 'fileName',
92
+ label: 'filename'
93
+ }
94
+ ]
95
+
96
+ HeadlessPDFOpen.help = 'integration/task/headless-pdf-open'
97
+
98
+ TaskRegistry.registerTaskHandler('headless-pdf-open', HeadlessPDFOpen)
@@ -0,0 +1,88 @@
1
+ import { PDFDocument } from 'pdf-lib'
2
+ import { Readable } from 'stream'
3
+ import { TaskRegistry } from '@things-factory/integration-base'
4
+ import { Attachment, createAttachment } from '@things-factory/attachment-base'
5
+ import { getRepository } from '@things-factory/shell'
6
+
7
+ async function HeadlessPDFSave(step, context) {
8
+ const { domain, user, __headless_pdf } = context
9
+
10
+ if (!__headless_pdf || !__headless_pdf.pdfDoc) {
11
+ throw new Error(
12
+ 'No PDF document found. Ensure that headless-pdf-open and headless-pdf-capture tasks are executed before saving.'
13
+ )
14
+ }
15
+
16
+ const pdfDoc = __headless_pdf.pdfDoc
17
+
18
+ // Add last page if it exists
19
+ if (__headless_pdf.lastPageBuffer) {
20
+ await appendLastPageToPDF(pdfDoc, __headless_pdf.lastPageBuffer)
21
+ }
22
+
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)
28
+
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
+ }
46
+
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(
53
+ null,
54
+ {
55
+ attachment: {
56
+ file: {
57
+ createReadStream: () =>
58
+ new Readable({
59
+ read() {
60
+ this.push(pdfBytes)
61
+ this.push(null)
62
+ }
63
+ }),
64
+ filename: finalFileName,
65
+ mimetype: 'application/pdf',
66
+ encoding: 'binary'
67
+ },
68
+ refType: 'headless-pdf-save',
69
+ refBy: 'pdf-published'
70
+ }
71
+ },
72
+ {
73
+ state: { domain, user }
74
+ }
75
+ )
76
+ }
77
+
78
+ async function getAttachmentDetails(attachmentId) {
79
+ return await getRepository(Attachment).findOne({
80
+ where: { id: attachmentId }
81
+ })
82
+ }
83
+
84
+ HeadlessPDFSave.parameterSpec = []
85
+
86
+ HeadlessPDFSave.help = 'integration/task/headless-pdf-save'
87
+
88
+ TaskRegistry.registerTaskHandler('headless-pdf-save', HeadlessPDFSave)
@@ -0,0 +1,9 @@
1
+ import './headless-pdf-capture'
2
+ import './headless-pdf-capture-markdown'
3
+ import './headless-pdf-capture-board'
4
+ import './headless-pdf-open'
5
+ import './headless-pdf-save'
6
+ import './pdf-capture-util'
7
+
8
+ export * from './pdf-capture-util'
9
+ export * from './headless-pdf-capture-board'
@@ -0,0 +1,331 @@
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, pageOptions: any): Promise<PDFDocument> {
62
+ const contentBuffer = await page.pdf(pageOptions)
63
+ const contentPdfDoc = await PDFDocument.load(contentBuffer)
64
+ return contentPdfDoc
65
+ }
66
+
67
+ async appendPDFContentToDocument(pdfDoc: PDFDocument, contentPdfDoc: PDFDocument) {
68
+ const copiedPages = await pdfDoc.copyPages(contentPdfDoc, contentPdfDoc.getPageIndices())
69
+ copiedPages.forEach(page => pdfDoc.addPage(page))
70
+ this.context.__headless_pdf.pageCount += copiedPages.length
71
+ }
72
+
73
+ async processPageAndGeneratePDF(stepOptions: StepOptions, htmlContent: string, page?: Page) {
74
+ if (!this.browser) throw new Error('Browser not initialized. Call initBrowser() first.')
75
+
76
+ if (!page) {
77
+ page = page || (await this.browser.newPage())
78
+ }
79
+
80
+ if (htmlContent) {
81
+ // 페이지 컨텐츠 설정
82
+ await page.setContent(htmlContent, { waitUntil: 'networkidle0' }) // HTML 컨텐츠가 완전히 로드될 때까지 대기
83
+ }
84
+
85
+ // 페이지가 완전히 로드된 후에 워터마크와 머릿글 적용
86
+ const { __headless_pdf } = this.context
87
+ const { header, footer, watermark, landscape } = __headless_pdf
88
+
89
+ if (header || footer || watermark) {
90
+ await this.applyHeaderFooterWatermark(page, { header, footer, watermark, landscape, data: __headless_pdf })
91
+ }
92
+
93
+ const pageOptions = this.buildPageOptions(stepOptions)
94
+ const contentPdfDoc = await this.generatePDFContent(page, pageOptions)
95
+ await this.appendPDFContentToDocument(__headless_pdf.pdfDoc, contentPdfDoc)
96
+ await page.close()
97
+ }
98
+
99
+ buildPageOptions({
100
+ format,
101
+ width,
102
+ height,
103
+ marginTop,
104
+ marginBottom,
105
+ marginLeft,
106
+ marginRight,
107
+ scale,
108
+ printBackground,
109
+ landscape,
110
+ preferCSSPageSize
111
+ }: StepOptions): any {
112
+ if (preferCSSPageSize) {
113
+ width = undefined
114
+ height = undefined
115
+ }
116
+
117
+ return {
118
+ format,
119
+ width,
120
+ height,
121
+ margin: {
122
+ top: marginTop,
123
+ right: marginRight,
124
+ bottom: marginBottom,
125
+ left: marginLeft
126
+ },
127
+ scale,
128
+ printBackground,
129
+ landscape,
130
+ displayHeaderFooter: false,
131
+ preferCSSPageSize
132
+ }
133
+ }
134
+
135
+ async applyHeaderFooterWatermark(
136
+ page: Page,
137
+ {
138
+ header,
139
+ footer,
140
+ watermark,
141
+ landscape,
142
+ data
143
+ }: { header: string; footer: string; watermark: string; landscape: boolean; data: any }
144
+ ) {
145
+ await page.evaluate(
146
+ ({ header, footer, watermark, landscape, data }) => {
147
+ const setPositioning = (element: HTMLElement, position: 'header' | 'footer') => {
148
+ element.style.position = 'fixed'
149
+ element.style.left = '0'
150
+ element.style.width = '100%'
151
+ element.style.textAlign = 'center'
152
+ element.style.fontSize = '12px'
153
+ element.style.margin = '0'
154
+ element.style.padding = '0'
155
+ element.style.zIndex = '1000'
156
+
157
+ if (position === 'header') {
158
+ element.style.top = '0'
159
+ } else if (position === 'footer') {
160
+ element.style.bottom = '0'
161
+ }
162
+ }
163
+
164
+ if (header) {
165
+ const headerElement = document.createElement('div')
166
+ headerElement.innerHTML = header
167
+ setPositioning(headerElement, 'header')
168
+ document.body.appendChild(headerElement)
169
+ }
170
+
171
+ if (footer) {
172
+ const footerElement = document.createElement('div')
173
+ footerElement.innerHTML = footer
174
+ setPositioning(footerElement, 'footer')
175
+ document.body.appendChild(footerElement)
176
+ }
177
+
178
+ if (watermark) {
179
+ const watermarkElement = document.createElement('div')
180
+ watermarkElement.innerHTML = watermark
181
+ watermarkElement.style.position = 'fixed'
182
+ watermarkElement.style.top = landscape ? '40%' : '50%'
183
+ watermarkElement.style.left = '50%'
184
+ watermarkElement.style.transform = 'translate(-50%, -50%) rotate(-45deg)'
185
+ watermarkElement.style.opacity = '0.2'
186
+ watermarkElement.style.fontSize = '50px'
187
+ watermarkElement.style.color = 'red'
188
+ watermarkElement.style.pointerEvents = 'none'
189
+ watermarkElement.style.zIndex = '9999'
190
+ document.body.appendChild(watermarkElement)
191
+ }
192
+ },
193
+ {
194
+ header: header && this.renderTemplate(header, data),
195
+ footer: footer && this.renderTemplate(footer, data),
196
+ watermark: watermark && this.renderTemplate(watermark, data),
197
+ landscape,
198
+ data
199
+ }
200
+ )
201
+ }
202
+
203
+ /**
204
+ * Generates a PDF buffer for the last page.
205
+ * @param stepOptions - Options used for generating the PDF.
206
+ * @param htmlContent - HTML content to be converted to PDF.
207
+ * @returns {Promise<Uint8Array>} - The generated PDF as a buffer.
208
+ */
209
+ async generateLastPageBuffer(stepOptions: StepOptions, htmlContent: string): Promise<Uint8Array> {
210
+ if (!this.browser) throw new Error('Browser not initialized. Call initBrowser() first.')
211
+
212
+ const page = await this.browser.newPage()
213
+
214
+ // 페이지에 HTML 콘텐츠를 설정합니다.
215
+ await page.setContent(htmlContent, { waitUntil: 'networkidle0' })
216
+
217
+ // 페이지가 완전히 로드된 후에 워터마크와 머릿글 적용
218
+ const { __headless_pdf } = this.context
219
+ const { header, footer, watermark, landscape } = __headless_pdf
220
+
221
+ if (header || footer || watermark) {
222
+ await this.applyHeaderFooterWatermark(page, { header, footer, watermark, landscape, data: __headless_pdf })
223
+ }
224
+
225
+ // 페이지 옵션 설정
226
+ const pageOptions = this.buildPageOptions(stepOptions)
227
+
228
+ // PDF를 생성하고 버퍼를 반환합니다.
229
+ const lastPageBuffer = await page.pdf(pageOptions)
230
+
231
+ // 페이지 닫기
232
+ await page.close()
233
+
234
+ return lastPageBuffer
235
+ }
236
+ }
237
+
238
+ export function getCommonParameterSpec() {
239
+ return [
240
+ {
241
+ type: 'scenario-step-input',
242
+ name: 'accessor',
243
+ label: 'accessor'
244
+ },
245
+ {
246
+ type: 'select',
247
+ name: 'format',
248
+ label: 'page-format',
249
+ property: {
250
+ options: [
251
+ { display: '', value: '' },
252
+ { display: 'A4', value: 'A4' },
253
+ { display: 'A3', value: 'A3' },
254
+ { display: 'Letter', value: 'Letter' },
255
+ { display: 'Legal', value: 'Legal' }
256
+ ]
257
+ },
258
+ description: 'Select the paper format for the PDF'
259
+ },
260
+ {
261
+ type: 'string',
262
+ name: 'width',
263
+ label: 'page-width',
264
+ placeholder: '(e.g., "8.5in", "21cm", "600px")',
265
+ description: 'Specify the width of the page (e.g., "8.5in", "21cm", "600px")'
266
+ },
267
+ {
268
+ type: 'string',
269
+ name: 'height',
270
+ label: 'page-height',
271
+ placeholder: '(e.g., "11in", "29.7cm", "800px")',
272
+ description: 'Specify the height of the page (e.g., "11in", "29.7cm", "800px")'
273
+ },
274
+ {
275
+ type: 'string',
276
+ name: 'marginTop',
277
+ label: 'page-margin-top',
278
+ placeholder: '(e.g., "0.5in", "1cm", "100px")',
279
+ description: 'Set the top margin for the page'
280
+ },
281
+ {
282
+ type: 'string',
283
+ name: 'marginBottom',
284
+ label: 'page-margin-bottom',
285
+ placeholder: '(e.g., "0.5in", "1cm", "100px")',
286
+ description: 'Set the bottom margin for the page'
287
+ },
288
+ {
289
+ type: 'string',
290
+ name: 'marginLeft',
291
+ label: 'page-margin-left',
292
+ placeholder: '(e.g., "0.5in", "1cm", "100px")',
293
+ description: 'Set the left margin for the page'
294
+ },
295
+ {
296
+ type: 'string',
297
+ name: 'marginRight',
298
+ label: 'page-margin-right',
299
+ placeholder: '(e.g., "0.5in", "1cm", "100px")',
300
+ description: 'Set the right margin for the page'
301
+ },
302
+ {
303
+ type: 'number',
304
+ name: 'scale',
305
+ label: 'page-scale',
306
+ defaultValue: 1,
307
+ description: 'Set the scale of the page content (default is 1)'
308
+ },
309
+ {
310
+ type: 'boolean',
311
+ name: 'printBackground',
312
+ label: 'print-background',
313
+ defaultValue: true,
314
+ description: 'Include background graphics when printing the page'
315
+ },
316
+ {
317
+ type: 'boolean',
318
+ name: 'landscape',
319
+ label: 'landscape',
320
+ defaultValue: false,
321
+ description: 'Print the PDF in landscape orientation'
322
+ },
323
+ {
324
+ type: 'boolean',
325
+ name: 'preferCSSPageSize',
326
+ label: 'prefer-css-page-size',
327
+ defaultValue: false,
328
+ description: 'Whether to prefer the CSS-defined page size over the given width and height'
329
+ }
330
+ ]
331
+ }
@@ -0,0 +1,3 @@
1
+ import './engine'
2
+
3
+ export * from './engine/task'
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "../../tsconfig-base.json",
3
+ "compilerOptions": {
4
+ "strict": false,
5
+ "module": "commonjs",
6
+ "outDir": "../dist-server",
7
+ "baseUrl": "./"
8
+ },
9
+ "include": ["./**/*"]
10
+ }