Skip to content

Commit 3d1035f

Browse files
committed
feat(ghoulscript): add worker rpc
1 parent 3f68df0 commit 3d1035f

File tree

11 files changed

+1094
-217
lines changed

11 files changed

+1094
-217
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
# ghoulscript
2+
3+
## License
4+
5+
[AGPL-3.0](./LICENSE)

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
"scripts": {
99
"lint": "eslint . --ext .js,.vue,.ts --format pretty",
1010
"fix": "yarn lint --fix",
11+
"test": "yarn workspaces foreach -A run test",
12+
"build": "yarn workspaces foreach -A --topological-dev run build",
1113
"postinstall": "husky"
1214
},
1315
"lint-staged": {

packages/ghoulscript/LICENSE

Lines changed: 661 additions & 0 deletions
Large diffs are not rendered by default.

packages/ghoulscript/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
# ghoulscript
2+
3+
## License
4+
5+
[AGPL-3.0](./LICENSE)
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { defineBuildConfig } from 'unbuild'
22

33
export default defineBuildConfig({
4-
entries : ['src/index'],
4+
entries : ['src/index', 'src/rpc.worker'],
55
declaration: true,
66
rollup : { emitCJS: true },
77
})

packages/ghoulscript/package.json

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,13 @@
77
"exports": {
88
".": {
99
"import": "./dist/index.mjs",
10-
"require": "./dist/index.cjs"
10+
"require": "./dist/index.cjs",
11+
"types": "./dist/index.d.ts"
12+
},
13+
"./rpc.worker": {
14+
"import": "./dist/rpc.worker.mjs",
15+
"require": "./dist/rpc.worker.cjs",
16+
"types": "./dist/rpc.worker.d.ts"
1117
}
1218
},
1319
"files": [
@@ -27,5 +33,6 @@
2733
"dependencies": {
2834
"@privyid/ghostscript": "workspace:^",
2935
"defu": "^6.1.4"
30-
}
36+
},
37+
"license": "AGPL-3.0-only"
3138
}

