x4js 2.3.18 → 2.3.20

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 (38) hide show
  1. package/aicli.md +3 -3
  2. package/aicontext.md +51 -23
  3. package/cli/config.mjs +14 -7
  4. package/cli/copy-plugin.mjs +8 -0
  5. package/cli/create.mjs +8 -2
  6. package/cli/dev.mjs +4 -1
  7. package/cli/less-plugin.mjs +5 -1
  8. package/cli/main.mjs +15 -5
  9. package/cli/rawfile-plugin.mjs +23 -3
  10. package/cli/templates.mjs +5 -1
  11. package/package.json +1 -1
  12. package/src/components/calendar/calendar.module.scss +16 -1
  13. package/src/components/calendar/calendar.ts +52 -9
  14. package/src/components/checkbox/checkbox.ts +1 -0
  15. package/src/components/combobox/combobox.ts +51 -23
  16. package/src/components/dialog/dialog.ts +4 -1
  17. package/src/components/form/form.ts +9 -5
  18. package/src/components/gridview/gridview.ts +2 -1
  19. package/src/components/input/input.ts +63 -38
  20. package/src/components/keyboard/keyboard.module.scss +35 -4
  21. package/src/components/keyboard/keyboard.ts +120 -21
  22. package/src/components/listbox/listbox.ts +6 -1
  23. package/src/components/messages/messages.ts +12 -3
  24. package/src/components/popup/popup.ts +19 -20
  25. package/src/components/radio/radio.ts +8 -4
  26. package/src/components/rating/rating.ts +38 -48
  27. package/src/components/slider/slider.ts +38 -8
  28. package/src/components/spreadsheet/spreadsheet.ts +140 -131
  29. package/src/components/switch/switch.ts +2 -1
  30. package/src/components/textarea/textarea.module.scss +2 -4
  31. package/src/components/textarea/textarea.ts +2 -1
  32. package/src/components/textedit/textedit.ts +12 -2
  33. package/src/core/core_application.ts +8 -4
  34. package/src/core/core_data.ts +64 -26
  35. package/src/core/core_dom.ts +9 -1
  36. package/src/core/core_pdf.ts +1200 -46
  37. package/src/core/core_router.ts +42 -44
  38. package/src/core/core_state.ts +22 -5
