x4js 2.3.19 → 2.3.21
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/aicontext.md +48 -28
- package/package.json +1 -1
- package/src/components/autocomplete/autocomplete.ts +3 -0
- package/src/components/boxes/boxes.ts +1 -1
- package/src/components/chart/chart.ts +12 -3
- package/src/components/colorinput/colorinput.module.scss +11 -2
- package/src/components/colorinput/colorinput.ts +37 -4
- package/src/components/colorpicker/colorpicker.module.scss +14 -2
- package/src/components/colorpicker/colorpicker.ts +57 -11
- package/src/components/dialog/dialog.ts +16 -5
- package/src/components/form/form.ts +3 -4
- package/src/components/gridview/gridview.module.scss +18 -16
- package/src/components/gridview/gridview.ts +5 -0
- package/src/components/icon/icon.ts +34 -18
- package/src/components/keyboard/keyboard.ts +7 -3
- package/src/components/link/link.ts +18 -7
- package/src/components/messages/messages.ts +1 -1
- package/src/components/panel/panel.ts +3 -2
- package/src/components/spreadsheet/spreadsheet.module.scss +18 -16
- package/src/components/textarea/textarea.module.scss +10 -3
- package/src/components/video/video.ts +17 -3
- package/src/core/core_colors.ts +21 -10
- package/src/core/core_dragdrop.ts +5 -4
- package/src/core/core_i18n.ts +34 -54
- package/src/core/core_pdf.ts +524 -8
- package/src/core/core_shortcuts.ts +5 -1
- package/src/core/core_styles.ts +46 -25
- package/src/core/core_svg.ts +24 -16
- package/src/core/core_tools.ts +18 -9
package/src/core/core_pdf.ts
CHANGED
|
@@ -569,6 +569,472 @@ export function renderBarcodeImage( type: BarcodeType, text: string, opts: Barco
|
|
|
569
569
|
return 'data:image/svg+xml,' + encodeURIComponent(new XMLSerializer().serializeToString(svg.getDom()))
|
|
570
570
|
}
|
|
571
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
|
+
// "var:home" or "var:--home": the variable --home, set on <html>
|
|
839
|
+
let name = icon.substring(4)
|
|
840
|
+
if (!name.startsWith('--')) {
|
|
841
|
+
name = '--' + name
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
icon = document.documentElement.style.getPropertyValue(name).trim()
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
if (icon.startsWith('data:image/svg+xml;base64,')) {
|
|
848
|
+
return atob(icon.substring(26))
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
if (icon.startsWith('data:image/svg+xml,')) {
|
|
852
|
+
const body = icon.substring(19)
|
|
853
|
+
return body.startsWith('<') ? body : decodeURIComponent(body)
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
if (icon.trimStart().startsWith('<')) {
|
|
857
|
+
return icon
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
throw new Error(`icon: not a svg (a file is loaded first, by svgLoader.load): ${icon.substring(0, 40)}`)
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
export interface IconOptions {
|
|
864
|
+
color?: string // currentColor, and a fill that is not given; black when not given
|
|
865
|
+
background?: string // an opacity is mixed with it (a PDF without transparency); white when not given
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
interface SvgShape {
|
|
869
|
+
segs: PathSegment[] // in the coordinates of the viewBox
|
|
870
|
+
fill: string // null: not filled
|
|
871
|
+
stroke: string // null: not stroked
|
|
872
|
+
width: number // of the stroke, in the coordinates of the viewBox
|
|
873
|
+
alpha: number
|
|
874
|
+
evenOdd: boolean
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
const SVG_SKIP = ['defs', 'clipPath', 'mask', 'symbol', 'style', 'title', 'desc', 'metadata', 'linearGradient', 'radialGradient', 'pattern', 'marker', 'filter', 'text', 'image', 'use', 'foreignObject']
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* the shapes of a svg: path, rect, circle, ellipse, line, polyline, polygon, in groups,
|
|
881
|
+
* with their transforms. fill, stroke, stroke-width, opacity, fill-opacity, fill-rule:
|
|
882
|
+
* as attributes or in style="". currentColor, and a fill that is not given, are `color`
|
|
883
|
+
*/
|
|
884
|
+
|
|
885
|
+
function svgShapes( source: string, color: string ) {
|
|
886
|
+
const doc = new DOMParser().parseFromString(source, 'image/svg+xml')
|
|
887
|
+
const root = doc.documentElement
|
|
888
|
+
if (root.nodeName !== 'svg') {
|
|
889
|
+
throw new Error('icon: not a svg')
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
let vb = (root.getAttribute('viewBox') ?? '').split(/[\s,]+/).filter(s => s).map(Number)
|
|
893
|
+
if (vb.length !== 4) {
|
|
894
|
+
vb = [0, 0, parseFloat(root.getAttribute('width')), parseFloat(root.getAttribute('height'))]
|
|
895
|
+
}
|
|
896
|
+
if (!(vb[2] > 0 && vb[3] > 0)) {
|
|
897
|
+
throw new Error('icon: no viewBox, nor width and height')
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
interface Paint { fill: string; stroke: string; width: number; alpha: number; fillAlpha: number; evenOdd: boolean }
|
|
901
|
+
|
|
902
|
+
const shapes: SvgShape[] = []
|
|
903
|
+
const n = ( el: Element, name: string, def = 0 ) => {
|
|
904
|
+
const v = parseFloat(el.getAttribute(name))
|
|
905
|
+
return isNaN(v) ? def : v
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
const visit = ( el: Element, m: Matrix, inherited: Paint ) => {
|
|
909
|
+
if (SVG_SKIP.includes(el.nodeName)) {
|
|
910
|
+
return
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// the properties of the element: its attributes, then its style
|
|
914
|
+
const props: Record<string, string> = {}
|
|
915
|
+
for (const name of ['fill', 'stroke', 'stroke-width', 'opacity', 'fill-opacity', 'fill-rule', 'display', 'visibility']) {
|
|
916
|
+
const v = el.getAttribute(name)?.trim()
|
|
917
|
+
if (v) {
|
|
918
|
+
props[name] = v
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
for (const decl of (el.getAttribute('style') ?? '').split(';')) {
|
|
922
|
+
const [k, v] = decl.split(':').map(s => s?.trim())
|
|
923
|
+
if (k && v) {
|
|
924
|
+
props[k] = v
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
if (props.display === 'none' || props.visibility === 'hidden') {
|
|
929
|
+
return
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
const paint = (v: string, cur: string) => {
|
|
933
|
+
if (v === undefined || v === 'inherit') {
|
|
934
|
+
return cur
|
|
935
|
+
}
|
|
936
|
+
if (v === 'none' || v === 'transparent') {
|
|
937
|
+
return null
|
|
938
|
+
}
|
|
939
|
+
return (v === 'currentColor' || v.startsWith('url(')) ? color : v
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
const p: Paint = {
|
|
943
|
+
fill: paint(props.fill, inherited.fill),
|
|
944
|
+
stroke: paint(props.stroke, inherited.stroke),
|
|
945
|
+
width: props['stroke-width'] !== undefined ? parseFloat(props['stroke-width']) : inherited.width,
|
|
946
|
+
alpha: inherited.alpha * (props.opacity !== undefined ? parseFloat(props.opacity) : 1),
|
|
947
|
+
fillAlpha: props['fill-opacity'] !== undefined ? parseFloat(props['fill-opacity']) : inherited.fillAlpha,
|
|
948
|
+
evenOdd: props['fill-rule'] !== undefined ? props['fill-rule'] === 'evenodd' : inherited.evenOdd,
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
const tr = el.getAttribute('transform')
|
|
952
|
+
const mm = tr ? mulMatrix(m, parseTransform(tr)) : m
|
|
953
|
+
|
|
954
|
+
let d: string = null
|
|
955
|
+
switch (el.nodeName) {
|
|
956
|
+
case 'svg':
|
|
957
|
+
case 'g':
|
|
958
|
+
case 'a':
|
|
959
|
+
for (const child of Array.from(el.children)) {
|
|
960
|
+
visit(child, mm, p)
|
|
961
|
+
}
|
|
962
|
+
return
|
|
963
|
+
|
|
964
|
+
case 'path':
|
|
965
|
+
d = el.getAttribute('d')
|
|
966
|
+
break
|
|
967
|
+
|
|
968
|
+
case 'rect': {
|
|
969
|
+
const x = n(el, 'x'), y = n(el, 'y'), w = n(el, 'width'), h = n(el, 'height')
|
|
970
|
+
let rx = n(el, 'rx', -1), ry = n(el, 'ry', -1)
|
|
971
|
+
rx = rx < 0 ? Math.max(ry, 0) : rx
|
|
972
|
+
ry = ry < 0 ? rx : ry
|
|
973
|
+
rx = Math.min(rx, w / 2)
|
|
974
|
+
ry = Math.min(ry, h / 2)
|
|
975
|
+
d = (rx && ry)
|
|
976
|
+
? `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`
|
|
977
|
+
: `M${x},${y}H${x + w}V${y + h}H${x}Z`
|
|
978
|
+
break
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
case 'circle':
|
|
982
|
+
case 'ellipse': {
|
|
983
|
+
const cx = n(el, 'cx'), cy = n(el, 'cy')
|
|
984
|
+
const rx = el.nodeName === 'circle' ? n(el, 'r') : n(el, 'rx')
|
|
985
|
+
const ry = el.nodeName === 'circle' ? rx : n(el, 'ry')
|
|
986
|
+
d = `M${cx - rx},${cy}A${rx},${ry} 0 1 0 ${cx + rx},${cy}A${rx},${ry} 0 1 0 ${cx - rx},${cy}Z`
|
|
987
|
+
break
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
case 'line':
|
|
991
|
+
d = `M${n(el, 'x1')},${n(el, 'y1')}L${n(el, 'x2')},${n(el, 'y2')}`
|
|
992
|
+
break
|
|
993
|
+
|
|
994
|
+
case 'polyline':
|
|
995
|
+
case 'polygon':
|
|
996
|
+
d = 'M' + (el.getAttribute('points') ?? '').trim() + (el.nodeName === 'polygon' ? 'Z' : '')
|
|
997
|
+
break
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
if (!d) {
|
|
1001
|
+
return
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// the stroke width grows with the transform
|
|
1005
|
+
const scale = Math.sqrt(Math.abs(mm[0] * mm[3] - mm[1] * mm[2]))
|
|
1006
|
+
|
|
1007
|
+
shapes.push({
|
|
1008
|
+
segs: transformSegments(parseSvgPath(d), mm),
|
|
1009
|
+
fill: p.fill,
|
|
1010
|
+
stroke: p.stroke,
|
|
1011
|
+
width: p.width * scale,
|
|
1012
|
+
alpha: p.alpha * p.fillAlpha,
|
|
1013
|
+
evenOdd: p.evenOdd,
|
|
1014
|
+
})
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// by default, a svg is filled in black: here in the color asked
|
|
1018
|
+
visit(root, IDENTITY, { fill: color, stroke: null, width: 1, alpha: 1, fillAlpha: 1, evenOdd: false })
|
|
1019
|
+
return { box: vb, shapes }
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
/**
|
|
1023
|
+
* a color with an opacity, mixed with the background: a PDF without transparency
|
|
1024
|
+
* (printers do not like it)
|
|
1025
|
+
*/
|
|
1026
|
+
|
|
1027
|
+
function mixColor( color: string, background: string, alpha: number ): Color {
|
|
1028
|
+
const c = new Color(color)
|
|
1029
|
+
if (alpha >= 1) {
|
|
1030
|
+
return c
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
const f = c.toRgb(), b = new Color(background).toRgb()
|
|
1034
|
+
const mix = ( x: number, y: number ) => Math.round(alpha * x + (1 - alpha) * y)
|
|
1035
|
+
return new Color(mix(f.red, b.red), mix(f.green, b.green), mix(f.blue, b.blue))
|
|
1036
|
+
}
|
|
1037
|
+
|
|
572
1038
|
type PDFValue = null | boolean | number | string | PDFValue[] | Ref | { [key: string]: PDFValue }
|
|
573
1039
|
|
|
574
1040
|
interface PDFObject {
|
|
@@ -707,6 +1173,16 @@ export interface PageContext {
|
|
|
707
1173
|
* a Code 128: its bars on w x h. Without the quiet zone, left to the caller
|
|
708
1174
|
*/
|
|
709
1175
|
barcode(type: BarcodeType, text: string, x: number, y: number, w: number, h: number, opts?: BarcodeOptions): void;
|
|
1176
|
+
/**
|
|
1177
|
+
* the "d" of a svg path: the origin of the svg on (x, y), its y going down;
|
|
1178
|
+
* scale: points by unit of the svg. fill and stroke as for path
|
|
1179
|
+
*/
|
|
1180
|
+
svgPath(d: string, x: number, y: number, scale: number, fill: string, stroke?: string, lineWidth?: number): void;
|
|
1181
|
+
/**
|
|
1182
|
+
* a svg icon, as an Icon takes it (not a file: load it first, by svgLoader.load),
|
|
1183
|
+
* its viewBox fitted and centered in w x h, (x, y) the bottom left corner
|
|
1184
|
+
*/
|
|
1185
|
+
icon(svg: string, x: number, y: number, w: number, h: number, opts?: IconOptions): void;
|
|
710
1186
|
}
|
|
711
1187
|
|
|
712
1188
|
export class X4PDFBuilder {
|
|
@@ -772,36 +1248,74 @@ export class X4PDFBuilder {
|
|
|
772
1248
|
ops.push('S')
|
|
773
1249
|
}
|
|
774
1250
|
|
|
775
|
-
const
|
|
1251
|
+
const draw = (segments: PathSegment[], fill: Color, stroke: Color, lineWidth: number, evenOdd = false) => {
|
|
776
1252
|
if (!fill && !stroke) {
|
|
777
1253
|
return
|
|
778
1254
|
}
|
|
779
1255
|
|
|
780
1256
|
if (fill) {
|
|
781
|
-
ops.push( formatColor(
|
|
1257
|
+
ops.push( formatColor(fill, 'rg') );
|
|
782
1258
|
}
|
|
783
1259
|
|
|
784
1260
|
if (stroke) {
|
|
785
1261
|
ops.push(`${fx(lineWidth,2)} w`);
|
|
786
|
-
ops.push( formatColor(
|
|
1262
|
+
ops.push( formatColor(stroke, 'RG') );
|
|
787
1263
|
}
|
|
788
1264
|
|
|
789
1265
|
for (const [op, ...coords] of segments) {
|
|
790
1266
|
ops.push( [...coords.map( v => fx(v,2) ), op].join(' ') );
|
|
791
1267
|
}
|
|
792
1268
|
|
|
793
|
-
// f: fill, S: stroke, B: both
|
|
794
|
-
|
|
1269
|
+
// f: fill, S: stroke, B: both; * for the even-odd rule
|
|
1270
|
+
const star = evenOdd ? '*' : ''
|
|
1271
|
+
ops.push( fill ? (stroke ? 'B' + star : 'f' + star) : 'S' );
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
const path = (segments: PathSegment[], fill: string, stroke?: string, lineWidth: number = 1) => {
|
|
1275
|
+
draw(segments, fill ? new Color(fill) : null, stroke ? new Color(stroke) : null, lineWidth)
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
const svgPath = (d: string, x: number, y: number, scale: number, fill: string, stroke?: string, lineWidth: number = 1) => {
|
|
1279
|
+
// the origin of the svg on (x, y), its y going down
|
|
1280
|
+
const segs = transformSegments(parseSvgPath(d), [scale, 0, 0, -scale, x, y])
|
|
1281
|
+
draw(segs, fill ? new Color(fill) : null, stroke ? new Color(stroke) : null, lineWidth)
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
const icon = (svg: string, x: number, y: number, w: number, h: number, opts: IconOptions = {}) => {
|
|
1285
|
+
const { box: [bx, by, bw, bh], shapes } = svgShapes(svgSource(svg), opts.color ?? '#000000')
|
|
1286
|
+
const background = opts.background ?? '#ffffff'
|
|
1287
|
+
|
|
1288
|
+
// the viewBox fits in w x h, centered, its y going down
|
|
1289
|
+
const s = Math.min(w / bw, h / bh)
|
|
1290
|
+
const ox = x + (w - bw * s) / 2 - bx * s
|
|
1291
|
+
const oy = y + h - (h - bh * s) / 2 + by * s
|
|
1292
|
+
const m: Matrix = [s, 0, 0, -s, ox, oy]
|
|
1293
|
+
|
|
1294
|
+
for (const shape of shapes) {
|
|
1295
|
+
draw(
|
|
1296
|
+
transformSegments(shape.segs, m),
|
|
1297
|
+
shape.fill ? mixColor(shape.fill, background, shape.alpha) : null,
|
|
1298
|
+
shape.stroke ? mixColor(shape.stroke, background, shape.alpha) : null,
|
|
1299
|
+
shape.width * s,
|
|
1300
|
+
shape.evenOdd
|
|
1301
|
+
)
|
|
1302
|
+
}
|
|
795
1303
|
}
|
|
796
1304
|
|
|
797
1305
|
const image = (jpegBytes: Uint8Array, x: number, y: number, w: number, h: number) => {
|
|
1306
|
+
// the size is in the frame header (SOF): the segments are followed, an EXIF
|
|
1307
|
+
// thumbnail inside a segment is skipped
|
|
798
1308
|
let imgWidth = 0, imgHeight = 0
|
|
799
|
-
|
|
800
|
-
|
|
1309
|
+
let i = 2
|
|
1310
|
+
while (i + 9 < jpegBytes.length && jpegBytes[i] === 0xFF) {
|
|
1311
|
+
const marker = jpegBytes[i + 1]
|
|
1312
|
+
if (marker >= 0xC0 && marker <= 0xCF && marker !== 0xC4 && marker !== 0xC8 && marker !== 0xCC) {
|
|
801
1313
|
imgHeight = (jpegBytes[i + 5] << 8) | jpegBytes[i + 6]
|
|
802
1314
|
imgWidth = (jpegBytes[i + 7] << 8) | jpegBytes[i + 8]
|
|
803
1315
|
break
|
|
804
1316
|
}
|
|
1317
|
+
|
|
1318
|
+
i += 2 + ((jpegBytes[i + 2] << 8) | jpegBytes[i + 3])
|
|
805
1319
|
}
|
|
806
1320
|
|
|
807
1321
|
const imgName = `/Im${imageCount++}`
|
|
@@ -863,7 +1377,9 @@ export class X4PDFBuilder {
|
|
|
863
1377
|
image,
|
|
864
1378
|
link,
|
|
865
1379
|
path,
|
|
866
|
-
barcode
|
|
1380
|
+
barcode,
|
|
1381
|
+
svgPath,
|
|
1382
|
+
icon
|
|
867
1383
|
}
|
|
868
1384
|
|
|
869
1385
|
let width: number
|
|
@@ -91,6 +91,11 @@ function sequenceOf( ev: KeyboardEvent ): string {
|
|
|
91
91
|
key = ev.code[3].toLowerCase( );
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
// the space bar ( " " ): written "Space" in a sequence
|
|
95
|
+
if( key===" " ) {
|
|
96
|
+
key = "space";
|
|
97
|
+
}
|
|
98
|
+
|
|
94
99
|
return join( { shift: ev.shiftKey, ctrl: ev.ctrlKey, cmd: ev.metaKey, alt: ev.altKey, key } );
|
|
95
100
|
}
|
|
96
101
|
|
|
@@ -158,7 +163,6 @@ const key_names: Record<string,string> = {
|
|
|
158
163
|
arrowdown: "↓",
|
|
159
164
|
escape: "Esc",
|
|
160
165
|
delete: "Del",
|
|
161
|
-
" ": "Space",
|
|
162
166
|
};
|
|
163
167
|
|
|
164
168
|
/**
|