diff --git a/modules/base/BasePainter.mjs b/modules/base/BasePainter.mjs index f64398ca0..b5f289535 100644 --- a/modules/base/BasePainter.mjs +++ b/modules/base/BasePainter.mjs @@ -1,5 +1,5 @@ import { select as d3_select } from '../d3.mjs'; -import { settings, internals, isNodeJs, isFunc, isStr, isObject, btoa_func, getDocument } from '../core.mjs'; +import { settings, internals, isNodeJs, isFunc, isStr, isObject, btoa_func, getDocument, constants } from '../core.mjs'; import { getColor, addColor } from './colors.mjs'; /** @summary Standard prefix for SVG file context as data url @@ -882,19 +882,46 @@ async function svgToImage(svg, image_format, args) { }); const img_src = 'data:image/svg+xml;base64,' + btoa_func(decodeURIComponent(svg)); + + // Use the newer and stabler `resvg-js` backend for converting SVG to PNG + if (constants.Embed3D.UseResvgJs || constants.Render3D.UseResvgJs) { + return import('@resvg/resvg-js').then(({ Resvg }) => { + const rawSvg = decodeURIComponent(svg); // raw SVG XML + + // Initialize Resvg and create the PNG buffer + const resvg = new Resvg(rawSvg); + const renderData = resvg.render(); + const pngBuffer = renderData.asPng(); + + // Return raw RGBA pixels if caller requested it + if (image_format === 'rgba') { + return { + width: renderData.width, + height: renderData.height, + data: renderData.pixels + }; + } + + if (args?.as_buffer) { + return pngBuffer; + } - return import('canvas').then(async handle => { - return handle.default.loadImage(img_src).then(img => { - const canvas = handle.default.createCanvas(img.width, img.height); + return 'data:image/png;base64,' + pngBuffer.toString('base64'); + }); + } else { // Fallback to `node-canvas` + return import('canvas').then(async handle => { + return handle.default.loadImage(img_src).then(img => { + const canvas = handle.default.createCanvas(img.width, img.height); - canvas.getContext('2d').drawImage(img, 0, 0, img.width, img.height); + canvas.getContext('2d').drawImage(img, 0, 0, img.width, img.height); - if (args?.as_buffer) - return canvas.toBuffer('image/' + image_format); + if (args?.as_buffer) + return canvas.toBuffer('image/' + image_format); - return image_format ? canvas.toDataURL('image/' + image_format) : canvas; + return image_format ? canvas.toDataURL('image/' + image_format) : canvas; + }); }); - }); + } } const img_src = URL.createObjectURL(new Blob([doctype + svg], { type: 'image/svg+xml;charset=utf-8' })); diff --git a/modules/core.mjs b/modules/core.mjs index 649a5f269..05239e272 100644 --- a/modules/core.mjs +++ b/modules/core.mjs @@ -169,6 +169,8 @@ const constants = { SVG: 3, /** @summary Disable renderer, used for three.js model creation, only for internal use recommended */ None: 4, + /** @summary Use `resvg-js` backend for converting SVGs */ + UseResvgJs: true, fromString(s) { if ((s === 'webgl') || (s === 'gl')) return this.WebGL; @@ -194,6 +196,8 @@ const constants = { Embed: 2, /** @summary Embedding, but when SVG rendering or SVG image conversion is used */ EmbedSVG: 3, + /** @summary Use `resvg-js` backend for converting SVGs */ + UseResvgJs: true, /** @summary Convert string values into number */ fromString(s) { if (s === 'embed') diff --git a/modules/hist/TPavePainter.mjs b/modules/hist/TPavePainter.mjs index 3dd7910f0..ecadbfa91 100644 --- a/modules/hist/TPavePainter.mjs +++ b/modules/hist/TPavePainter.mjs @@ -66,18 +66,29 @@ class TPavePainter extends ObjectPainter { tm = pad?.fTopMargin ?? gStyle.fPadTopMargin, bm = pad?.fBottomMargin ?? gStyle.fPadBottomMargin; - return svgToImage(svg_code).then(canvas => { - if (!canvas) + return svgToImage(svg_code, 'rgba').then(image => { + if (!image) return false; - - let nX = 100, nY = 100; - const context = canvas.getContext('2d'), - arr = context.getImageData(0, 0, canvas.width, canvas.height).data, - boxW = Math.floor(canvas.width / nX), boxH = Math.floor(canvas.height / nY), + + let arr; + const width = image.width; + const height = image.height; + + if (image.data && image.width && image.height) { + arr = image.data; + } else if (image.getContext('2d')) { + arr = image.getContext('2d').getImageData(0, 0, width, height).data; + } else { + return false; + } + + let nX = 100, nY = 100; + const boxW = Math.floor(width / nX), + boxH = Math.floor(height / nY), raster = new Array(nX * nY); - if (arr.length !== canvas.width * canvas.height * 4) { - console.log(`Image size missmatch in TLegend autoplace ${arr.length} expected ${canvas.width * canvas.height * 4}`); + if (arr.length !== width * height * 4) { + console.log(`Image size missmatch in TLegend autoplace ${arr.length} expected ${width * height * 4}`); nX = nY = 0; } @@ -89,7 +100,7 @@ class TPavePainter extends ObjectPainter { for (let x = px1; (x < px2) && !filled; ++x) { for (let y = py1; y < py2; ++y) { - const indx = (y * canvas.width + x) * 4; + const indx = (y * width + x) * 4; if (arr[indx] || arr[indx + 1] || arr[indx + 2] || arr[indx + 3]) { filled = 1; break; diff --git a/package-lock.json b/package-lock.json index c650e7500..7c75e32a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "@oneidentity/zstd-js": "^1.0.3", + "@resvg/resvg-js": "^2.6.2", "canvas": "^3.2.1", "jsdom": "^27.4.0", "mathjax": "3.2.2", @@ -648,6 +649,221 @@ "@types/emscripten": "^1.39.4" } }, + "node_modules/@resvg/resvg-js": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js/-/resvg-js-2.6.2.tgz", + "integrity": "sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q==", + "license": "MPL-2.0", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@resvg/resvg-js-android-arm-eabi": "2.6.2", + "@resvg/resvg-js-android-arm64": "2.6.2", + "@resvg/resvg-js-darwin-arm64": "2.6.2", + "@resvg/resvg-js-darwin-x64": "2.6.2", + "@resvg/resvg-js-linux-arm-gnueabihf": "2.6.2", + "@resvg/resvg-js-linux-arm64-gnu": "2.6.2", + "@resvg/resvg-js-linux-arm64-musl": "2.6.2", + "@resvg/resvg-js-linux-x64-gnu": "2.6.2", + "@resvg/resvg-js-linux-x64-musl": "2.6.2", + "@resvg/resvg-js-win32-arm64-msvc": "2.6.2", + "@resvg/resvg-js-win32-ia32-msvc": "2.6.2", + "@resvg/resvg-js-win32-x64-msvc": "2.6.2" + } + }, + "node_modules/@resvg/resvg-js-android-arm-eabi": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm-eabi/-/resvg-js-android-arm-eabi-2.6.2.tgz", + "integrity": "sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-android-arm64": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm64/-/resvg-js-android-arm64-2.6.2.tgz", + "integrity": "sha512-VcOKezEhm2VqzXpcIJoITuvUS/fcjIw5NA/w3tjzWyzmvoCdd+QXIqy3FBGulWdClvp4g+IfUemigrkLThSjAQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-darwin-arm64": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-arm64/-/resvg-js-darwin-arm64-2.6.2.tgz", + "integrity": "sha512-nmok2LnAd6nLUKI16aEB9ydMC6Lidiiq2m1nEBDR1LaaP7FGs4AJ90qDraxX+CWlVuRlvNjyYJTNv8qFjtL9+A==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-darwin-x64": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-x64/-/resvg-js-darwin-x64-2.6.2.tgz", + "integrity": "sha512-GInyZLjgWDfsVT6+SHxQVRwNzV0AuA1uqGsOAW+0th56J7Nh6bHHKXHBWzUrihxMetcFDmQMAX1tZ1fZDYSRsw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-arm-gnueabihf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm-gnueabihf/-/resvg-js-linux-arm-gnueabihf-2.6.2.tgz", + "integrity": "sha512-YIV3u/R9zJbpqTTNwTZM5/ocWetDKGsro0SWp70eGEM9eV2MerWyBRZnQIgzU3YBnSBQ1RcxRZvY/UxwESfZIw==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-arm64-gnu": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-gnu/-/resvg-js-linux-arm64-gnu-2.6.2.tgz", + "integrity": "sha512-zc2BlJSim7YR4FZDQ8OUoJg5holYzdiYMeobb9pJuGDidGL9KZUv7SbiD4E8oZogtYY42UZEap7dqkkYuA91pg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-arm64-musl": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-musl/-/resvg-js-linux-arm64-musl-2.6.2.tgz", + "integrity": "sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-x64-gnu": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-gnu/-/resvg-js-linux-x64-gnu-2.6.2.tgz", + "integrity": "sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-x64-musl": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-musl/-/resvg-js-linux-x64-musl-2.6.2.tgz", + "integrity": "sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-win32-arm64-msvc": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-arm64-msvc/-/resvg-js-win32-arm64-msvc-2.6.2.tgz", + "integrity": "sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-win32-ia32-msvc": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-ia32-msvc/-/resvg-js-win32-ia32-msvc-2.6.2.tgz", + "integrity": "sha512-har4aPAlvjnLcil40AC77YDIk6loMawuJwFINEM7n0pZviwMkMvjb2W5ZirsNOZY4aDbo5tLx0wNMREp5Brk+w==", + "cpu": [ + "ia32" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-win32-x64-msvc": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-x64-msvc/-/resvg-js-win32-x64-msvc-2.6.2.tgz", + "integrity": "sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@rollup/plugin-json": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", diff --git a/package.json b/package.json index 1edbfb113..cb454785c 100644 --- a/package.json +++ b/package.json @@ -58,22 +58,23 @@ ], "dependencies": { "@oneidentity/zstd-js": "^1.0.3", + "@resvg/resvg-js": "^2.6.2", "canvas": "^3.2.1", "jsdom": "^27.4.0", "mathjax": "3.2.2", + "three": "0.162.0", "tmp": "^0.2.5", - "xhr2": "^0.2.1", - "three": "0.162.0" + "xhr2": "^0.2.1" }, "devDependencies": { - "gl": "9.0.0-rc.9", "@rollup/plugin-json": "6.1.0", "@rollup/plugin-node-resolve": "16.0.1", - "@rollup/plugin-terser": "0.4.4", "@rollup/plugin-replace": "6.0.2", - "eslint": "^9.39.2", + "@rollup/plugin-terser": "0.4.4", "@stylistic/eslint-plugin": "^4.4.1", "docdash": "^2.0.2", + "eslint": "^9.39.2", + "gl": "9.0.0-rc.9", "jsdoc": "^4.0.5", "rollup": "4.56.0", "rollup-plugin-ascii": "0.0.3",