@@ -19,27 +19,1016 @@
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
+
572
+ // :: SVG :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
573
+
574
+ /**
575
+ * the endpoint arc of SVG as Bézier curves, a quarter of turn at most each
576
+ * (SVG 1.1, appendix F.6.5)
577
+ */
578
+
579
+ function arcToCurves( x1: number, y1: number, rx: number, ry: number, phi: number, large: boolean, sweep: boolean, x2: number, y2: number ): PathSegment[] {
580
+ if (x1 === x2 && y1 === y2) {
581
+ return []
582
+ }
583
+
584
+ rx = Math.abs(rx)
585
+ ry = Math.abs(ry)
586
+ if (!rx || !ry) {
587
+ return [['l', x2, y2]]
588
+ }
589
+
590
+ const cos = Math.cos(phi * Math.PI / 180), sin = Math.sin(phi * Math.PI / 180)
591
+ const dx = (x1 - x2) / 2, dy = (y1 - y2) / 2
592
+ const xp = cos * dx + sin * dy, yp = -sin * dx + cos * dy
593
+
594
+ // radii too small: scaled up
595
+ const lambda = (xp * xp) / (rx * rx) + (yp * yp) / (ry * ry)
596
+ if (lambda > 1) {
597
+ rx *= Math.sqrt(lambda)
598
+ ry *= Math.sqrt(lambda)
599
+ }
600
+
601
+ const num = rx * rx * ry * ry - rx * rx * yp * yp - ry * ry * xp * xp
602
+ const den = rx * rx * yp * yp + ry * ry * xp * xp
603
+ const k = (large === sweep ? -1 : 1) * Math.sqrt(Math.max(0, num / den))
604
+ const cxp = k * rx * yp / ry, cyp = -k * ry * xp / rx
605
+ const cx = cos * cxp - sin * cyp + (x1 + x2) / 2
606
+ const cy = sin * cxp + cos * cyp + (y1 + y2) / 2
607
+
608
+ const angle = ( ux: number, uy: number, vx: number, vy: number ) => {
609
+ const a = Math.acos(Math.max(-1, Math.min(1, (ux * vx + uy * vy) / (Math.hypot(ux, uy) * Math.hypot(vx, vy)))))
610
+ return (ux * vy - uy * vx < 0) ? -a : a
611
+ }
612
+
613
+ const t1 = angle(1, 0, (xp - cxp) / rx, (yp - cyp) / ry)
614
+ let dt = angle((xp - cxp) / rx, (yp - cyp) / ry, (-xp - cxp) / rx, (-yp - cyp) / ry)
615
+ if (!sweep && dt > 0) {
616
+ dt -= 2 * Math.PI
617
+ }
618
+ else if (sweep && dt < 0) {
619
+ dt += 2 * Math.PI
620
+ }
621
+
622
+ const n = Math.ceil(Math.abs(dt) / (Math.PI / 2) - 1e-9)
623
+ const step = dt / n
624
+ const kappa = 4 / 3 * Math.tan(step / 4)
625
+
626
+ // a point of the ellipse, and its derivative
627
+ const pt = ( t: number ) => [cx + rx * Math.cos(t) * cos - ry * Math.sin(t) * sin, cy + rx * Math.cos(t) * sin + ry * Math.sin(t) * cos]
628
+ const dv = ( t: number ) => [-rx * Math.sin(t) * cos - ry * Math.cos(t) * sin, -rx * Math.sin(t) * sin + ry * Math.cos(t) * cos]
629
+
630
+ const out: PathSegment[] = []
631
+ for (let i = 0; i < n; i++) {
632
+ const a = t1 + i * step, b = a + step
633
+ const [ax, ay] = pt(a), [bx, by] = pt(b)
634
+ const [adx, ady] = dv(a), [bdx, bdy] = dv(b)
635
+ const last = i === n - 1
636
+ out.push(['c', ax + kappa * adx, ay + kappa * ady, bx - kappa * bdx, by - kappa * bdy, last ? x2 : bx, last ? y2 : by])
637
+ }
638
+ return out
639
+ }
640
+
641
+ /**
642
+ * The "d" of a SVG path as absolute segments, in its own coordinates (y goes down).
643
+ * M L H V C S Q T A Z, absolute and relative; Q and T become exact cubic curves,
644
+ * A becomes Bézier curves. Another command throws: better than a drawing cut short.
645
+ */
646
+
647
+ function parseSvgPath( d: string ): PathSegment[] {
648
+ let pos = 0
649
+
650
+ const skip = () => {
651
+ while (pos < d.length && /[\s,]/.test(d[pos])) {
652
+ pos++
653
+ }
654
+ }
655
+
656
+ const number = () => {
657
+ skip()
658
+ const m = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/.exec(d.substring(pos))
659
+ if (!m) {
660
+ throw new Error(`svg path: number expected at ${pos}: ${d.substring(pos, pos + 20)}`)
661
+ }
662
+ pos += m[0].length
663
+ return parseFloat(m[0])
664
+ }
665
+
666
+ // the flags of an arc are one digit, without a separator: "a1 1 0 001 1"
667
+ const flag = () => {
668
+ skip()
669
+ const f = d[pos++]
670
+ if (f !== '0' && f !== '1') {
671
+ throw new Error(`svg path: flag expected at ${pos - 1}`)
672
+ }
673
+ return f === '1'
674
+ }
675
+
676
+ const out: PathSegment[] = []
677
+ let cx = 0, cy = 0 // current point
678
+ let sx = 0, sy = 0 // start of the subpath
679
+ let qx = 0, qy = 0 // last control point, reflected by S and T
680
+ let last = '' // last command, uppercase
681
+ let cmd = ''
682
+
683
+ const curve = ( x1: number, y1: number, x2: number, y2: number, x: number, y: number ) => {
684
+ out.push(['c', x1, y1, x2, y2, x, y])
685
+ qx = x2; qy = y2
686
+ cx = x; cy = y
687
+ }
688
+
689
+ const quad = ( x1: number, y1: number, x: number, y: number ) => {
690
+ out.push(['c', cx + 2 / 3 * (x1 - cx), cy + 2 / 3 * (y1 - cy), x + 2 / 3 * (x1 - x), y + 2 / 3 * (y1 - y), x, y])
691
+ qx = x1; qy = y1
692
+ cx = x; cy = y
693
+ }
694
+
695
+ skip()
696
+ while (pos < d.length) {
697
+ if (/[a-zA-Z]/.test(d[pos])) {
698
+ cmd = d[pos++]
699
+ }
700
+ else if (!cmd) {
701
+ throw new Error(`svg path: command expected at ${pos}`)
702
+ }
703
+
704
+ const rel = cmd === cmd.toLowerCase()
705
+ const ox = rel ? cx : 0, oy = rel ? cy : 0
706
+ const C = cmd.toUpperCase()
707
+
708
+ switch (C) {
709
+ case 'Z':
710
+ out.push(['h'])
711
+ cx = sx; cy = sy
712
+ break
713
+
714
+ case 'M':
715
+ cx = sx = number() + ox
716
+ cy = sy = number() + oy
717
+ out.push(['m', cx, cy])
718
+ cmd = rel ? 'l' : 'L' // the next pairs are lines
719
+ break
720
+
721
+ case 'L':
722
+ cx = number() + ox
723
+ cy = number() + oy
724
+ out.push(['l', cx, cy])
725
+ break
726
+
727
+ case 'H':
728
+ cx = number() + ox
729
+ out.push(['l', cx, cy])
730
+ break
731
+
732
+ case 'V':
733
+ cy = number() + oy
734
+ out.push(['l', cx, cy])
735
+ break
736
+
737
+ case 'C':
738
+ curve(number() + ox, number() + oy, number() + ox, number() + oy, number() + ox, number() + oy)
739
+ break
740
+
741
+ case 'S': {
742
+ const x1 = (last === 'C' || last === 'S') ? 2 * cx - qx : cx
743
+ const y1 = (last === 'C' || last === 'S') ? 2 * cy - qy : cy
744
+ curve(x1, y1, number() + ox, number() + oy, number() + ox, number() + oy)
745
+ break
746
+ }
747
+
748
+ case 'Q':
749
+ quad(number() + ox, number() + oy, number() + ox, number() + oy)
750
+ break
751
+
752
+ case 'T': {
753
+ const x1 = (last === 'Q' || last === 'T') ? 2 * cx - qx : cx
754
+ const y1 = (last === 'Q' || last === 'T') ? 2 * cy - qy : cy
755
+ quad(x1, y1, number() + ox, number() + oy)
756
+ break
757
+ }
758
+
759
+ case 'A': {
760
+ const rx = number(), ry = number(), phi = number()
761
+ const large = flag(), sweep = flag()
762
+ const x = number() + ox, y = number() + oy
763
+ out.push(...arcToCurves(cx, cy, rx, ry, phi, large, sweep, x, y))
764
+ cx = x; cy = y
765
+ break
766
+ }
767
+
768
+ default:
769
+ throw new Error(`svg path: command not handled: ${cmd}`)
770
+ }
771
+
772
+ last = C
773
+ skip()
774
+ }
775
+
776
+ return out
777
+ }
778
+
779
+ // a 2d affine transform: [a b c d e f], x' = a x + c y + e, y' = b x + d y + f
780
+ type Matrix = [number, number, number, number, number, number]
781
+
782
+ const IDENTITY: Matrix = [1, 0, 0, 1, 0, 0]
783
+
784
+ function mulMatrix( m: Matrix, n: Matrix ): Matrix {
785
+ return [
786
+ m[0] * n[0] + m[2] * n[1], m[1] * n[0] + m[3] * n[1],
787
+ m[0] * n[2] + m[2] * n[3], m[1] * n[2] + m[3] * n[3],
788
+ m[0] * n[4] + m[2] * n[5] + m[4], m[1] * n[4] + m[3] * n[5] + m[5],
789
+ ]
790
+ }
791
+
792
+ function parseTransform( tr: string ): Matrix {
793
+ let m = IDENTITY
794
+ for (const [, fn, args] of tr.matchAll(/(\w+)\s*\(([^)]*)\)/g)) {
795
+ const v = args.split(/[\s,]+/).filter(s => s).map(Number)
796
+ let t: Matrix
797
+ switch (fn) {
798
+ case 'matrix': t = v.slice(0, 6) as Matrix; break
799
+ case 'translate': t = [1, 0, 0, 1, v[0], v[1] ?? 0]; break
800
+ case 'scale': t = [v[0], 0, 0, v[1] ?? v[0], 0, 0]; break
801
+ case 'rotate': {
802
+ const a = v[0] * Math.PI / 180, c = Math.cos(a), s = Math.sin(a)
803
+ const [px, py] = [v[1] ?? 0, v[2] ?? 0]
804
+ t = [c, s, -s, c, px - c * px + s * py, py - s * px - c * py]
805
+ break
806
+ }
807
+ case 'skewX': t = [1, 0, Math.tan(v[0] * Math.PI / 180), 1, 0, 0]; break
808
+ case 'skewY': t = [1, Math.tan(v[0] * Math.PI / 180), 0, 1, 0, 0]; break
809
+ default: continue
810
+ }
811
+ m = mulMatrix(m, t)
812
+ }
813
+ return m
814
+ }
815
+
816
+ function transformSegments( segs: PathSegment[], m: Matrix ): PathSegment[] {
817
+ const X = ( x: number, y: number ) => m[0] * x + m[2] * y + m[4]
818
+ const Y = ( x: number, y: number ) => m[1] * x + m[3] * y + m[5]
819
+
820
+ return segs.map( ( s ): PathSegment => {
821
+ switch (s[0]) {
822
+ case 'm': return ['m', X(s[1], s[2]), Y(s[1], s[2])]
823
+ case 'l': return ['l', X(s[1], s[2]), Y(s[1], s[2])]
824
+ case 'c': return ['c', X(s[1], s[2]), Y(s[1], s[2]), X(s[3], s[4]), Y(s[3], s[4]), X(s[5], s[6]), Y(s[5], s[6])]
825
+ default: return ['h']
826
+ }
827
+ } )
828
+ }
829
+
830
+ /**
831
+ * The text of a svg, as an Icon takes it: "var:name" (a CSS variable), "data:image/svg+xml,...",
832
+ * "data:image/svg+xml;base64,..." (a svg imported by the bundler), "<svg...". A file (".svg") cannot
833
+ * be read while a page is drawn: load it first, with svgLoader.load( )
834
+ */
835
+
836
+ function svgSource( icon: string ): string {
837
+ while (icon.startsWith('var:')) {
838
+ icon = document.documentElement.style.getPropertyValue(icon.substring(4)).trim()
839
+ }
840
+
841
+ if (icon.startsWith('data:image/svg+xml;base64,')) {
842
+ return atob(icon.substring(26))
843
+ }
844
+
845
+ if (icon.startsWith('data:image/svg+xml,')) {
846
+ const body = icon.substring(19)
847
+ return body.startsWith('<') ? body : decodeURIComponent(body)
848
+ }
849
+
850
+ if (icon.trimStart().startsWith('<')) {
851
+ return icon
852
+ }
853
+
854
+ throw new Error(`icon: not a svg (a file is loaded first, by svgLoader.load): ${icon.substring(0, 40)}`)
855
+ }
856
+
857
+ export interface IconOptions {
858
+ color?: string // currentColor, and a fill that is not given; black when not given
859
+ background?: string // an opacity is mixed with it (a PDF without transparency); white when not given
860
+ }
861
+
862
+ interface SvgShape {
863
+ segs: PathSegment[] // in the coordinates of the viewBox
864
+ fill: string // null: not filled
865
+ stroke: string // null: not stroked
866
+ width: number // of the stroke, in the coordinates of the viewBox
867
+ alpha: number
868
+ evenOdd: boolean
869
+ }
870
+
871
+ const SVG_SKIP = ['defs', 'clipPath', 'mask', 'symbol', 'style', 'title', 'desc', 'metadata', 'linearGradient', 'radialGradient', 'pattern', 'marker', 'filter', 'text', 'image', 'use', 'foreignObject']
872
+
873
+ /**
874
+ * the shapes of a svg: path, rect, circle, ellipse, line, polyline, polygon, in groups,
875
+ * with their transforms. fill, stroke, stroke-width, opacity, fill-opacity, fill-rule:
876
+ * as attributes or in style="". currentColor, and a fill that is not given, are `color`
877
+ */
878
+
879
+ function svgShapes( source: string, color: string ) {
880
+ const doc = new DOMParser().parseFromString(source, 'image/svg+xml')
881
+ const root = doc.documentElement
882
+ if (root.nodeName !== 'svg') {
883
+ throw new Error('icon: not a svg')
884
+ }
885
+
886
+ let vb = (root.getAttribute('viewBox') ?? '').split(/[\s,]+/).filter(s => s).map(Number)
887
+ if (vb.length !== 4) {
888
+ vb = [0, 0, parseFloat(root.getAttribute('width')), parseFloat(root.getAttribute('height'))]
889
+ }
890
+ if (!(vb[2] > 0 && vb[3] > 0)) {
891
+ throw new Error('icon: no viewBox, nor width and height')
892
+ }
893
+
894
+ interface Paint { fill: string; stroke: string; width: number; alpha: number; fillAlpha: number; evenOdd: boolean }
895
+
896
+ const shapes: SvgShape[] = []
897
+ const n = ( el: Element, name: string, def = 0 ) => {
898
+ const v = parseFloat(el.getAttribute(name))
899
+ return isNaN(v) ? def : v
900
+ }
901
+
902
+ const visit = ( el: Element, m: Matrix, inherited: Paint ) => {
903
+ if (SVG_SKIP.includes(el.nodeName)) {
904
+ return
905
+ }
906
+
907
+ // the properties of the element: its attributes, then its style
908
+ const props: Record<string, string> = {}
909
+ for (const name of ['fill', 'stroke', 'stroke-width', 'opacity', 'fill-opacity', 'fill-rule', 'display', 'visibility']) {
910
+ const v = el.getAttribute(name)?.trim()
911
+ if (v) {
912
+ props[name] = v
913
+ }
914
+ }
915
+ for (const decl of (el.getAttribute('style') ?? '').split(';')) {
916
+ const [k, v] = decl.split(':').map(s => s?.trim())
917
+ if (k && v) {
918
+ props[k] = v
919
+ }
920
+ }
921
+
922
+ if (props.display === 'none' || props.visibility === 'hidden') {
923
+ return
924
+ }
925
+
926
+ const paint = (v: string, cur: string) => {
927
+ if (v === undefined || v === 'inherit') {
928
+ return cur
929
+ }
930
+ if (v === 'none' || v === 'transparent') {
931
+ return null
932
+ }
933
+ return (v === 'currentColor' || v.startsWith('url(')) ? color : v
934
+ }
935
+
936
+ const p: Paint = {
937
+ fill: paint(props.fill, inherited.fill),
938
+ stroke: paint(props.stroke, inherited.stroke),
939
+ width: props['stroke-width'] !== undefined ? parseFloat(props['stroke-width']) : inherited.width,
940
+ alpha: inherited.alpha * (props.opacity !== undefined ? parseFloat(props.opacity) : 1),
941
+ fillAlpha: props['fill-opacity'] !== undefined ? parseFloat(props['fill-opacity']) : inherited.fillAlpha,
942
+ evenOdd: props['fill-rule'] !== undefined ? props['fill-rule'] === 'evenodd' : inherited.evenOdd,
943
+ }
944
+
945
+ const tr = el.getAttribute('transform')
946
+ const mm = tr ? mulMatrix(m, parseTransform(tr)) : m
947
+
948
+ let d: string = null
949
+ switch (el.nodeName) {
950
+ case 'svg':
951
+ case 'g':
952
+ case 'a':
953
+ for (const child of Array.from(el.children)) {
954
+ visit(child, mm, p)
955
+ }
956
+ return
957
+
958
+ case 'path':
959
+ d = el.getAttribute('d')
960
+ break
961
+
962
+ case 'rect': {
963
+ const x = n(el, 'x'), y = n(el, 'y'), w = n(el, 'width'), h = n(el, 'height')
964
+ let rx = n(el, 'rx', -1), ry = n(el, 'ry', -1)
965
+ rx = rx < 0 ? Math.max(ry, 0) : rx
966
+ ry = ry < 0 ? rx : ry
967
+ rx = Math.min(rx, w / 2)
968
+ ry = Math.min(ry, h / 2)
969
+ d = (rx && ry)
970
+ ? `M${x + rx},${y}H${x + w - rx}A${rx},${ry} 0 0 1 ${x + w},${y + ry}V${y + h - ry}A${rx},${ry} 0 0 1 ${x + w - rx},${y + h}H${x + rx}A${rx},${ry} 0 0 1 ${x},${y + h - ry}V${y + ry}A${rx},${ry} 0 0 1 ${x + rx},${y}Z`
971
+ : `M${x},${y}H${x + w}V${y + h}H${x}Z`
972
+ break
973
+ }
974
+
975
+ case 'circle':
976
+ case 'ellipse': {
977
+ const cx = n(el, 'cx'), cy = n(el, 'cy')
978
+ const rx = el.nodeName === 'circle' ? n(el, 'r') : n(el, 'rx')
979
+ const ry = el.nodeName === 'circle' ? rx : n(el, 'ry')
980
+ d = `M${cx - rx},${cy}A${rx},${ry} 0 1 0 ${cx + rx},${cy}A${rx},${ry} 0 1 0 ${cx - rx},${cy}Z`
981
+ break
982
+ }
983
+
984
+ case 'line':
985
+ d = `M${n(el, 'x1')},${n(el, 'y1')}L${n(el, 'x2')},${n(el, 'y2')}`
986
+ break
987
+
988
+ case 'polyline':
989
+ case 'polygon':
990
+ d = 'M' + (el.getAttribute('points') ?? '').trim() + (el.nodeName === 'polygon' ? 'Z' : '')
991
+ break
992
+ }
993
+
994
+ if (!d) {
995
+ return
996
+ }
997
+
998
+ // the stroke width grows with the transform
999
+ const scale = Math.sqrt(Math.abs(mm[0] * mm[3] - mm[1] * mm[2]))
1000
+
1001
+ shapes.push({
1002
+ segs: transformSegments(parseSvgPath(d), mm),
1003
+ fill: p.fill,
1004
+ stroke: p.stroke,
1005
+ width: p.width * scale,
1006
+ alpha: p.alpha * p.fillAlpha,
1007
+ evenOdd: p.evenOdd,
1008
+ })
1009
+ }
1010
+
1011
+ // by default, a svg is filled in black: here in the color asked
1012
+ visit(root, IDENTITY, { fill: color, stroke: null, width: 1, alpha: 1, fillAlpha: 1, evenOdd: false })
1013
+ return { box: vb, shapes }
1014
+ }
1015
+
1016
+ /**
1017
+ * a color with an opacity, mixed with the background: a PDF without transparency
1018
+ * (printers do not like it)
1019
+ */
1020
+
1021
+ function mixColor( color: string, background: string, alpha: number ): Color {
1022
+ const c = new Color(color)
1023
+ if (alpha >= 1) {
1024
+ return c
1025
+ }
1026
+
1027
+ const f = c.toRgb(), b = new Color(background).toRgb()
1028
+ const mix = ( x: number, y: number ) => Math.round(alpha * x + (1 - alpha) * y)
1029
+ return new Color(mix(f.red, b.red), mix(f.green, b.green), mix(f.blue, b.blue))
1030
+ }
1031
+
43
1032
  type PDFValue = null | boolean | number | string | PDFValue[] | Ref | { [key: string]: PDFValue }
