x4js 2.3.18 → 2.3.19
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/aicli.md +3 -3
- package/aicontext.md +49 -23
- package/cli/config.mjs +14 -7
- package/cli/copy-plugin.mjs +8 -0
- package/cli/create.mjs +8 -2
- package/cli/dev.mjs +4 -1
- package/cli/less-plugin.mjs +5 -1
- package/cli/main.mjs +15 -5
- package/cli/rawfile-plugin.mjs +23 -3
- package/cli/templates.mjs +5 -1
- package/package.json +1 -1
- package/src/components/calendar/calendar.module.scss +16 -1
- package/src/components/calendar/calendar.ts +52 -9
- package/src/components/checkbox/checkbox.ts +1 -0
- package/src/components/combobox/combobox.ts +51 -23
- package/src/components/dialog/dialog.ts +4 -1
- package/src/components/form/form.ts +9 -5
- package/src/components/gridview/gridview.ts +2 -1
- package/src/components/input/input.ts +63 -38
- package/src/components/keyboard/keyboard.module.scss +35 -4
- package/src/components/keyboard/keyboard.ts +120 -21
- package/src/components/listbox/listbox.ts +6 -1
- package/src/components/messages/messages.ts +12 -3
- package/src/components/popup/popup.ts +19 -20
- package/src/components/radio/radio.ts +8 -4
- package/src/components/rating/rating.ts +38 -48
- package/src/components/slider/slider.ts +38 -8
- package/src/components/spreadsheet/spreadsheet.ts +140 -131
- package/src/components/switch/switch.ts +2 -1
- package/src/components/textarea/textarea.module.scss +2 -4
- package/src/components/textarea/textarea.ts +2 -1
- package/src/components/textedit/textedit.ts +12 -2
- package/src/core/core_application.ts +8 -4
- package/src/core/core_data.ts +64 -26
- package/src/core/core_dom.ts +9 -1
- package/src/core/core_pdf.ts +696 -46
- package/src/core/core_router.ts +42 -44
- package/src/core/core_state.ts +22 -5
package/src/core/core_pdf.ts
CHANGED
|
@@ -19,27 +19,556 @@
|
|
|
19
19
|
|
|
20
20
|
import { Color } from './core_colors.js'
|
|
21
21
|
import { isArray, isFunction, isNumber, isPlainObject, isString } from './core_tools.js'
|
|
22
|
+
import { SvgBuilder, SvgGroup } from './core_svg.js'
|
|
22
23
|
|
|
23
24
|
|
|
25
|
+
/**
|
|
26
|
+
* the widths of the characters, by their code in WinAnsiEncoding, from 32 to 255:
|
|
27
|
+
* the AFM of Adobe for the core fonts. 0: a code that has no character
|
|
28
|
+
*/
|
|
29
|
+
|
|
24
30
|
const WIDTHS: number[] = [
|
|
25
31
|
278, 278, 355, 556, 556, 889, 667, 191, 333, 333, 389, 584, 278, 333, 278, 278,
|
|
26
32
|
556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 278, 278, 584, 584, 584, 556,
|
|
27
33
|
1015, 667, 667, 722, 722, 667, 611, 778, 722, 278, 500, 667, 556, 833, 722, 778,
|
|
28
34
|
667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611, 278, 278, 278, 469, 556,
|
|
29
35
|
333, 556, 556, 500, 556, 556, 278, 556, 556, 222, 222, 500, 222, 833, 556, 556,
|
|
30
|
-
556, 556, 333, 500, 278, 556, 500, 722, 500, 500, 500, 334, 260, 334, 584
|
|
36
|
+
556, 556, 333, 500, 278, 556, 500, 722, 500, 500, 500, 334, 260, 334, 584, 0,
|
|
37
|
+
556, 0, 222, 556, 333, 1000, 556, 556, 333, 1000, 667, 333, 1000, 0, 611, 0,
|
|
38
|
+
0, 222, 222, 333, 333, 350, 556, 1000, 333, 1000, 500, 333, 944, 0, 500, 667,
|
|
39
|
+
278, 333, 556, 556, 556, 556, 260, 556, 333, 737, 370, 556, 584, 333, 737, 333,
|
|
40
|
+
400, 584, 333, 333, 333, 556, 537, 278, 333, 333, 365, 556, 834, 834, 834, 611,
|
|
41
|
+
667, 667, 667, 667, 667, 667, 1000, 722, 667, 667, 667, 667, 278, 278, 278, 278,
|
|
42
|
+
722, 722, 778, 778, 778, 778, 778, 584, 778, 722, 722, 722, 722, 667, 667, 611,
|
|
43
|
+
556, 556, 556, 556, 556, 556, 889, 500, 556, 556, 556, 556, 278, 278, 278, 278,
|
|
44
|
+
556, 556, 556, 556, 556, 556, 556, 584, 611, 556, 556, 556, 556, 500, 556, 500,
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
const WIDTHS_BOLD: number[] = [
|
|
48
|
+
278, 333, 474, 556, 556, 889, 722, 238, 333, 333, 389, 584, 278, 333, 278, 278,
|
|
49
|
+
556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 333, 333, 584, 584, 584, 611,
|
|
50
|
+
975, 722, 722, 722, 722, 667, 611, 778, 722, 278, 556, 722, 611, 833, 722, 778,
|
|
51
|
+
667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611, 333, 278, 333, 584, 556,
|
|
52
|
+
333, 556, 611, 556, 611, 556, 333, 611, 611, 278, 278, 556, 278, 889, 611, 611,
|
|
53
|
+
611, 611, 389, 556, 333, 611, 556, 778, 556, 556, 500, 389, 280, 389, 584, 0,
|
|
54
|
+
556, 0, 278, 556, 500, 1000, 556, 556, 333, 1000, 667, 333, 1000, 0, 611, 0,
|
|
55
|
+
0, 278, 278, 500, 500, 350, 556, 1000, 333, 1000, 556, 333, 944, 0, 500, 667,
|
|
56
|
+
278, 333, 556, 556, 556, 556, 280, 556, 333, 737, 370, 556, 584, 333, 737, 333,
|
|
57
|
+
400, 584, 333, 333, 333, 611, 556, 278, 333, 333, 365, 556, 834, 834, 834, 611,
|
|
58
|
+
722, 722, 722, 722, 722, 722, 1000, 722, 667, 667, 667, 667, 278, 278, 278, 278,
|
|
59
|
+
722, 722, 778, 778, 778, 778, 778, 584, 778, 722, 722, 722, 722, 667, 667, 611,
|
|
60
|
+
556, 556, 556, 556, 556, 556, 889, 556, 556, 556, 556, 556, 278, 278, 278, 278,
|
|
61
|
+
611, 611, 611, 611, 611, 611, 611, 584, 611, 611, 611, 611, 611, 556, 611, 556,
|
|
31
62
|
]
|
|
32
63
|
|
|
64
|
+
/**
|
|
65
|
+
* WinAnsiEncoding is Windows-1252: from 0x20 to 0x7e and from 0xa0 to 0xff
|
|
66
|
+
* the codes of Unicode, between them these characters
|
|
67
|
+
*/
|
|
68
|
+
|
|
69
|
+
const CP1252: Record<number, number> = {
|
|
70
|
+
0x20ac: 0x80, 0x201a: 0x82, 0x0192: 0x83, 0x201e: 0x84, 0x2026: 0x85, 0x2020: 0x86, 0x2021: 0x87,
|
|
71
|
+
0x02c6: 0x88, 0x2030: 0x89, 0x0160: 0x8a, 0x2039: 0x8b, 0x0152: 0x8c, 0x017d: 0x8e,
|
|
72
|
+
0x2018: 0x91, 0x2019: 0x92, 0x201c: 0x93, 0x201d: 0x94, 0x2022: 0x95, 0x2013: 0x96, 0x2014: 0x97,
|
|
73
|
+
0x02dc: 0x98, 0x2122: 0x99, 0x0161: 0x9a, 0x203a: 0x9b, 0x0153: 0x9c, 0x017e: 0x9e, 0x0178: 0x9f,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* the codes of a text in WinAnsiEncoding; a character that it does not have is a '?'
|
|
78
|
+
*/
|
|
79
|
+
|
|
80
|
+
function winAnsiCodes(str: string): number[] {
|
|
81
|
+
const codes: number[] = []
|
|
82
|
+
for (const ch of str) {
|
|
83
|
+
const code = ch.codePointAt(0)
|
|
84
|
+
if ((code >= 0x20 && code <= 0x7e) || (code >= 0xa0 && code <= 0xff)) {
|
|
85
|
+
codes.push(code)
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
codes.push(CP1252[code] ?? 0x3f)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return codes
|
|
92
|
+
}
|
|
93
|
+
|
|
33
94
|
export interface TextOptions {
|
|
34
95
|
align?: 'left' | 'center' | 'right'
|
|
35
96
|
width?: number
|
|
36
97
|
color?: string
|
|
98
|
+
bold?: boolean // Helvetica-Bold
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* m: moveto, l: lineto, c: bezier curve (two control points, then the end), h: closepath
|
|
103
|
+
*/
|
|
104
|
+
|
|
105
|
+
export type PathSegment =
|
|
106
|
+
| ['m', number, number]
|
|
107
|
+
| ['l', number, number]
|
|
108
|
+
| ['c', number, number, number, number, number, number]
|
|
109
|
+
| ['h']
|
|
110
|
+
|
|
111
|
+
export interface PDFProps {
|
|
112
|
+
title?: string // shown by the reader in place of the name of the file
|
|
113
|
+
author?: string
|
|
114
|
+
subject?: string
|
|
37
115
|
}
|
|
38
116
|
|
|
39
117
|
export interface LinkOptions {
|
|
40
118
|
underline?: string
|
|
41
119
|
}
|
|
42
120
|
|
|
121
|
+
// :: BARCODES ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
|
122
|
+
|
|
123
|
+
export interface BarcodeOptions {
|
|
124
|
+
/**
|
|
125
|
+
* the text is a GS1 element string, its AIs between parentheses:
|
|
126
|
+
* "(01)03400930000120(17)270101(10)AB12". FNC1 starts the code and
|
|
127
|
+
* ends the fields of variable length
|
|
128
|
+
*/
|
|
129
|
+
gs1?: boolean
|
|
130
|
+
color?: string // of the modules; black when not given
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const FNC1 = -1
|
|
134
|
+
|
|
135
|
+
// the AIs of fixed length, by their first two digits: they need no FNC1 after them
|
|
136
|
+
const GS1_FIXED = ['00', '01', '02', '03', '04', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '31', '32', '33', '34', '35', '36', '41']
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* the characters of the code, FNC1 for the separators of GS1
|
|
140
|
+
*/
|
|
141
|
+
|
|
142
|
+
function barcodeChars( text: string, opts: BarcodeOptions ): number[] {
|
|
143
|
+
if (!opts.gs1) {
|
|
144
|
+
return [...text].map( ch => ch.codePointAt(0) )
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const re = /\((\d{2,4})\)([^(]*)/y
|
|
148
|
+
const out: number[] = [FNC1]
|
|
149
|
+
let m: RegExpExecArray
|
|
150
|
+
let fixed = true
|
|
151
|
+
|
|
152
|
+
while ((m = re.exec(text)) !== null) {
|
|
153
|
+
if (!fixed) {
|
|
154
|
+
out.push(FNC1)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
for (const ch of m[1] + m[2]) {
|
|
158
|
+
out.push(ch.codePointAt(0))
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
fixed = GS1_FIXED.includes(m[1].substring(0, 2))
|
|
162
|
+
if (re.lastIndex === text.length) {
|
|
163
|
+
return out
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
throw new Error(`not a GS1 element string: ${text}`)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Data Matrix (ECC200): square, 10 x 10 to 26 x 26 modules, ASCII encoding
|
|
172
|
+
* (the digits by pairs). Its sizes: symbol, data codewords, error codewords
|
|
173
|
+
*/
|
|
174
|
+
|
|
175
|
+
const DM_SIZES: [number, number, number][] = [
|
|
176
|
+
[10, 3, 5], [12, 5, 7], [14, 8, 10], [16, 12, 12], [18, 18, 14],
|
|
177
|
+
[20, 22, 18], [22, 30, 20], [24, 36, 24], [26, 44, 28],
|
|
178
|
+
]
|
|
179
|
+
|
|
180
|
+
const isDigit = ( c: number ) => c >= 0x30 && c <= 0x39
|
|
181
|
+
|
|
182
|
+
function dmCodewords( chars: number[] ): number[] {
|
|
183
|
+
const cw: number[] = []
|
|
184
|
+
for (let i = 0; i < chars.length; i++) {
|
|
185
|
+
const c = chars[i]
|
|
186
|
+
if (c === FNC1) {
|
|
187
|
+
cw.push(232)
|
|
188
|
+
}
|
|
189
|
+
else if (isDigit(c) && isDigit(chars[i + 1])) {
|
|
190
|
+
cw.push(130 + (c - 0x30) * 10 + (chars[i + 1] - 0x30))
|
|
191
|
+
i++
|
|
192
|
+
}
|
|
193
|
+
else if (c < 128) {
|
|
194
|
+
cw.push(c + 1)
|
|
195
|
+
}
|
|
196
|
+
else if (c < 256) {
|
|
197
|
+
cw.push(235, c - 128 + 1) // upper shift
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
throw new Error(`character not encodable in a Data Matrix: ${String.fromCodePoint(c)}`)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return cw
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// GF(256), x^8 + x^5 + x^3 + x^2 + 1
|
|
207
|
+
const GF_EXP: number[] = []
|
|
208
|
+
const GF_LOG: number[] = []
|
|
209
|
+
{
|
|
210
|
+
let v = 1
|
|
211
|
+
for (let i = 0; i < 255; i++) {
|
|
212
|
+
GF_EXP[i] = v
|
|
213
|
+
GF_LOG[v] = i
|
|
214
|
+
v <<= 1
|
|
215
|
+
if (v & 0x100) {
|
|
216
|
+
v ^= 0x12d
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const gfMul = ( a: number, b: number ) => (a && b) ? GF_EXP[(GF_LOG[a] + GF_LOG[b]) % 255] : 0
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* the error codewords of Reed-Solomon, generator ( x - a^1 )...( x - a^n )
|
|
225
|
+
*/
|
|
226
|
+
|
|
227
|
+
function dmErrors( data: number[], n: number ): number[] {
|
|
228
|
+
let gen = [1]
|
|
229
|
+
for (let i = 1; i <= n; i++) {
|
|
230
|
+
const next = new Array(gen.length + 1).fill(0)
|
|
231
|
+
gen.forEach( ( g, j ) => {
|
|
232
|
+
next[j] ^= g
|
|
233
|
+
next[j + 1] ^= gfMul(g, GF_EXP[i])
|
|
234
|
+
} )
|
|
235
|
+
gen = next
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const ecc = new Array(n).fill(0)
|
|
239
|
+
for (const d of data) {
|
|
240
|
+
const k = d ^ ecc[0]
|
|
241
|
+
ecc.shift()
|
|
242
|
+
ecc.push(0)
|
|
243
|
+
for (let j = 0; j < n; j++) {
|
|
244
|
+
ecc[j] ^= gfMul(gen[j + 1], k)
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return ecc
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* the place of each bit in the data region (ISO 16022, annex F):
|
|
252
|
+
* 10 * codeword + bit (1 the highest), 1 for a module always dark, 0 for one always light
|
|
253
|
+
*/
|
|
254
|
+
|
|
255
|
+
function dmPlacement( nrow: number, ncol: number ): number[] {
|
|
256
|
+
const array = new Array(nrow * ncol).fill(0)
|
|
257
|
+
|
|
258
|
+
const module = ( row: number, col: number, chr: number, bit: number ) => {
|
|
259
|
+
if (row < 0) {
|
|
260
|
+
row += nrow
|
|
261
|
+
col += 4 - ((nrow + 4) % 8)
|
|
262
|
+
}
|
|
263
|
+
if (col < 0) {
|
|
264
|
+
col += ncol
|
|
265
|
+
row += 4 - ((ncol + 4) % 8)
|
|
266
|
+
}
|
|
267
|
+
array[row * ncol + col] = 10 * chr + bit
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const utah = ( row: number, col: number, chr: number ) => {
|
|
271
|
+
module(row - 2, col - 2, chr, 1); module(row - 2, col - 1, chr, 2)
|
|
272
|
+
module(row - 1, col - 2, chr, 3); module(row - 1, col - 1, chr, 4); module(row - 1, col, chr, 5)
|
|
273
|
+
module(row, col - 2, chr, 6); module(row, col - 1, chr, 7); module(row, col, chr, 8)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const corner = ( chr: number, places: number[][] ) => {
|
|
277
|
+
places.forEach( ( [r, c], i ) => module(r, c, chr, i + 1) )
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const R = nrow, C = ncol
|
|
281
|
+
let row = 4, col = 0, chr = 1
|
|
282
|
+
|
|
283
|
+
do {
|
|
284
|
+
if (row === R && col === 0) {
|
|
285
|
+
corner(chr++, [[R - 1, 0], [R - 1, 1], [R - 1, 2], [0, C - 2], [0, C - 1], [1, C - 1], [2, C - 1], [3, C - 1]])
|
|
286
|
+
}
|
|
287
|
+
if (row === R - 2 && col === 0 && C % 4) {
|
|
288
|
+
corner(chr++, [[R - 3, 0], [R - 2, 0], [R - 1, 0], [0, C - 4], [0, C - 3], [0, C - 2], [0, C - 1], [1, C - 1]])
|
|
289
|
+
}
|
|
290
|
+
if (row === R - 2 && col === 0 && C % 8 === 4) {
|
|
291
|
+
corner(chr++, [[R - 3, 0], [R - 2, 0], [R - 1, 0], [0, C - 2], [0, C - 1], [1, C - 1], [2, C - 1], [3, C - 1]])
|
|
292
|
+
}
|
|
293
|
+
if (row === R + 4 && col === 2 && !(C % 8)) {
|
|
294
|
+
corner(chr++, [[R - 1, 0], [R - 1, C - 1], [0, C - 3], [0, C - 2], [0, C - 1], [1, C - 3], [1, C - 2], [1, C - 1]])
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
do {
|
|
298
|
+
if (row < R && col >= 0 && !array[row * C + col]) {
|
|
299
|
+
utah(row, col, chr++)
|
|
300
|
+
}
|
|
301
|
+
row -= 2
|
|
302
|
+
col += 2
|
|
303
|
+
} while (row >= 0 && col < C)
|
|
304
|
+
row += 1
|
|
305
|
+
col += 3
|
|
306
|
+
|
|
307
|
+
do {
|
|
308
|
+
if (row >= 0 && col < C && !array[row * C + col]) {
|
|
309
|
+
utah(row, col, chr++)
|
|
310
|
+
}
|
|
311
|
+
row += 2
|
|
312
|
+
col -= 2
|
|
313
|
+
} while (row < R && col >= 0)
|
|
314
|
+
row += 3
|
|
315
|
+
col += 1
|
|
316
|
+
} while (row < R || col < C)
|
|
317
|
+
|
|
318
|
+
if (!array[R * C - 1]) {
|
|
319
|
+
array[R * C - 1] = array[R * C - C - 2] = 1
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return array
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* The modules of a Data Matrix (ECC200), its rows from the top: true for a dark one.
|
|
327
|
+
* The smallest square that holds the text, up to 26 x 26 (44 codewords: 44
|
|
328
|
+
* characters, 88 digits). Characters from 0 to 255. Without its quiet zone, one module wide.
|
|
329
|
+
* "G1234K7" is a 12 x 12
|
|
330
|
+
*/
|
|
331
|
+
|
|
332
|
+
function encodeDataMatrix( text: string, opts: BarcodeOptions = {} ): boolean[][] {
|
|
333
|
+
const data = dmCodewords( barcodeChars(text, opts) )
|
|
334
|
+
const size = DM_SIZES.find( s => s[1] >= data.length )
|
|
335
|
+
if (!size) {
|
|
336
|
+
throw new Error(`too long for a Data Matrix of 26 x 26: ${text}`)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const [n, ndata, necc] = size
|
|
340
|
+
|
|
341
|
+
// pads: 129, then pseudo random from their position (counted from 1)
|
|
342
|
+
const first = data.length
|
|
343
|
+
while (data.length < ndata) {
|
|
344
|
+
const p = data.length + 1
|
|
345
|
+
let v = 129
|
|
346
|
+
if (data.length > first) {
|
|
347
|
+
v = 129 + ((149 * p) % 253) + 1
|
|
348
|
+
if (v > 254) {
|
|
349
|
+
v -= 254
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
data.push(v)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const cw = [...data, ...dmErrors(data, necc)]
|
|
356
|
+
const region = n - 2
|
|
357
|
+
const place = dmPlacement(region, region)
|
|
358
|
+
|
|
359
|
+
const rows: boolean[][] = []
|
|
360
|
+
for (let r = 0; r < n; r++) {
|
|
361
|
+
const row: boolean[] = []
|
|
362
|
+
for (let c = 0; c < n; c++) {
|
|
363
|
+
if (c === 0 || r === n - 1) {
|
|
364
|
+
row.push(true) // the L
|
|
365
|
+
}
|
|
366
|
+
else if (r === 0) {
|
|
367
|
+
row.push(c % 2 === 0) // the clocks
|
|
368
|
+
}
|
|
369
|
+
else if (c === n - 1) {
|
|
370
|
+
row.push(r % 2 === 1)
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
const v = place[(r - 1) * region + (c - 1)]
|
|
374
|
+
row.push(v === 1 || (v > 1 && (cw[Math.floor(v / 10) - 1] & (1 << (8 - v % 10))) !== 0))
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
rows.push(row)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return rows
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Code 128: the 107 symbols, as the widths of their bars and spaces
|
|
385
|
+
*/
|
|
386
|
+
|
|
387
|
+
const C128 = (
|
|
388
|
+
'212222 222122 222221 121223 121322 131222 122213 122312 132212 221213 221312 231212 112232 122132 122231 113222 ' +
|
|
389
|
+
'123122 123221 223211 221132 221231 213212 223112 312131 311222 321122 321221 312212 322112 322211 212123 212321 ' +
|
|
390
|
+
'232121 111323 131123 131321 112313 132113 132311 211313 231113 231311 112133 112331 132131 113123 113321 133121 ' +
|
|
391
|
+
'313121 211331 231131 213113 213311 213131 311123 311321 331121 312113 312311 332111 314111 221411 431111 111224 ' +
|
|
392
|
+
'111422 121124 121421 141122 141221 112214 112412 122114 122411 142112 142211 241211 221114 413111 241112 134111 ' +
|
|
393
|
+
'111242 121142 121241 114212 124112 124211 411212 421112 421211 212141 214121 412121 111143 111341 131141 114113 ' +
|
|
394
|
+
'114311 411113 411311 113141 114131 311141 411131 211412 211214 211232 2331112'
|
|
395
|
+
).split(' ')
|
|
396
|
+
|
|
397
|
+
const C128_START_A = 103, C128_START_B = 104, C128_START_C = 105
|
|
398
|
+
const C128_TO_A = 101, C128_TO_B = 100, C128_TO_C = 99, C128_FNC1 = 102
|
|
399
|
+
|
|
400
|
+
function c128Values( chars: number[] ): number[] {
|
|
401
|
+
const digitsAt = ( i: number ) => {
|
|
402
|
+
let n = 0
|
|
403
|
+
while (i + n < chars.length && isDigit(chars[i + n])) {
|
|
404
|
+
n++
|
|
405
|
+
}
|
|
406
|
+
return n
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
for (const c of chars) {
|
|
410
|
+
if (c !== FNC1 && (c < 0 || c > 127)) {
|
|
411
|
+
throw new Error(`character not encodable in a Code 128: ${String.fromCodePoint(c)}`)
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// in C, the digits by pairs; else A for the controls, B for the rest
|
|
416
|
+
const textSet = ( c: number ) => (c >= 0 && c < 32) ? 'A' : 'B'
|
|
417
|
+
const values: number[] = []
|
|
418
|
+
let set: string
|
|
419
|
+
|
|
420
|
+
let i = 0
|
|
421
|
+
const lead = chars[0] === FNC1 ? 1 : 0
|
|
422
|
+
const d0 = digitsAt(lead)
|
|
423
|
+
if (d0 >= 4 || (d0 >= 2 && lead + d0 === chars.length && d0 % 2 === 0)) {
|
|
424
|
+
set = 'C'
|
|
425
|
+
values.push(C128_START_C)
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
set = textSet(chars.find( c => c !== FNC1 ) ?? 0x20)
|
|
429
|
+
values.push(set === 'A' ? C128_START_A : C128_START_B)
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
while (i < chars.length) {
|
|
433
|
+
const c = chars[i]
|
|
434
|
+
if (c === FNC1) {
|
|
435
|
+
values.push(C128_FNC1)
|
|
436
|
+
i++
|
|
437
|
+
continue
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (set === 'C') {
|
|
441
|
+
if (digitsAt(i) >= 2) {
|
|
442
|
+
values.push((c - 0x30) * 10 + (chars[i + 1] - 0x30))
|
|
443
|
+
i += 2
|
|
444
|
+
continue
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
set = textSet(c)
|
|
448
|
+
values.push(set === 'A' ? C128_TO_A : C128_TO_B)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// a run of digits worth C: 4 at the end, 6 elsewhere (C costs a switch to it, and one back)
|
|
452
|
+
const d = digitsAt(i)
|
|
453
|
+
if (d >= 6 || (d >= 4 && i + d === chars.length)) {
|
|
454
|
+
if (d % 2) {
|
|
455
|
+
values.push(c - 32) // the odd digit, in A or B
|
|
456
|
+
i++
|
|
457
|
+
}
|
|
458
|
+
set = 'C'
|
|
459
|
+
values.push(C128_TO_C)
|
|
460
|
+
continue
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const want = (set === 'A' && c >= 96) ? 'B' : (set === 'B' && c < 32) ? 'A' : set
|
|
464
|
+
if (want !== set) {
|
|
465
|
+
set = want
|
|
466
|
+
values.push(set === 'A' ? C128_TO_A : C128_TO_B)
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
values.push(c < 32 ? c + 64 : c - 32)
|
|
470
|
+
i++
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
let sum = values[0]
|
|
474
|
+
for (let k = 1; k < values.length; k++) {
|
|
475
|
+
sum += values[k] * k
|
|
476
|
+
}
|
|
477
|
+
values.push(sum % 103)
|
|
478
|
+
|
|
479
|
+
values.push(106) // stop
|
|
480
|
+
return values
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* The modules of a Code 128, from the left: true for a bar. Its sets A, B and C
|
|
485
|
+
* are chosen for the shortest code. ASCII only. Without its quiet zone (10 modules on each side).
|
|
486
|
+
*/
|
|
487
|
+
|
|
488
|
+
function encodeCode128( text: string, opts: BarcodeOptions = {} ): boolean[] {
|
|
489
|
+
const modules: boolean[] = []
|
|
490
|
+
for (const v of c128Values( barcodeChars(text, opts) )) {
|
|
491
|
+
[...C128[v]].forEach( ( w, k ) => {
|
|
492
|
+
for (let n = +w; n > 0; n--) {
|
|
493
|
+
modules.push(k % 2 === 0)
|
|
494
|
+
}
|
|
495
|
+
} )
|
|
496
|
+
}
|
|
497
|
+
return modules
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
export type BarcodeType = 'datamatrix' | 'code128'
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* the rows of the modules (one for a Code 128), and the runs of dark
|
|
504
|
+
* modules on them: [row, column, length]
|
|
505
|
+
*/
|
|
506
|
+
|
|
507
|
+
function barcodeRuns( type: BarcodeType, text: string, opts: BarcodeOptions ) {
|
|
508
|
+
const rows = type === 'datamatrix' ? encodeDataMatrix(text, opts) : [encodeCode128(text, opts)]
|
|
509
|
+
const runs: [number, number, number][] = []
|
|
510
|
+
|
|
511
|
+
rows.forEach( ( row, r ) => {
|
|
512
|
+
let c = 0
|
|
513
|
+
while (c < row.length) {
|
|
514
|
+
if (!row[c]) {
|
|
515
|
+
c++
|
|
516
|
+
continue
|
|
517
|
+
}
|
|
518
|
+
let end = c
|
|
519
|
+
while (end < row.length && row[end]) {
|
|
520
|
+
end++
|
|
521
|
+
}
|
|
522
|
+
runs.push([r, c, end - c])
|
|
523
|
+
c = end
|
|
524
|
+
}
|
|
525
|
+
} )
|
|
526
|
+
|
|
527
|
+
return { rows: rows.length, cols: rows[0].length, runs }
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
export interface BarcodeImageOptions extends BarcodeOptions {
|
|
531
|
+
module?: number // the size of a module, in px; 4 when not given
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* A barcode as an image, for the screen: "data:image/svg+xml,...", for the
|
|
536
|
+
* src of an Image or an Icon. Drawn on white, with its quiet zone (1 module
|
|
537
|
+
* around a Data Matrix, 10 on the sides of a Code 128): it can be read on a dark theme too.
|
|
538
|
+
* A Code 128 is 50 modules high.
|
|
539
|
+
*
|
|
540
|
+
* @example
|
|
541
|
+
* new Image( { src: renderBarcodeImage( "datamatrix", "G1234K7" ) } )
|
|
542
|
+
* new Image( { src: renderBarcodeImage( "code128", "(01)03400930000120(17)270101", { gs1: true, module: 2 } ) } )
|
|
543
|
+
*/
|
|
544
|
+
|
|
545
|
+
export function renderBarcodeImage( type: BarcodeType, text: string, opts: BarcodeImageOptions = {} ): string {
|
|
546
|
+
const { rows, cols, runs } = barcodeRuns(type, text, opts)
|
|
547
|
+
const qx = type === 'datamatrix' ? 1 : 10
|
|
548
|
+
const qy = type === 'datamatrix' ? 1 : 0
|
|
549
|
+
const h = type === 'datamatrix' ? 1 : 50 // height of a row, in modules
|
|
550
|
+
const W = cols + 2 * qx
|
|
551
|
+
const H = rows * h + 2 * qy
|
|
552
|
+
const m = opts.module ?? 4
|
|
553
|
+
|
|
554
|
+
const svg = new SvgGroup('svg')
|
|
555
|
+
svg.setAttr('viewBox', `0 0 ${W} ${H}`)
|
|
556
|
+
svg.setAttr('width', `${W * m}`)
|
|
557
|
+
svg.setAttr('height', `${H * m}`)
|
|
558
|
+
|
|
559
|
+
const bld = svg.append(new SvgBuilder())
|
|
560
|
+
bld.rect(0, 0, W, H).fill('#ffffff')
|
|
561
|
+
|
|
562
|
+
const path = bld.path()
|
|
563
|
+
for (const [r, c, len] of runs) {
|
|
564
|
+
const x = qx + c, y = qy + r * h
|
|
565
|
+
path.moveTo(x, y).lineTo(x + len, y).lineTo(x + len, y + h).lineTo(x, y + h).closePath()
|
|
566
|
+
}
|
|
567
|
+
path.fill(opts.color ?? '#000000').antiAlias(false)
|
|
568
|
+
|
|
569
|
+
return 'data:image/svg+xml,' + encodeURIComponent(new XMLSerializer().serializeToString(svg.getDom()))
|
|
570
|
+
}
|
|
571
|
+
|
|
43
572
|
type PDFValue = null | boolean | number | string | PDFValue[] | Ref | { [key: string]: PDFValue }
|
|
44
573
|
|
|
45
574
|
interface PDFObject {
|
|
@@ -48,12 +577,11 @@ interface PDFObject {
|
|
|
48
577
|
stream: Uint8Array | null
|
|
49
578
|
}
|
|
50
579
|
|
|
51
|
-
export function measureText(str: string, size: number): number {
|
|
580
|
+
export function measureText(str: string, size: number, bold = false): number {
|
|
581
|
+
const widths = bold ? WIDTHS_BOLD : WIDTHS
|
|
52
582
|
let width = 0
|
|
53
|
-
for (
|
|
54
|
-
|
|
55
|
-
const w = (code >= 32 && code <= 126) ? WIDTHS[code - 32] : 556
|
|
56
|
-
width += w
|
|
583
|
+
for (const code of winAnsiCodes(str)) {
|
|
584
|
+
width += widths[code - 32]
|
|
57
585
|
}
|
|
58
586
|
return (width * size) / 1000
|
|
59
587
|
}
|
|
@@ -62,9 +590,36 @@ function fx( v:number, n:number ) {
|
|
|
62
590
|
return (+v.toFixed(n)).toString();
|
|
63
591
|
}
|
|
64
592
|
|
|
593
|
+
// the components of a color go from 0 to 1
|
|
65
594
|
function formatColor( clr: Color, suffix: string ) : string {
|
|
66
595
|
const rgb = clr.toNumber( );
|
|
67
|
-
return `${fx((rgb>>16)&0xff,3)} ${fx((rgb>>8)&0xff,3)} ${fx(rgb&0xff,3)} ${suffix}`;
|
|
596
|
+
return `${fx(((rgb>>16)&0xff)/255,3)} ${fx(((rgb>>8)&0xff)/255,3)} ${fx((rgb&0xff)/255,3)} ${suffix}`;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function hex( codes: number[] ): string {
|
|
600
|
+
return codes.map( c => c.toString(16).padStart(2,'0') ).join('');
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* a text of the content, in WinAnsiEncoding: written in hex, the content
|
|
605
|
+
* stays in ASCII
|
|
606
|
+
*/
|
|
607
|
+
|
|
608
|
+
function pdfWinAnsi(str: string): string {
|
|
609
|
+
return '<' + hex( winAnsiCodes(str) ) + '>';
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* a text of the document (its title...): in UTF-16BE, any character passes
|
|
614
|
+
*/
|
|
615
|
+
|
|
616
|
+
function pdfTextString(str: string): string {
|
|
617
|
+
const codes = [0xfe, 0xff];
|
|
618
|
+
for (let i = 0; i < str.length; i++) {
|
|
619
|
+
const c = str.charCodeAt(i);
|
|
620
|
+
codes.push( c >> 8, c & 0xff );
|
|
621
|
+
}
|
|
622
|
+
return '<' + hex( codes ) + '>';
|
|
68
623
|
}
|
|
69
624
|
|
|
70
625
|
|
|
@@ -98,8 +653,8 @@ function serialize(val: PDFValue): string {
|
|
|
98
653
|
return val // name
|
|
99
654
|
}
|
|
100
655
|
|
|
101
|
-
if (val.startsWith('(')) {
|
|
102
|
-
return val // already escaped string
|
|
656
|
+
if (val.startsWith('(') || val.startsWith('<')) {
|
|
657
|
+
return val // already escaped string, or hex string
|
|
103
658
|
}
|
|
104
659
|
|
|
105
660
|
return pdfString(val)
|
|
@@ -124,18 +679,34 @@ function serialize(val: PDFValue): string {
|
|
|
124
679
|
return String(val)
|
|
125
680
|
}
|
|
126
681
|
|
|
682
|
+
/**
|
|
683
|
+
* zlib, as FlateDecode reads it
|
|
684
|
+
*/
|
|
685
|
+
|
|
686
|
+
async function deflate(data: Uint8Array): Promise<Uint8Array> {
|
|
687
|
+
const stream = new Blob([data as BlobPart]).stream().pipeThrough(new CompressionStream('deflate'))
|
|
688
|
+
return new Uint8Array(await new Response(stream).arrayBuffer())
|
|
689
|
+
}
|
|
690
|
+
|
|
127
691
|
// @internal
|
|
128
692
|
class Ref {
|
|
129
693
|
id: number
|
|
130
694
|
constructor(id: number) { this.id = id }
|
|
131
695
|
}
|
|
132
696
|
|
|
133
|
-
interface PageContext {
|
|
697
|
+
export interface PageContext {
|
|
134
698
|
text(str: string, x: number, y: number, size: number, opts?: TextOptions): void;
|
|
135
699
|
rect(x: number, y: number, w: number, h: number, fill: string): void;
|
|
136
700
|
line(x1: number, y1: number, x2: number, y2: number, stroke: string, lineWidth?: number): void;
|
|
137
701
|
image(jpegBytes: Uint8Array, x: number, y: number, w: number, h: number): void;
|
|
138
702
|
link(url: string, x: number, y: number, w: number, h: number, opts?: LinkOptions): void;
|
|
703
|
+
/** filled with fill, its outline drawn with stroke; null for none of them */
|
|
704
|
+
path(segments: PathSegment[], fill: string, stroke?: string, lineWidth?: number): void;
|
|
705
|
+
/**
|
|
706
|
+
* a Data Matrix: the square of side min( w, h ), (x, y) its bottom left corner;
|
|
707
|
+
* a Code 128: its bars on w x h. Without the quiet zone, left to the caller
|
|
708
|
+
*/
|
|
709
|
+
barcode(type: BarcodeType, text: string, x: number, y: number, w: number, h: number, opts?: BarcodeOptions): void;
|
|
139
710
|
}
|
|
140
711
|
|
|
141
712
|
export class X4PDFBuilder {
|
|
@@ -143,6 +714,11 @@ export class X4PDFBuilder {
|
|
|
143
714
|
private objects: PDFObject[] = [];
|
|
144
715
|
private pages: Ref[] = [];
|
|
145
716
|
private nextId = 1;
|
|
717
|
+
private props: PDFProps;
|
|
718
|
+
|
|
719
|
+
constructor( props: PDFProps = {} ) {
|
|
720
|
+
this.props = props;
|
|
721
|
+
}
|
|
146
722
|
|
|
147
723
|
private _addObject(dict: Record<string, PDFValue>, streamBytes: Uint8Array = null): Ref {
|
|
148
724
|
const id = this.nextId++;
|
|
@@ -159,11 +735,11 @@ export class X4PDFBuilder {
|
|
|
159
735
|
let imageCount = 0;
|
|
160
736
|
|
|
161
737
|
const text = (str: string, x: number, y: number, size: number, opts: TextOptions = {}) => {
|
|
162
|
-
const { align = 'left', width: boxWidth, color = '#000000' } = opts;
|
|
738
|
+
const { align = 'left', width: boxWidth, color = '#000000', bold = false } = opts;
|
|
163
739
|
|
|
164
740
|
let tx = x;
|
|
165
741
|
if (align !== 'left' && boxWidth !== undefined) {
|
|
166
|
-
const textWidth = measureText(str, size);
|
|
742
|
+
const textWidth = measureText(str, size, bold);
|
|
167
743
|
if (align === 'center') {
|
|
168
744
|
tx = x + (boxWidth - textWidth) / 2;
|
|
169
745
|
}
|
|
@@ -173,36 +749,49 @@ export class X4PDFBuilder {
|
|
|
173
749
|
}
|
|
174
750
|
}
|
|
175
751
|
|
|
176
|
-
|
|
177
|
-
if (rgb) {
|
|
178
|
-
ops.push( formatColor(rgb, 'rg') );
|
|
179
|
-
}
|
|
752
|
+
ops.push( formatColor(new Color(color), 'rg') );
|
|
180
753
|
|
|
181
754
|
ops.push('BT')
|
|
182
|
-
ops.push(
|
|
755
|
+
ops.push(`/${bold ? 'F2' : 'F1'} ${size} Tf`)
|
|
183
756
|
ops.push(`${fx(tx,2)} ${fx(y,2)} Td`)
|
|
184
|
-
ops.push(`${
|
|
757
|
+
ops.push(`${pdfWinAnsi(str)} Tj`)
|
|
185
758
|
ops.push('ET')
|
|
186
759
|
}
|
|
187
|
-
|
|
760
|
+
|
|
188
761
|
const rect = (x: number, y: number, w: number, h: number, fill: string) => {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
ops.push(`${fx(x,2)} ${fx(y,2)} ${fx(w,2)} ${fx(h,2)} re`);
|
|
193
|
-
ops.push('f');
|
|
194
|
-
}
|
|
762
|
+
ops.push( formatColor(new Color(fill), 'rg' ) );
|
|
763
|
+
ops.push(`${fx(x,2)} ${fx(y,2)} ${fx(w,2)} ${fx(h,2)} re`);
|
|
764
|
+
ops.push('f');
|
|
195
765
|
}
|
|
196
|
-
|
|
766
|
+
|
|
197
767
|
const line = (x1: number, y1: number, x2: number, y2: number, stroke: string, lineWidth: number = 1) => {
|
|
198
|
-
|
|
199
|
-
|
|
768
|
+
ops.push(`${fx(lineWidth,2)} w`);
|
|
769
|
+
ops.push( formatColor( new Color(stroke),'RG') );
|
|
770
|
+
ops.push(`${fx(x1,2)} ${fx(y1,2)} m`);
|
|
771
|
+
ops.push(`${fx(x2,2)} ${fx(y2,2)} l`);
|
|
772
|
+
ops.push('S')
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const path = (segments: PathSegment[], fill: string, stroke?: string, lineWidth: number = 1) => {
|
|
776
|
+
if (!fill && !stroke) {
|
|
777
|
+
return
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
if (fill) {
|
|
781
|
+
ops.push( formatColor(new Color(fill), 'rg') );
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
if (stroke) {
|
|
200
785
|
ops.push(`${fx(lineWidth,2)} w`);
|
|
201
|
-
ops.push( formatColor(
|
|
202
|
-
ops.push(`${fx(x1,2)} ${fx(y1,2)} m`);
|
|
203
|
-
ops.push(`${fx(x2,2)} ${fx(y2,2)} l`);
|
|
204
|
-
ops.push('S')
|
|
786
|
+
ops.push( formatColor(new Color(stroke), 'RG') );
|
|
205
787
|
}
|
|
788
|
+
|
|
789
|
+
for (const [op, ...coords] of segments) {
|
|
790
|
+
ops.push( [...coords.map( v => fx(v,2) ), op].join(' ') );
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// f: fill, S: stroke, B: both
|
|
794
|
+
ops.push( fill ? (stroke ? 'B' : 'f') : 'S' );
|
|
206
795
|
}
|
|
207
796
|
|
|
208
797
|
const image = (jpegBytes: Uint8Array, x: number, y: number, w: number, h: number) => {
|
|
@@ -238,23 +827,43 @@ export class X4PDFBuilder {
|
|
|
238
827
|
const link = (url: string, x: number, y: number, w: number, h: number, opts: LinkOptions = {}) => {
|
|
239
828
|
links.push({ url, rect: [x, y, x + w, y + h] })
|
|
240
829
|
if (opts.underline) {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
ops.push(`${fx(x + w,2)} ${fx(y + 2,2)} l`);
|
|
247
|
-
ops.push('S')
|
|
248
|
-
}
|
|
830
|
+
ops.push(`0.75 w`)
|
|
831
|
+
ops.push( formatColor(new Color(opts.underline), 'RG' ) );
|
|
832
|
+
ops.push(`${fx(x,2)} ${fx(y + 2,2)} m`);
|
|
833
|
+
ops.push(`${fx(x + w,2)} ${fx(y + 2,2)} l`);
|
|
834
|
+
ops.push('S')
|
|
249
835
|
}
|
|
250
836
|
}
|
|
251
837
|
|
|
838
|
+
const barcode = (type: BarcodeType, str: string, x: number, y: number, w: number, h: number, opts: BarcodeOptions = {}) => {
|
|
839
|
+
const { rows, cols, runs } = barcodeRuns(type, str, opts)
|
|
840
|
+
|
|
841
|
+
// a module: square in a Data Matrix, the whole height in a Code 128
|
|
842
|
+
let mw: number, mh: number
|
|
843
|
+
if (type === 'datamatrix') {
|
|
844
|
+
mw = mh = Math.min(w, h) / cols
|
|
845
|
+
}
|
|
846
|
+
else {
|
|
847
|
+
mw = w / cols
|
|
848
|
+
mh = h
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
const top = y + rows * mh
|
|
852
|
+
ops.push( formatColor(new Color(opts.color ?? '#000000'), 'rg') )
|
|
853
|
+
for (const [r, c, len] of runs) {
|
|
854
|
+
ops.push(`${fx(x + c * mw,3)} ${fx(top - (r + 1) * mh,3)} ${fx(len * mw,3)} ${fx(mh,3)} re`)
|
|
855
|
+
}
|
|
856
|
+
ops.push('f')
|
|
857
|
+
}
|
|
858
|
+
|
|
252
859
|
const ctx: PageContext = {
|
|
253
860
|
text,
|
|
254
861
|
rect,
|
|
255
862
|
line,
|
|
256
863
|
image,
|
|
257
|
-
link
|
|
864
|
+
link,
|
|
865
|
+
path,
|
|
866
|
+
barcode
|
|
258
867
|
}
|
|
259
868
|
|
|
260
869
|
let width: number
|
|
@@ -295,7 +904,7 @@ export class X4PDFBuilder {
|
|
|
295
904
|
MediaBox: [0, 0, width, height],
|
|
296
905
|
Contents: contentRef,
|
|
297
906
|
Resources: {
|
|
298
|
-
Font: { F1: null },
|
|
907
|
+
Font: { F1: null, F2: null },
|
|
299
908
|
XObject: Object.keys(xobjects).length > 0 ? xobjects : undefined
|
|
300
909
|
},
|
|
301
910
|
Annots: annots.length > 0 ? annots : undefined
|
|
@@ -304,6 +913,27 @@ export class X4PDFBuilder {
|
|
|
304
913
|
this.pages.push(pageRef)
|
|
305
914
|
}
|
|
306
915
|
|
|
916
|
+
/**
|
|
917
|
+
* the document, its streams compressed (FlateDecode). The images, already
|
|
918
|
+
* JPEG, are left as they are
|
|
919
|
+
*/
|
|
920
|
+
|
|
921
|
+
async buildAsync(): Promise<Uint8Array> {
|
|
922
|
+
for (const obj of this.objects) {
|
|
923
|
+
if (obj.stream && !obj.dict.Filter) {
|
|
924
|
+
obj.stream = await deflate(obj.stream)
|
|
925
|
+
obj.dict.Filter = '/FlateDecode'
|
|
926
|
+
obj.dict.Length = obj.stream.length
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
return this.build()
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/**
|
|
934
|
+
* the document, its streams not compressed
|
|
935
|
+
*/
|
|
936
|
+
|
|
307
937
|
build(): Uint8Array {
|
|
308
938
|
const fontRef = this._addObject({
|
|
309
939
|
Type: '/Font',
|
|
@@ -312,6 +942,13 @@ export class X4PDFBuilder {
|
|
|
312
942
|
Encoding: '/WinAnsiEncoding'
|
|
313
943
|
})
|
|
314
944
|
|
|
945
|
+
const boldRef = this._addObject({
|
|
946
|
+
Type: '/Font',
|
|
947
|
+
Subtype: '/Type1',
|
|
948
|
+
BaseFont: '/Helvetica-Bold',
|
|
949
|
+
Encoding: '/WinAnsiEncoding'
|
|
950
|
+
})
|
|
951
|
+
|
|
315
952
|
const pagesRef = this._addObject({
|
|
316
953
|
Type: '/Pages',
|
|
317
954
|
Kids: this.pages,
|
|
@@ -323,20 +960,33 @@ export class X4PDFBuilder {
|
|
|
323
960
|
obj.dict.Parent = pagesRef
|
|
324
961
|
const resources = obj.dict.Resources as Record<string, PDFValue> | undefined
|
|
325
962
|
if (resources?.Font) {
|
|
326
|
-
|
|
963
|
+
const fonts = resources.Font as Record<string, PDFValue>
|
|
964
|
+
fonts.F1 = fontRef
|
|
965
|
+
fonts.F2 = boldRef
|
|
327
966
|
}
|
|
328
967
|
}
|
|
329
968
|
}
|
|
330
969
|
|
|
970
|
+
// printed at its size, not fitted to the paper
|
|
331
971
|
const catalogRef = this._addObject({
|
|
332
972
|
Type: '/Catalog',
|
|
333
|
-
Pages: pagesRef
|
|
973
|
+
Pages: pagesRef,
|
|
974
|
+
ViewerPreferences: { PrintScaling: '/None', DisplayDocTitle: true }
|
|
975
|
+
})
|
|
976
|
+
|
|
977
|
+
const { title, author, subject } = this.props
|
|
978
|
+
const infoRef = this._addObject({
|
|
979
|
+
Title: title ? pdfTextString(title) : undefined,
|
|
980
|
+
Author: author ? pdfTextString(author) : undefined,
|
|
981
|
+
Subject: subject ? pdfTextString(subject) : undefined,
|
|
982
|
+
Producer: pdfTextString('x4js')
|
|
334
983
|
})
|
|
335
984
|
|
|
336
985
|
const parts: (string | Uint8Array)[] = []
|
|
337
986
|
const offsets: number[] = []
|
|
338
987
|
|
|
339
|
-
|
|
988
|
+
// 1.6: PrintScaling
|
|
989
|
+
parts.push('%PDF-1.6\n%\xFF\xFF\xFF\xFF\n')
|
|
340
990
|
|
|
341
991
|
for (const obj of this.objects) {
|
|
342
992
|
offsets[obj.id] = parts.reduce((sum, p) => sum + (typeof p === 'string' ? new TextEncoder().encode(p).length : p.length), 0)
|
|
@@ -363,7 +1013,7 @@ export class X4PDFBuilder {
|
|
|
363
1013
|
}
|
|
364
1014
|
parts.push(xref)
|
|
365
1015
|
|
|
366
|
-
parts.push(`trailer\n${serialize({ Size: this.objects.length + 1, Root: catalogRef })}\n`)
|
|
1016
|
+
parts.push(`trailer\n${serialize({ Size: this.objects.length + 1, Root: catalogRef, Info: infoRef })}\n`)
|
|
367
1017
|
parts.push(`startxref\n${xrefOffset}\n%%EOF\n`)
|
|
368
1018
|
|
|
369
1019
|
const totalLength = parts.reduce((sum, p) => sum + (typeof p === 'string' ? new TextEncoder().encode(p).length : p.length), 0)
|