Skip to content

Commit e42c096

Browse files
committed
fix #4016; exporting with image fill
1 parent 621f876 commit e42c096

File tree

4 files changed

+116
-59
lines changed

4 files changed

+116
-59
lines changed

src/charts/common/bar/Helpers.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ export default class Helpers {
185185

186186
getPathFillColor(series, i, j, realIndex) {
187187
const w = this.w
188-
let fill = new Fill(this.barCtx.ctx)
188+
let fill = this.barCtx.ctx.fill
189189

190190
let fillColor = null
191191
let seriesNumber = this.barCtx.barOptions.distributed ? j : i

src/modules/Exports.js

Lines changed: 90 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -20,25 +20,67 @@ class Exports {
2020
}
2121

2222
getSvgString() {
23-
const w = this.w
24-
const width = w.config.chart.toolbar.export.width
25-
let scale =
26-
w.config.chart.toolbar.export.scale || width / w.globals.svgWidth
23+
return new Promise((resolve) => {
24+
const w = this.w
25+
const width = w.config.chart.toolbar.export.width
26+
let scale =
27+
w.config.chart.toolbar.export.scale || width / w.globals.svgWidth
28+
29+
if (!scale) {
30+
scale = 1 // if no scale is specified, don't scale...
31+
}
32+
let svgString = this.w.globals.dom.Paper.svg()
2733

28-
if (!scale) {
29-
scale = 1 // if no scale is specified, don't scale...
30-
}
31-
let svgString = this.w.globals.dom.Paper.svg()
32-
// in case the scale is different than 1, the svg needs to be rescaled
33-
if (scale !== 1) {
3434
// clone the svg node so it remains intact in the UI
3535
const svgNode = this.w.globals.dom.Paper.node.cloneNode(true)
36-
// scale the image
37-
this.scaleSvgNode(svgNode, scale)
38-
// get the string representation of the svgNode
39-
svgString = new XMLSerializer().serializeToString(svgNode)
40-
}
41-
return svgString.replace(/ /g, ' ')
36+
37+
// in case the scale is different than 1, the svg needs to be rescaled
38+
39+
if (scale !== 1) {
40+
// scale the image
41+
this.scaleSvgNode(svgNode, scale)
42+
}
43+
// Convert image URLs to base64
44+
this.convertImagesToBase64(svgNode).then(() => {
45+
svgString = new XMLSerializer().serializeToString(svgNode)
46+
resolve(svgString.replace(/ /g, ' '))
47+
})
48+
})
49+
}
50+
51+
convertImagesToBase64(svgNode) {
52+
const images = svgNode.getElementsByTagName('image')
53+
const promises = Array.from(images).map((img) => {
54+
const href = img.getAttributeNS('http://www.w3.org/1999/xlink', 'href')
55+
if (href && !href.startsWith('data:')) {
56+
return this.getBase64FromUrl(href)
57+
.then((base64) => {
58+
img.setAttributeNS('http://www.w3.org/1999/xlink', 'href', base64)
59+
})
60+
.catch((error) => {
61+
console.error('Error converting image to base64:', error)
62+
})
63+
}
64+
return Promise.resolve()
65+
})
66+
return Promise.all(promises)
67+
}
68+
69+
getBase64FromUrl(url) {
70+
return new Promise((resolve, reject) => {
71+
const img = new Image()
72+
img.crossOrigin = 'Anonymous'
73+
img.onload = () => {
74+
const canvas = document.createElement('canvas')
75+
canvas.width = img.width
76+
canvas.height = img.height
77+
const ctx = canvas.getContext('2d')
78+
ctx.drawImage(img, 0, 0)
79+
resolve(canvas.toDataURL())
80+
}
81+
img.onerror = reject
82+
img.src = url
83+
})
4284
}
4385

4486
cleanup() {
@@ -70,11 +112,15 @@ class Exports {
70112
}
71113

72114
svgUrl() {
73-
this.cleanup()
74-
75-
const svgData = this.getSvgString()
76-
const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' })
77-
return URL.createObjectURL(svgBlob)
115+
return new Promise((resolve) => {
116+
this.cleanup()
117+
this.getSvgString().then((svgData) => {
118+
const svgBlob = new Blob([svgData], {
119+
type: 'image/svg+xml;charset=utf-8',
120+
})
121+
resolve(URL.createObjectURL(svgBlob))
122+
})
123+
})
78124
}
79125

80126
dataURI(options) {
@@ -100,35 +146,37 @@ class Exports {
100146
ctx.fillStyle = canvasBg
101147
ctx.fillRect(0, 0, canvas.width * scale, canvas.height * scale)
102148

103-
const svgData = this.getSvgString()
149+
this.getSvgString().then((svgData) => {
150+
const svgUrl = 'data:image/svg+xml,' + encodeURIComponent(svgData)
151+
let img = new Image()
152+
img.crossOrigin = 'anonymous'
104153

105-
const svgUrl = 'data:image/svg+xml,' + encodeURIComponent(svgData)
106-
let img = new Image()
107-
img.crossOrigin = 'anonymous'
154+
img.onload = () => {
155+
ctx.drawImage(img, 0, 0)
108156

109-
img.onload = () => {
110-
ctx.drawImage(img, 0, 0)
111-
112-
if (canvas.msToBlob) {
113-
// Microsoft Edge can't navigate to data urls, so we return the blob instead
114-
let blob = canvas.msToBlob()
115-
resolve({ blob })
116-
} else {
117-
let imgURI = canvas.toDataURL('image/png')
118-
resolve({ imgURI })
157+
if (canvas.msToBlob) {
158+
// Microsoft Edge can't navigate to data urls, so we return the blob instead
159+
let blob = canvas.msToBlob()
160+
resolve({ blob })
161+
} else {
162+
let imgURI = canvas.toDataURL('image/png')
163+
resolve({ imgURI })
164+
}
119165
}
120-
}
121166

122-
img.src = svgUrl
167+
img.src = svgUrl
168+
})
123169
})
124170
}
125171

126172
exportToSVG() {
127-
this.triggerDownload(
128-
this.svgUrl(),
129-
this.w.config.chart.toolbar.export.svg.filename,
130-
'.svg'
131-
)
173+
this.svgUrl().then((url) => {
174+
this.triggerDownload(
175+
url,
176+
this.w.config.chart.toolbar.export.svg.filename,
177+
'.svg'
178+
)
179+
})
132180
}
133181

134182
exportToPng() {

src/modules/Fill.js

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class Fill {
1414

1515
this.opts = null
1616
this.seriesIndex = 0
17+
this.patternIDs = []
1718
}
1819

1920
clippedImgArea(params) {
@@ -176,23 +177,29 @@ class Fill {
176177
let imgSrc = cnf.fill.image.src
177178

178179
let patternID = opts.patternID ? opts.patternID : ''
179-
this.clippedImgArea({
180-
opacity: fillOpacity,
181-
image: Array.isArray(imgSrc)
182-
? opts.seriesNumber < imgSrc.length
183-
? imgSrc[opts.seriesNumber]
184-
: imgSrc[0]
185-
: imgSrc,
186-
width: opts.width ? opts.width : undefined,
187-
height: opts.height ? opts.height : undefined,
188-
patternUnits: opts.patternUnits,
189-
patternID: `pattern${w.globals.cuid}${
190-
opts.seriesNumber + 1
191-
}${patternID}`,
192-
})
193-
pathFill = `url(#pattern${w.globals.cuid}${
180+
const patternKey = `pattern${w.globals.cuid}${
194181
opts.seriesNumber + 1
195-
}${patternID})`
182+
}${patternID}`
183+
184+
if (this.patternIDs.indexOf(patternKey) === -1) {
185+
console.log('patternKey', patternKey)
186+
this.clippedImgArea({
187+
opacity: fillOpacity,
188+
image: Array.isArray(imgSrc)
189+
? opts.seriesNumber < imgSrc.length
190+
? imgSrc[opts.seriesNumber]
191+
: imgSrc[0]
192+
: imgSrc,
193+
width: opts.width ? opts.width : undefined,
194+
height: opts.height ? opts.height : undefined,
195+
patternUnits: opts.patternUnits,
196+
patternID: patternKey,
197+
})
198+
199+
this.patternIDs.push(patternKey)
200+
}
201+
202+
pathFill = `url(#${patternKey})`
196203
} else if (fillType === 'gradient') {
197204
pathFill = gradientFill
198205
} else if (fillType === 'pattern') {

src/modules/helpers/InitCtxVariables.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import Crosshairs from '../Crosshairs'
88
import Grid from '../axes/Grid'
99
import Graphics from '../Graphics'
1010
import Exports from '../Exports'
11+
import Fill from '../Fill.js'
1112
import Options from '../settings/Options'
1213
import Responsive from '../Responsive'
1314
import Series from '../Series'
@@ -90,6 +91,7 @@ export default class InitCtxVariables {
9091
this.ctx.crosshairs = new Crosshairs(this.ctx)
9192
this.ctx.events = new Events(this.ctx)
9293
this.ctx.exports = new Exports(this.ctx)
94+
this.ctx.fill = new Fill(this.ctx)
9395
this.ctx.localization = new Localization(this.ctx)
9496
this.ctx.options = new Options()
9597
this.ctx.responsive = new Responsive(this.ctx)

0 commit comments

Comments
 (0)