packages/ghoulscript/src/core.ts

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
import useGS from '@privyid/ghostscript'
2+
import { defu } from 'defu'
3+
4+
interface PageRange {
5+
start: number,
6+
end: number,
7+
}
8+
9+
type PageList = Array<number | PageRange | [number, number] | string>
10+
11+
export interface CompressOptions {
12+
/**
13+
* Document protection password
14+
*/
15+
password?: string,
16+
/**
17+
* PDF Preset setting
18+
* @default 'screen'
19+
*/
20+
pdfSettings: 'screen' | 'ebook' | 'printer' | 'prepress' | 'default',
21+
/**
22+
* Enable Linearization
23+
* @default true
24+
*/
25+
fastWebView: boolean,
26+
/**
27+
* Compability version
28+
* @default '1.4'
29+
*/
30+
compatibilityLevel: string,
31+
/**
32+
* Color conversion strategy
33+
* @default 'RGB'
34+
*/
35+
colorConversionStrategy: 'RGB' | 'CMYK',
36+
/**
37+
* Remove transparency
38+
* @default true
39+
*/
40+
noTransparency: boolean,
41+
/**
42+
* Owner password
43+
*/
44+
ownerPassword?: string,
45+
/**
46+
* User password
47+
*/
48+
userPassword?: string,
49+
/**
50+
* Keep document password if document have a password protection
51+
* otherwise will remove the password
52+
* @default true
53+
*/
54+
keepPassword: boolean,
55+
/**
56+
* Page list for splitting
57+
* @example
58+
* // Spesific page
59+
* createPDF({ pageList: [1, 2, 3] })
60+
* // Range page
61+
* createPDF({ pageList: ['1-3', '6-10'] })
62+
*/
63+
pageList?: PageList,
64+
}
65+
66+
async function createPDF (inputs: ArrayBufferView[], options: Partial<CompressOptions> = {}): Promise<Uint8Array> {
67+
const gs = await useGS()
68+
const opts = defu<CompressOptions, [CompressOptions]>(options, {
69+
pdfSettings : 'screen',
70+
compatibilityLevel : '1.4',
71+
colorConversionStrategy: 'RGB',
72+
fastWebView : true,
73+
noTransparency : true,
74+
keepPassword : true,
75+
})
76+
77+
const args = [
78+
'-dQUIET',
79+
'-dNOPAUSE',
80+
'-dBATCH',
81+
'-dSAFER',
82+
'-sDEVICE=pdfwrite',
83+
]
84+
85+
let userPassword = opts.userPassword
86+
let ownerPassword = opts.ownerPassword
87+
88+
if (opts.password) {
89+
args.push(`-sPDFPassword=${opts.password}`)
90+
91+
if (opts.keepPassword) {
92+
userPassword = userPassword ?? opts.password
93+
ownerPassword = ownerPassword ?? opts.password
94+
}
95+
}
96+
97+
if (userPassword)
98+
args.push(`-sUserPassword=${userPassword}`, `-sOwnerPassword=${ownerPassword ?? userPassword}`)
99+
100+
if (opts.noTransparency)
101+
args.push('-dNOTRANSPARENCY')
102+
103+
if (opts.pageList) {
104+
const pageList = Array.isArray(opts.pageList)
105+
? opts.pageList
106+
.map((page) => {
107+
if (Array.isArray(page))
108+
return `${page[0]}-${page[1]}`
109+
110+
if (typeof page === 'object' && page !== null)
111+
return `${page.start}-${page.end}`
112+
113+
return page.toString()
114+
})
115+
.join(',')
116+
: opts.pageList
117+
118+
args.push(`-sPageList=${pageList}`)
119+
}
120+
121+
args.push(
122+
`-dCompatibilityLevel=${opts.compatibilityLevel}`,
123+
`-sColorConversionStrategy=${opts.colorConversionStrategy}`,
124+
`-dPDFSETTINGS=/${opts.pdfSettings}`,
125+
`-dFastWebView=${opts.fastWebView.toString()}`,
126+
'-sOutputFile=./output',
127+
)
128+
129+
for (const [i, input] of inputs.entries()) {
130+
const inputFilename = `./input-${i}`
131+
132+
gs.FS.writeFile(inputFilename, input)
133+
args.push(inputFilename)
134+
}
135+
136+
await gs.callMain(args)
137+
138+
return gs.FS.readFile('./output', { encoding: 'binary' })
139+
}
140+
141+
export async function optimizePDF (input: ArrayBufferView, option: Partial<CompressOptions> = {}) {
142+
return await createPDF([input], option)
143+
}
144+
145+
export async function combinePDF (inputs: ArrayBufferView[], option: Partial<CompressOptions> = {}) {
146+
return await createPDF(inputs, option)
147+
}
148+
149+
export async function splitPdf (input: ArrayBufferView, pageLists: PageList[], option: Partial<CompressOptions> = {}) {
150+
return await Promise.all(
151+
pageLists.map(async (pageList) => {
152+
return await createPDF([input], defu({ pageList }, option))
153+
}),
154+
)
155+
}
156+
157+
export async function addPassword (input: ArrayBufferView, userPassword: string, ownerPassword: string = userPassword) {
158+
return await createPDF([input], {
159+
ownerPassword,
160+
userPassword,
161+
})
162+
}
163+
164+
/**
165+
* Remove password
166+
* @param input
167+
* @param password
168+
* @returns
169+
*/
170+
export async function removePassword (input: ArrayBufferView, password: string) {
171+
return await createPDF([input], { keepPassword: false, password: password })
172+
}
173+
174+
export interface RenderOptions {
175+
/**
176+
* Render resolution
177+
* @default 96
178+
*/
179+
resolution: number,
180+
/**
181+
* Text alpha bits
182+
* @default 4
183+
*/
184+
textAlphaBits: 1 | 2 | 3 | 4,
185+
/**
186+
* Graphic alpha bits
187+
* @default 4
188+
*/
189+
graphicsAlphaBits: 1 | 2 | 3 | 4,
190+
/**
191+
* Output format
192+
* @default 'jpeg'
193+
*/
194+
format: 'jpg' | 'png',
195+
}
196+
197+
export async function renderPageAsImage (input: ArrayBufferView, pageNumber: number = 1, options: Partial<RenderOptions> = {}) {
198+
const gs = await useGS()
199+
const opts = defu<RenderOptions, [RenderOptions]>(options, {
200+
format : 'jpg',
201+
graphicsAlphaBits: 4,
202+
textAlphaBits : 4,
203+
resolution : 96,
204+
})
205+
206+
const device = opts.format === 'png' ? 'png16m' : 'jpeg'
207+
const args = [
208+
'-dQUIET',
209+
'-dNOPAUSE',
210+
'-dBATCH',
211+
'-dSAFER',
212+
`-sDEVICE=${device}`,
213+
`-sPageList=${pageNumber}`,
214+
`-r${opts.resolution}`,
215+
`-dTextAlphaBits=${opts.textAlphaBits}`,
216+
`-dGraphicsAlphaBits=${opts.graphicsAlphaBits}`,
217+
'-sOutputFile=./output',
218+
'./input',
219+
]
220+
221+
gs.FS.writeFile('./input', input)
222+
223+
await gs.callMain(args)
224+
225+
return gs.FS.readFile('./output', { encoding: 'binary' })
226+
}
227+
228+
export interface Info {
229+
numPages: number,
230+
pages: Array<{
231+
page: number,
232+
width: number,
233+
height: number,
234+
}>,
235+
}
236+
237+
/**
238+
* Extract PDF Meta
239+
* @param input
240+
* @param options
241+
* @returns
242+
*/
243+
export async function getInfo (input: ArrayBufferView, options: Pick<CompressOptions, 'password'> = {}): Promise<Info> {
244+
const info: Info = {
245+
numPages: 0,
246+
pages : [],
247+
}
248+
249+
const gs = await useGS({
250+
printErr (str) {
251+
const totalpageMatch = str.match(/File has (\d+) pages?/)
252+
253+
if (totalpageMatch)
254+
info.numPages = Number.parseInt(totalpageMatch[1])
255+
256+
const pageMatch = str.match(/Page (\d+) MediaBox: \[([\d .]+)]/)
257+
258+
if (pageMatch) {
259+
const mediaBox = pageMatch[2].split(' ')
260+
261+
info.pages.push({
262+
page : Number.parseInt(pageMatch[1]),
263+
width : Number.parseFloat(mediaBox[2]),
264+
height: Number.parseFloat(mediaBox[3]),
265+
})
266+
}
267+
},
268+
})
269+
270+
const args = [
271+
'-dQUIET',
272+
'-dNOPAUSE',
273+
'-dNODISPLAY',
274+
'-dBATCH',
275+
'-dSAFER',
276+
'-dPDFINFO',
277+
'./input',
278+
]
279+
280+
if (options.password)
281+
args.splice(-1, 0, `-sPDFPassword=${options.password}`)
282+
283+
gs.FS.writeFile('./input', input)
284+
285+
await gs.callMain(args)
286+
287+
return info
288+
}

0 commit comments

Comments
 (0)