Skip to content

Commit

Permalink
Added XLink namespace for SVG results with tests, touch #547.
Browse files Browse the repository at this point in the history
  • Loading branch information
PaulDalek committed Feb 10, 2025
1 parent 48d3794 commit 2d44ffa
Show file tree
Hide file tree
Showing 10 changed files with 85 additions and 14 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# 4.0.3

_Fixes_:

- 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.2

_Hotfix_:
Expand Down
2 changes: 0 additions & 2 deletions dist/index.cjs

This file was deleted.

2 changes: 0 additions & 2 deletions dist/index.esm.js

This file was deleted.

1 change: 0 additions & 1 deletion dist/index.esm.js.map

This file was deleted.

6 changes: 1 addition & 5 deletions lib/highcharts.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,7 @@ export async function triggerExport(chartOptions, options, displayErrors) {
let constr = options.export.constr || 'chart';
constr = typeof Highcharts[constr] !== 'undefined' ? constr : 'chart';

Highcharts[constr](
'container',
finalOptions,
finalCallback
);
Highcharts[constr]('container', finalOptions, finalCallback);

// Get the current global options
const defaultOptions = getOptions();
Expand Down
15 changes: 14 additions & 1 deletion lib/server/routes/export.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ import {
isObjectEmpty,
isPrivateRangeUrlFound,
optionsStringify,
measureTime
measureTime,
addXlinkNamespace
} from '../../utils.js';

import HttpError from '../../errors/HttpError.js';
Expand Down Expand Up @@ -262,6 +263,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, as 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
Expand Down
34 changes: 34 additions & 0 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,39 @@ const MAX_BACKOFF_ATTEMPTS = 6;

export const __dirname = fileURLToPath(new URL('../.', import.meta.url));

/**
* 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 <svg> tag
const svgTagEnd = svgString.indexOf('>');

// If <svg> 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;
};

/**
* Clears and standardizes text by replacing multiple consecutive whitespace
* characters with a single space and trimming any leading or trailing
Expand Down Expand Up @@ -450,6 +483,7 @@ export const measureTime = () => {

export default {
__dirname,
addXlinkNamespace,
clearText,
expBackoff,
fixType,
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"author": "Highsoft AS <[email protected]> (http://www.highcharts.com/about)",
"license": "MIT",
"type": "module",
"version": "4.0.2",
"version": "4.0.3",
"main": "./dist/index.esm.js",
"engines": {
"node": ">=18.12.0"
Expand Down
27 changes: 27 additions & 0 deletions tests/unit/utils.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
addXlinkNamespace,
clearText,
fixType,
roundNumber,
Expand All @@ -9,6 +10,32 @@ import {
isPrivateRangeUrlFound
} from '../../lib/utils';

describe('addXlinkNamespace', () => {
it('adds the xlink namespace to an SVG string', () => {
const svg = '<svg><image xlink:href="http://localhost/image.jpg"/></svg>';
const expected =
'<svg xmlns:xlink="http://www.w3.org/1999/xlink"><image xlink:href="http://localhost/image.jpg"/></svg>';

expect(addXlinkNamespace(svg)).toBe(expected);
});

it('does not add the xlink namespace if it already exists', () => {
const svg =
'<svg xmlns:xlink="http://www.w3.org/1999/xlink"><image xlink:href="http://localhost/image.jpg"/></svg>';
expect(addXlinkNamespace(svg)).toBe(svg);
});

it('does add the xlink namespace properly for SVG tag with multiple attributes', () => {
const svg =
'<svg xmlns="http://www.w3.org/2000/svg" custom_attr="0" width="200" height="200"><image xlink:href="http://localhost/image.jpg" width="100" height="100"/></svg>';

const expected =
'<svg xmlns="http://www.w3.org/2000/svg" custom_attr="0" width="200" height="200" xmlns:xlink="http://www.w3.org/1999/xlink"><image xlink:href="http://localhost/image.jpg" width="100" height="100"/></svg>';

expect(addXlinkNamespace(svg)).toBe(expected);
});
});

describe('clearText', () => {
it('replaces multiple spaces with a single space and trims the text', () => {
const input = ' This is a test ';
Expand Down

0 comments on commit 2d44ffa

Please sign in to comment.