diff --git a/CHANGELOG.md b/CHANGELOG.md index 9254b1bb..13e1c1aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ _Hotfix_: _Fixes:_ - Made chart userOptions available within `customCode` as variable `options` [(#551)](https://github.com/highcharts/node-export-server/issues/551). +- Fix the base64 images not working in exported SVGs (Namespace prefix xlink for href on image is not defined, [#547](https://github.com/highcharts/node-export-server/issues/547)). # 4.0.1 diff --git a/lib/server/routes/export.js b/lib/server/routes/export.js index 4895a25b..7d20f731 100644 --- a/lib/server/routes/export.js +++ b/lib/server/routes/export.js @@ -23,7 +23,8 @@ import { isObjectEmpty, isPrivateRangeUrlFound, optionsStringify, - measureTime + measureTime, + addXlinkNamespace } from '../../utils.js'; import HttpError from '../../errors/HttpError.js'; @@ -249,6 +250,18 @@ const exportHandler = async (request, response, next) => { doCallbacks(afterRequest, request, response, { id, body: info.result }); if (info.result) { + // This exception is a workaround for #547 + // The plainly downloaded SVG is not properly formatted - + // it lacks the xmlns:xlink so images with "xlink:href" cannot be displayed + // and the entire SVG is deemed as incorrect (this may be a Highcharts + // problem as well as they should take care of this). + // A proper SVG has xlmns:xlink defined if they are used + // and Highcharts does not seem to have that for now. + // Once they do, we can get rid of this. + if (type === 'svg') { + info.result = addXlinkNamespace(info.result); + } + // If only base64 is required, return it if (body.b64) { // SVG Exception for the Highcharts 11.3.0 version diff --git a/lib/utils.js b/lib/utils.js index 6823c003..eb3b5205 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -448,6 +448,39 @@ export const measureTime = () => { return () => Number(process.hrtime.bigint() - start) / 1000000; }; +/** + * This method is used to add the xlink namespace to the SVG string. + * This may be a workaround, as Highcharts should take care of this. + * @param {string} svgString + * @returns {string} + */ +export const addXlinkNamespace = (svgString) => { + // Check if the xlink namespace is already present + const xlinkNamespace = 'xmlns:xlink="http://www.w3.org/1999/xlink"'; + if (svgString.includes(xlinkNamespace)) { + // The namespace is already included, no need to add it + return svgString; + } + + // Find the position of the opening tag + const svgTagEnd = svgString.indexOf('>'); + + // If is self-closing, find the position accordingly + const selfClosing = svgString[svgTagEnd - 1] === '/'; + + // Define the insertion point for the namespace attribute + const insertionPoint = selfClosing ? svgTagEnd - 1 : svgTagEnd; + + // Insert the xlink namespace declaration + const modifiedSvgString = + svgString.slice(0, insertionPoint) + + ' ' + + xlinkNamespace + + svgString.slice(insertionPoint); + + return modifiedSvgString; +}; + export default { __dirname, clearText, diff --git a/tests/unit/utils.test.js b/tests/unit/utils.test.js index 7c89f52d..77a98e2e 100644 --- a/tests/unit/utils.test.js +++ b/tests/unit/utils.test.js @@ -6,7 +6,8 @@ import { isCorrectJSON, isObject, isObjectEmpty, - isPrivateRangeUrlFound + isPrivateRangeUrlFound, + addXlinkNamespace } from '../../lib/utils'; describe('clearText', () => { @@ -166,3 +167,29 @@ describe('isPrivateRangeUrlFound', () => { }); }); }); + +describe('addXlinkNamespace', () => { + it('adds the xlink namespace to an SVG string', () => { + const svg = ''; + const expected = + ''; + + expect(addXlinkNamespace(svg)).toBe(expected); + }); + + it('does not add the xlink namespace if it already exists', () => { + const svg = + ''; + expect(addXlinkNamespace(svg)).toBe(svg); + }); + + it('does add the xlink namespace properly for SVG tag with multiple attributes', () => { + const svg = + ''; + + const expected = + ''; + + expect(addXlinkNamespace(svg)).toBe(expected); + }); +});