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.
Files changed (38) hide show
  1. package/aicli.md +3 -3
  2. package/aicontext.md +49 -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 +696 -46
  37. package/src/core/core_router.ts +42 -44
  38. package/src/core/core_state.ts +22 -5
@@ -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 (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
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
- const rgb = new Color(color);
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(`/F1 ${size} Tf`)
755
+ ops.push(`/${bold ? 'F2' : 'F1'} ${size} Tf`)
183
756
  ops.push(`${fx(tx,2)} ${fx(y,2)} Td`)
184
- ops.push(`${str.startsWith('(') ? str : pdfString(str)} Tj`)
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
- 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
- }
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
- const rgb = new Color(stroke)
199
- if (rgb) {
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( 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')
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
- 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
- }
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
- (resources.Font as Record<string, PDFValue>).F1 = fontRef
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
- parts.push('%PDF-1.4\n%\xFF\xFF\xFF\xFF\n')
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)