44
1033
 
45
1034
  interface PDFObject {
@@ -48,12 +1037,11 @@ interface PDFObject {
48
1037
  stream: Uint8Array | null
49
1038
  }
50
1039
 
51
- export function measureText(str: string, size: number): number {
1040
+ export function measureText(str: string, size: number, bold = false): number {
1041
+ const widths = bold ? WIDTHS_BOLD : WIDTHS
52
1042
  let width = 0
53
- for (let i = 0; i < str.length; i++) {
54
- const code = str.charCodeAt(i)
55
- const w = (code >= 32 && code <= 126) ? WIDTHS[code - 32] : 556
56
- width += w
1043
+ for (const code of winAnsiCodes(str)) {
1044
+ width += widths[code - 32]
57
1045
  }
58
1046
  return (width * size) / 1000
59
1047
  }
@@ -62,9 +1050,36 @@ function fx( v:number, n:number ) {
62
1050
  return (+v.toFixed(n)).toString();
63
1051
  }
64
1052
 
1053
+ // the components of a color go from 0 to 1
65
1054
  function formatColor( clr: Color, suffix: string ) : string {
66
1055
  const rgb = clr.toNumber( );
67
- return `${fx((rgb>>16)&0xff,3)} ${fx((rgb>>8)&0xff,3)} ${fx(rgb&0xff,3)} ${suffix}`;
1056
+ return `${fx(((rgb>>16)&0xff)/255,3)} ${fx(((rgb>>8)&0xff)/255,3)} ${fx((rgb&0xff)/255,3)} ${suffix}`;
1057
+ }
1058
+
1059
+ function hex( codes: number[] ): string {
1060
+ return codes.map( c => c.toString(16).padStart(2,'0') ).join('');
1061
+ }
1062
+
1063
+ /**
1064
+ * a text of the content, in WinAnsiEncoding: written in hex, the content
1065
+ * stays in ASCII
1066
+ */
1067
+
1068
+ function pdfWinAnsi(str: string): string {
1069
+ return '<' + hex( winAnsiCodes(str) ) + '>';
1070
+ }
1071
+
1072
+ /**
1073
+ * a text of the document (its title...): in UTF-16BE, any character passes
1074
+ */
1075
+
1076
+ function pdfTextString(str: string): string {
1077
+ const codes = [0xfe, 0xff];
1078
+ for (let i = 0; i < str.length; i++) {
1079
+ const c = str.charCodeAt(i);
1080
+ codes.push( c >> 8, c & 0xff );
1081
+ }
1082
+ return '<' + hex( codes ) + '>';
68
1083
  }
69
1084
 
70
1085
 
@@ -98,8 +1113,8 @@ function serialize(val: PDFValue): string {
98
1113
  return val // name
99
1114
  }
100
1115
 
101
- if (val.startsWith('(')) {
102
- return val // already escaped string
1116
+ if (val.startsWith('(') || val.startsWith('<')) {
1117
+ return val // already escaped string, or hex string
103
1118
  }
104
1119
 
105
1120
  return pdfString(val)
@@ -124,18 +1139,44 @@ function serialize(val: PDFValue): string {
124
1139
  return String(val)
125
1140
  }
126
1141
 
1142
+ /**
1143
+ * zlib, as FlateDecode reads it
1144
+ */
1145
+
1146
+ async function deflate(data: Uint8Array): Promise<Uint8Array> {
1147
+ const stream = new Blob([data as BlobPart]).stream().pipeThrough(new CompressionStream('deflate'))
1148
+ return new Uint8Array(await new Response(stream).arrayBuffer())
1149
+ }
1150
+
127
1151
  // @internal
128
1152
  class Ref {
129
1153
  id: number
130
1154
  constructor(id: number) { this.id = id }
131
1155
  }
132
1156
 
133
- interface PageContext {
1157
+ export interface PageContext {
134
1158
  text(str: string, x: number, y: number, size: number, opts?: TextOptions): void;
135
1159
  rect(x: number, y: number, w: number, h: number, fill: string): void;
136
1160
  line(x1: number, y1: number, x2: number, y2: number, stroke: string, lineWidth?: number): void;
137
1161
  image(jpegBytes: Uint8Array, x: number, y: number, w: number, h: number): void;
138
1162
  link(url: string, x: number, y: number, w: number, h: number, opts?: LinkOptions): void;
1163
+ /** filled with fill, its outline drawn with stroke; null for none of them */
1164
+ path(segments: PathSegment[], fill: string, stroke?: string, lineWidth?: number): void;
1165
+ /**
1166
+ * a Data Matrix: the square of side min( w, h ), (x, y) its bottom left corner;
1167
+ * a Code 128: its bars on w x h. Without the quiet zone, left to the caller
1168
+ */
1169
+ barcode(type: BarcodeType, text: string, x: number, y: number, w: number, h: number, opts?: BarcodeOptions): void;
1170
+ /**
1171
+ * the "d" of a svg path: the origin of the svg on (x, y), its y going down;
1172
+ * scale: points by unit of the svg. fill and stroke as for path
1173
+ */
1174
+ svgPath(d: string, x: number, y: number, scale: number, fill: string, stroke?: string, lineWidth?: number): void;
1175
+ /**
1176
+ * a svg icon, as an Icon takes it (not a file: load it first, by svgLoader.load),
1177
+ * its viewBox fitted and centered in w x h, (x, y) the bottom left corner
1178
+ */
1179
+ icon(svg: string, x: number, y: number, w: number, h: number, opts?: IconOptions): void;
139
1180
  }
140
1181
 
141
1182
  export class X4PDFBuilder {
@@ -143,6 +1184,11 @@ export class X4PDFBuilder {
143
1184
  private objects: PDFObject[] = [];
144
1185
  private pages: Ref[] = [];
145
1186
  private nextId = 1;
1187
+ private props: PDFProps;
1188
+
1189
+ constructor( props: PDFProps = {} ) {
1190
+ this.props = props;
1191
+ }
146
1192
 
147
1193
  private _addObject(dict: Record<string, PDFValue>, streamBytes: Uint8Array = null): Ref {
148
1194
  const id = this.nextId++;
@@ -159,11 +1205,11 @@ export class X4PDFBuilder {
159
1205
  let imageCount = 0;
160
1206
 
161
1207
  const text = (str: string, x: number, y: number, size: number, opts: TextOptions = {}) => {
162
- const { align = 'left', width: boxWidth, color = '#000000' } = opts;
1208
+ const { align = 'left', width: boxWidth, color = '#000000', bold = false } = opts;
163
1209
 
164
1210
  let tx = x;
165
1211
  if (align !== 'left' && boxWidth !== undefined) {
166
- const textWidth = measureText(str, size);
1212
+ const textWidth = measureText(str, size, bold);
167
1213
  if (align === 'center') {
168
1214
  tx = x + (boxWidth - textWidth) / 2;
169
1215
  }
@@ -173,35 +1219,80 @@ export class X4PDFBuilder {
173
1219
  }
174
1220
  }
175
1221
 
176
- const rgb = new Color(color);
177
- if (rgb) {
178
- ops.push( formatColor(rgb, 'rg') );
179
- }
1222
+ ops.push( formatColor(new Color(color), 'rg') );
180
1223
 
181
1224
  ops.push('BT')
182
- ops.push(`/F1 ${size} Tf`)
1225
+ ops.push(`/${bold ? 'F2' : 'F1'} ${size} Tf`)
183
1226
  ops.push(`${fx(tx,2)} ${fx(y,2)} Td`)
184
- ops.push(`${str.startsWith('(') ? str : pdfString(str)} Tj`)
1227
+ ops.push(`${pdfWinAnsi(str)} Tj`)
185
1228
  ops.push('ET')
186
1229
  }
187
-
1230
+
188
1231
  const rect = (x: number, y: number, w: number, h: number, fill: string) => {
189
- const rgb = new Color(fill)
190
- if (rgb) {
191
- ops.push( formatColor(rgb, 'rg' ) );
192
- ops.push(`${fx(x,2)} ${fx(y,2)} ${fx(w,2)} ${fx(h,2)} re`);
193
- ops.push('f');
194
- }
1232
+ ops.push( formatColor(new Color(fill), 'rg' ) );
1233
+ ops.push(`${fx(x,2)} ${fx(y,2)} ${fx(w,2)} ${fx(h,2)} re`);
1234
+ ops.push('f');
195
1235
  }
196
-
1236
+
197
1237
  const line = (x1: number, y1: number, x2: number, y2: number, stroke: string, lineWidth: number = 1) => {
198
- const rgb = new Color(stroke)
199
- if (rgb) {
1238
+ ops.push(`${fx(lineWidth,2)} w`);
1239
+ ops.push( formatColor( new Color(stroke),'RG') );
1240
+ ops.push(`${fx(x1,2)} ${fx(y1,2)} m`);
1241
+ ops.push(`${fx(x2,2)} ${fx(y2,2)} l`);
1242
+ ops.push('S')
1243
+ }
1244
+
1245
+ const draw = (segments: PathSegment[], fill: Color, stroke: Color, lineWidth: number, evenOdd = false) => {
1246
+ if (!fill && !stroke) {
1247
+ return
1248
+ }
1249
+
1250
+ if (fill) {
1251
+ ops.push( formatColor(fill, 'rg') );
1252
+ }
1253
+
1254
+ if (stroke) {
200
1255
  ops.push(`${fx(lineWidth,2)} w`);
201
- ops.push( formatColor( rgb,'RG') );
202
- ops.push(`${fx(x1,2)} ${fx(y1,2)} m`);
203
- ops.push(`${fx(x2,2)} ${fx(y2,2)} l`);
204
- ops.push('S')
1256
+ ops.push( formatColor(stroke, 'RG') );
1257
+ }
1258
+
1259
+ for (const [op, ...coords] of segments) {
1260
+ ops.push( [...coords.map( v => fx(v,2) ), op].join(' ') );
1261
+ }
1262
+
1263
+ // f: fill, S: stroke, B: both; * for the even-odd rule
1264
+ const star = evenOdd ? '*' : ''
1265
+ ops.push( fill ? (stroke ? 'B' + star : 'f' + star) : 'S' );
1266
+ }
1267
+
1268
+ const path = (segments: PathSegment[], fill: string, stroke?: string, lineWidth: number = 1) => {
1269
+ draw(segments, fill ? new Color(fill) : null, stroke ? new Color(stroke) : null, lineWidth)
1270
+ }
1271
+
1272
+ const svgPath = (d: string, x: number, y: number, scale: number, fill: string, stroke?: string, lineWidth: number = 1) => {
1273
+ // the origin of the svg on (x, y), its y going down
1274
+ const segs = transformSegments(parseSvgPath(d), [scale, 0, 0, -scale, x, y])
1275
+ draw(segs, fill ? new Color(fill) : null, stroke ? new Color(stroke) : null, lineWidth)
1276
+ }
1277
+
1278
+ const icon = (svg: string, x: number, y: number, w: number, h: number, opts: IconOptions = {}) => {
1279
+ const { box: [bx, by, bw, bh], shapes } = svgShapes(svgSource(svg), opts.color ?? '#000000')
1280
+ const background = opts.background ?? '#ffffff'
1281
+
1282
+ // the viewBox fits in w x h, centered, its y going down
1283
+ const s = Math.min(w / bw, h / bh)
1284
+ const ox = x + (w - bw * s) / 2 - bx * s
1285
+ const oy = y + h - (h - bh * s) / 2 + by * s
1286
+ const m: Matrix = [s, 0, 0, -s, ox, oy]
1287
+
1288
+ for (const shape of shapes) {
1289
+ draw(
1290
+ transformSegments(shape.segs, m),
1291
+ shape.fill ? mixColor(shape.fill, background, shape.alpha) : null,
1292
+ shape.stroke ? mixColor(shape.stroke, background, shape.alpha) : null,
1293
+ shape.width * s,
1294
+ shape.evenOdd
1295
+ )
205
1296
  }
206
1297
  }
207
1298
 
@@ -238,23 +1329,45 @@ export class X4PDFBuilder {
238
1329
  const link = (url: string, x: number, y: number, w: number, h: number, opts: LinkOptions = {}) => {
239
1330
  links.push({ url, rect: [x, y, x + w, y + h] })
240
1331
  if (opts.underline) {
241
- const rgb = new Color(opts.underline)
242
- if (rgb) {
243
- ops.push(`0.75 w`)
244
- ops.push( formatColor(rgb, 'RG' ) );
245
- ops.push(`${fx(x,2)} ${fx(y + 2,2)} m`);
246
- ops.push(`${fx(x + w,2)} ${fx(y + 2,2)} l`);
247
- ops.push('S')
248
- }
1332
+ ops.push(`0.75 w`)
1333
+ ops.push( formatColor(new Color(opts.underline), 'RG' ) );
1334
+ ops.push(`${fx(x,2)} ${fx(y + 2,2)} m`);
1335
+ ops.push(`${fx(x + w,2)} ${fx(y + 2,2)} l`);
1336
+ ops.push('S')
249
1337
  }
250
1338
  }
251
1339
 
1340
+ const barcode = (type: BarcodeType, str: string, x: number, y: number, w: number, h: number, opts: BarcodeOptions = {}) => {
1341
+ const { rows, cols, runs } = barcodeRuns(type, str, opts)
1342
+
1343
+ // a module: square in a Data Matrix, the whole height in a Code 128
1344
+ let mw: number, mh: number
1345
+ if (type === 'datamatrix') {
1346
+ mw = mh = Math.min(w, h) / cols
1347
+ }
1348
+ else {
1349
+ mw = w / cols
1350
+ mh = h
1351
+ }
1352
+
1353
+ const top = y + rows * mh
1354
+ ops.push( formatColor(new Color(opts.color ?? '#000000'), 'rg') )
1355
+ for (const [r, c, len] of runs) {
1356
+ ops.push(`${fx(x + c * mw,3)} ${fx(top - (r + 1) * mh,3)} ${fx(len * mw,3)} ${fx(mh,3)} re`)
1357
+ }
1358
+ ops.push('f')
1359
+ }
1360
+
252
1361
  const ctx: PageContext = {
253
1362
  text,
254
1363
  rect,
255
1364
  line,
256
1365
  image,
257
- link
1366
+ link,
1367
+ path,
1368
+ barcode,
1369
+ svgPath,
1370
+ icon
258
1371
  }
259
1372
 
260
1373
  let width: number
@@ -295,7 +1408,7 @@ export class X4PDFBuilder {
295
1408
  MediaBox: [0, 0, width, height],
296
1409
  Contents: contentRef,
297
1410
  Resources: {
298
- Font: { F1: null },
1411
+ Font: { F1: null, F2: null },
299
1412
  XObject: Object.keys(xobjects).length > 0 ? xobjects : undefined
300
1413
  },
301
1414
  Annots: annots.length > 0 ? annots : undefined
@@ -304,6 +1417,27 @@ export class X4PDFBuilder {
304
1417
  this.pages.push(pageRef)
305
1418
  }
306
1419
 
1420
+ /**
1421
+ * the document, its streams compressed (FlateDecode). The images, already
1422
+ * JPEG, are left as they are
1423
+ */
1424
+
1425
+ async buildAsync(): Promise<Uint8Array> {
1426
+ for (const obj of this.objects) {
1427
+ if (obj.stream && !obj.dict.Filter) {
1428
+ obj.stream = await deflate(obj.stream)
1429
+ obj.dict.Filter = '/FlateDecode'
1430
+ obj.dict.Length = obj.stream.length
1431
+ }
1432
+ }
1433
+
1434
+ return this.build()
1435
+ }
1436
+
1437
+ /**
1438
+ * the document, its streams not compressed
1439
+ */
1440
+
307
1441
  build(): Uint8Array {
308
1442
  const fontRef = this._addObject({
309
1443
  Type: '/Font',
@@ -312,6 +1446,13 @@ export class X4PDFBuilder {
312
1446
  Encoding: '/WinAnsiEncoding'
313
1447
  })
314
1448
 
1449
+ const boldRef = this._addObject({
1450
+ Type: '/Font',
1451
+ Subtype: '/Type1',
1452
+ BaseFont: '/Helvetica-Bold',
1453
+ Encoding: '/WinAnsiEncoding'
1454
+ })
1455
+
315
1456
  const pagesRef = this._addObject({
316
1457
  Type: '/Pages',
317
1458
  Kids: this.pages,
@@ -323,20 +1464,33 @@ export class X4PDFBuilder {
323
1464
  obj.dict.Parent = pagesRef
324
1465
  const resources = obj.dict.Resources as Record<string, PDFValue> | undefined
325
1466
  if (resources?.Font) {
326
- (resources.Font as Record<string, PDFValue>).F1 = fontRef
1467
+ const fonts = resources.Font as Record<string, PDFValue>
1468
+ fonts.F1 = fontRef
1469
+ fonts.F2 = boldRef
327
1470
  }
328
1471
  }
329
1472
  }
330
1473
 
1474
+ // printed at its size, not fitted to the paper
331
1475
  const catalogRef = this._addObject({
332
1476
  Type: '/Catalog',
333
- Pages: pagesRef
1477
+ Pages: pagesRef,
1478
+ ViewerPreferences: { PrintScaling: '/None', DisplayDocTitle: true }
1479
+ })
1480
+
1481
+ const { title, author, subject } = this.props
1482
+ const infoRef = this._addObject({
1483
+ Title: title ? pdfTextString(title) : undefined,
1484
+ Author: author ? pdfTextString(author) : undefined,
1485
+ Subject: subject ? pdfTextString(subject) : undefined,
1486
+ Producer: pdfTextString('x4js')
334
1487
  })
335
1488
 
336
1489
  const parts: (string | Uint8Array)[] = []
337
1490
  const offsets: number[] = []
338
1491
 
339
- parts.push('%PDF-1.4\n%\xFF\xFF\xFF\xFF\n')
1492
+ // 1.6: PrintScaling
1493
+ parts.push('%PDF-1.6\n%\xFF\xFF\xFF\xFF\n')
340
1494
 
341
1495
  for (const obj of this.objects) {
342
1496
  offsets[obj.id] = parts.reduce((sum, p) => sum + (typeof p === 'string' ? new TextEncoder().encode(p).length : p.length), 0)
@@ -363,7 +1517,7 @@ export class X4PDFBuilder {
363
1517
  }
364
1518
  parts.push(xref)
365
1519
 
366
- parts.push(`trailer\n${serialize({ Size: this.objects.length + 1, Root: catalogRef })}\n`)
1520
+ parts.push(`trailer\n${serialize({ Size: this.objects.length + 1, Root: catalogRef, Info: infoRef })}\n`)
367
1521
  parts.push(`startxref\n${xrefOffset}\n%%EOF\n`)
368
1522
 
369
1523
  const totalLength = parts.reduce((sum, p) => sum + (typeof p === 'string' ? new TextEncoder().encode(p).length : p.length), 0)