Skip to content

Commit

Permalink
fix #4016; exporting with image fill
Browse files Browse the repository at this point in the history
  • Loading branch information
junedchhipa committed Aug 29, 2024
1 parent 621f876 commit e42c096
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 59 deletions.
2 changes: 1 addition & 1 deletion src/charts/common/bar/Helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ export default class Helpers {

getPathFillColor(series, i, j, realIndex) {
const w = this.w
let fill = new Fill(this.barCtx.ctx)
let fill = this.barCtx.ctx.fill

let fillColor = null
let seriesNumber = this.barCtx.barOptions.distributed ? j : i
Expand Down
132 changes: 90 additions & 42 deletions src/modules/Exports.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,67 @@ class Exports {
}

getSvgString() {
const w = this.w
const width = w.config.chart.toolbar.export.width
let scale =
w.config.chart.toolbar.export.scale || width / w.globals.svgWidth
return new Promise((resolve) => {
const w = this.w
const width = w.config.chart.toolbar.export.width
let scale =
w.config.chart.toolbar.export.scale || width / w.globals.svgWidth

if (!scale) {
scale = 1 // if no scale is specified, don't scale...
}
let svgString = this.w.globals.dom.Paper.svg()

if (!scale) {
scale = 1 // if no scale is specified, don't scale...
}
let svgString = this.w.globals.dom.Paper.svg()
// in case the scale is different than 1, the svg needs to be rescaled
if (scale !== 1) {
// clone the svg node so it remains intact in the UI
const svgNode = this.w.globals.dom.Paper.node.cloneNode(true)
// scale the image
this.scaleSvgNode(svgNode, scale)
// get the string representation of the svgNode
svgString = new XMLSerializer().serializeToString(svgNode)
}
return svgString.replace(/ /g, ' ')

// in case the scale is different than 1, the svg needs to be rescaled

if (scale !== 1) {
// scale the image
this.scaleSvgNode(svgNode, scale)
}
// Convert image URLs to base64
this.convertImagesToBase64(svgNode).then(() => {
svgString = new XMLSerializer().serializeToString(svgNode)
resolve(svgString.replace(/ /g, ' '))
})
})
}

convertImagesToBase64(svgNode) {
const images = svgNode.getElementsByTagName('image')
const promises = Array.from(images).map((img) => {
const href = img.getAttributeNS('http://www.w3.org/1999/xlink', 'href')
if (href && !href.startsWith('data:')) {
return this.getBase64FromUrl(href)
.then((base64) => {
img.setAttributeNS('http://www.w3.org/1999/xlink', 'href', base64)
})
.catch((error) => {
console.error('Error converting image to base64:', error)
})
}
return Promise.resolve()
})
return Promise.all(promises)
}

getBase64FromUrl(url) {
return new Promise((resolve, reject) => {
const img = new Image()
img.crossOrigin = 'Anonymous'
img.onload = () => {
const canvas = document.createElement('canvas')
canvas.width = img.width
canvas.height = img.height
const ctx = canvas.getContext('2d')
ctx.drawImage(img, 0, 0)
resolve(canvas.toDataURL())
}
img.onerror = reject
img.src = url
})
}

cleanup() {
Expand Down Expand Up @@ -70,11 +112,15 @@ class Exports {
}

svgUrl() {
this.cleanup()

const svgData = this.getSvgString()
const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' })
return URL.createObjectURL(svgBlob)
return new Promise((resolve) => {
this.cleanup()
this.getSvgString().then((svgData) => {
const svgBlob = new Blob([svgData], {
type: 'image/svg+xml;charset=utf-8',
})
resolve(URL.createObjectURL(svgBlob))
})
})
}

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

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

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

img.onload = () => {
ctx.drawImage(img, 0, 0)

if (canvas.msToBlob) {
// Microsoft Edge can't navigate to data urls, so we return the blob instead
let blob = canvas.msToBlob()
resolve({ blob })
} else {
let imgURI = canvas.toDataURL('image/png')
resolve({ imgURI })
if (canvas.msToBlob) {
// Microsoft Edge can't navigate to data urls, so we return the blob instead
let blob = canvas.msToBlob()
resolve({ blob })
} else {
let imgURI = canvas.toDataURL('image/png')
resolve({ imgURI })
}
}
}

img.src = svgUrl
img.src = svgUrl
})
})
}

exportToSVG() {
this.triggerDownload(
this.svgUrl(),
this.w.config.chart.toolbar.export.svg.filename,
'.svg'
)
this.svgUrl().then((url) => {
this.triggerDownload(
url,
this.w.config.chart.toolbar.export.svg.filename,
'.svg'
)
})
}

exportToPng() {
Expand Down
39 changes: 23 additions & 16 deletions src/modules/Fill.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class Fill {

this.opts = null
this.seriesIndex = 0
this.patternIDs = []
}

clippedImgArea(params) {
Expand Down Expand Up @@ -176,23 +177,29 @@ class Fill {
let imgSrc = cnf.fill.image.src

let patternID = opts.patternID ? opts.patternID : ''
this.clippedImgArea({
opacity: fillOpacity,
image: Array.isArray(imgSrc)
? opts.seriesNumber < imgSrc.length
? imgSrc[opts.seriesNumber]
: imgSrc[0]
: imgSrc,
width: opts.width ? opts.width : undefined,
height: opts.height ? opts.height : undefined,
patternUnits: opts.patternUnits,
patternID: `pattern${w.globals.cuid}${
opts.seriesNumber + 1
}${patternID}`,
})
pathFill = `url(#pattern${w.globals.cuid}${
const patternKey = `pattern${w.globals.cuid}${
opts.seriesNumber + 1
}${patternID})`
}${patternID}`

if (this.patternIDs.indexOf(patternKey) === -1) {
console.log('patternKey', patternKey)
this.clippedImgArea({
opacity: fillOpacity,
image: Array.isArray(imgSrc)
? opts.seriesNumber < imgSrc.length
? imgSrc[opts.seriesNumber]
: imgSrc[0]
: imgSrc,
width: opts.width ? opts.width : undefined,
height: opts.height ? opts.height : undefined,
patternUnits: opts.patternUnits,
patternID: patternKey,
})

this.patternIDs.push(patternKey)
}

pathFill = `url(#${patternKey})`
} else if (fillType === 'gradient') {
pathFill = gradientFill
} else if (fillType === 'pattern') {
Expand Down
2 changes: 2 additions & 0 deletions src/modules/helpers/InitCtxVariables.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Crosshairs from '../Crosshairs'
import Grid from '../axes/Grid'
import Graphics from '../Graphics'
import Exports from '../Exports'
import Fill from '../Fill.js'
import Options from '../settings/Options'
import Responsive from '../Responsive'
import Series from '../Series'
Expand Down Expand Up @@ -90,6 +91,7 @@ export default class InitCtxVariables {
this.ctx.crosshairs = new Crosshairs(this.ctx)
this.ctx.events = new Events(this.ctx)
this.ctx.exports = new Exports(this.ctx)
this.ctx.fill = new Fill(this.ctx)
this.ctx.localization = new Localization(this.ctx)
this.ctx.options = new Options()
this.ctx.responsive = new Responsive(this.ctx)
Expand Down

0 comments on commit e42c096

Please sign in to comment.