x4js 2.3.19 → 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/aicontext.md +2 -0
- package/package.json +1 -1
- package/src/core/core_pdf.ts +510 -6
package/aicontext.md
CHANGED
|
@@ -1548,6 +1548,8 @@ export interface PageContext { // origin bottom left, y goes up
|
|
|
1548
1548
|
image(jpegBytes: Uint8Array, x, y, w, h): void
|
|
1549
1549
|
link(url, x, y, w, h, opts?: { underline?: string }): void
|
|
1550
1550
|
barcode(type: BarcodeType, text, x, y, w, h, opts?: BarcodeOptions): void // datamatrix: square min(w,h), (x,y) bottom left; code128: bars on w x h; no quiet zone
|
|
1551
|
+
svgPath(d: string, x, y, scale, fill: string, stroke?: string, lineWidth?): void // (x,y): the svg origin, its y going down; M L H V C S Q T A Z
|
|
1552
|
+
icon(svg: string, x, y, w, h, opts?: { color?: string; background?: string }): void // as Icon takes it (not a file); viewBox fitted in w x h; opacity mixed with background
|
|
1551
1553
|
}
|
|
1552
1554
|
type PathSegment = ['m', x, y] | ['l', x, y] | ['c', x1, y1, x2, y2, x3, y3] | ['h']
|
|
1553
1555
|
type BarcodeType = 'datamatrix' | 'code128'
|
package/package.json
CHANGED
package/src/core/core_pdf.ts
CHANGED
|
@@ -569,6 +569,466 @@ 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
|
+
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
|
+
|
|
572
1032
|
type PDFValue = null | boolean | number | string | PDFValue[] | Ref | { [key: string]: PDFValue }
|
|
573
1033
|
|
|
574
1034
|
interface PDFObject {
|
|
@@ -707,6 +1167,16 @@ export interface PageContext {
|
|
|
707
1167
|
* a Code 128: its bars on w x h. Without the quiet zone, left to the caller
|
|
708
1168
|
*/
|
|
709
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;
|
|
710
1180
|
}
|
|
711
1181
|
|
|
712
1182
|
export class X4PDFBuilder {
|
|
@@ -772,26 +1242,58 @@ export class X4PDFBuilder {
|
|
|
772
1242
|
ops.push('S')
|
|
773
1243
|
}
|
|
774
1244
|
|
|
775
|
-
const
|
|
1245
|
+
const draw = (segments: PathSegment[], fill: Color, stroke: Color, lineWidth: number, evenOdd = false) => {
|
|
776
1246
|
if (!fill && !stroke) {
|
|
777
1247
|
return
|
|
778
1248
|
}
|
|
779
1249
|
|
|
780
1250
|
if (fill) {
|
|
781
|
-
ops.push( formatColor(
|
|
1251
|
+
ops.push( formatColor(fill, 'rg') );
|
|
782
1252
|
}
|
|
783
1253
|
|
|
784
1254
|
if (stroke) {
|
|
785
1255
|
ops.push(`${fx(lineWidth,2)} w`);
|
|
786
|
-
ops.push( formatColor(
|
|
1256
|
+
ops.push( formatColor(stroke, 'RG') );
|
|
787
1257
|
}
|
|
788
1258
|
|
|
789
1259
|
for (const [op, ...coords] of segments) {
|
|
790
1260
|
ops.push( [...coords.map( v => fx(v,2) ), op].join(' ') );
|
|
791
1261
|
}
|
|
792
1262
|
|
|
793
|
-
// f: fill, S: stroke, B: both
|
|
794
|
-
|
|
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
|
+
)
|
|
1296
|
+
}
|
|
795
1297
|
}
|
|
796
1298
|
|
|
797
1299
|
const image = (jpegBytes: Uint8Array, x: number, y: number, w: number, h: number) => {
|
|
@@ -863,7 +1365,9 @@ export class X4PDFBuilder {
|
|
|
863
1365
|
image,
|
|
864
1366
|
link,
|
|
865
1367
|
path,
|
|
866
|
-
barcode
|
|
1368
|
+
barcode,
|
|
1369
|
+
svgPath,
|
|
1370
|
+
icon
|
|
867
1371
|
}
|
|
868
1372
|
|
|
869
1373
|
let width: number
|