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.
- package/aicli.md +3 -3
- package/aicontext.md +51 -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 +1200 -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,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 (
|
|
54
|
-
|
|
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
|
-
|
|
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(
|
|
1225
|
+
ops.push(`/${bold ? 'F2' : 'F1'} ${size} Tf`)
|
|
183
1226
|
ops.push(`${fx(tx,2)} ${fx(y,2)} Td`)
|
|
184
|
-
ops.push(`${
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
199
|
-
|
|
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(
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|