From 1ea3a35f46713518b4c1babdda27716581a6418e Mon Sep 17 00:00:00 2001 From: OmarMesqq Date: Fri, 30 Jan 2026 12:45:24 -0300 Subject: [PATCH 1/8] build: :heavy_plus_sign: add `@resvg/resvg-js` as npm dependency --- package-lock.json | 216 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 11 +-- 2 files changed, 222 insertions(+), 5 deletions(-) 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", From a224dfa7e8a3907674adf315fab838b14f5cb52e Mon Sep 17 00:00:00 2001 From: OmarMesqq Date: Fri, 30 Jan 2026 12:46:43 -0300 Subject: [PATCH 2/8] refactor: :recycle: add `resvgjs` feature flag in painter module --- modules/base/BasePainter.mjs | 124 ++++++++++++++++++++--------------- 1 file changed, 71 insertions(+), 53 deletions(-) diff --git a/modules/base/BasePainter.mjs b/modules/base/BasePainter.mjs index f64398ca0..0c14a6c7c 100644 --- a/modules/base/BasePainter.mjs +++ b/modules/base/BasePainter.mjs @@ -5,9 +5,9 @@ import { getColor, addColor } from './colors.mjs'; /** @summary Standard prefix for SVG file context as data url * @private */ const prSVG = 'data:image/svg+xml;charset=utf-8,', -/** @summary Standard prefix for JSON file context as data url - * @private */ - prJSON = 'data:application/json;charset=utf-8,'; + /** @summary Standard prefix for JSON file context as data url + * @private */ + prJSON = 'data:application/json;charset=utf-8,'; /** @summary Returns visible rect of element @@ -92,13 +92,13 @@ function floatToString(value, fmt, ret_fmt) { return ret_fmt ? [value.toFixed(4), '6.4f'] : value.toFixed(4); const kind = fmt[len - 1].toLowerCase(), - compact = (len > 1) && (fmt[len - 2] === 'c') ? 'c' : ''; + compact = (len > 1) && (fmt[len - 2] === 'c') ? 'c' : ''; fmt = fmt.slice(0, len - (compact ? 2 : 1)); if (kind === 'g') { const se = floatToString(value, fmt + 'ce', true), - sg = floatToString(value, fmt + 'cf', true), - res = se[0].length < sg[0].length || ((sg[0] === '0') && value) ? se : sg; + sg = floatToString(value, fmt + 'cf', true), + res = se[0].length < sg[0].length || ((sg[0] === '0') && value) ? se : sg; return ret_fmt ? res : res[0]; } @@ -124,7 +124,7 @@ function floatToString(value, fmt, ret_fmt) { if (compact) { const pnt = se.indexOf('.'), - pe = se.toLowerCase().indexOf('e'); + pe = se.toLowerCase().indexOf('e'); if ((pnt > 0) && (pe > pnt)) { let p = pe; while ((p > pnt) && (se[p - 1] === '0')) @@ -352,8 +352,8 @@ function buildSvgCurve(p, args) { const end_point = (pnt1, pnt2, sign) => { const len = Math.sqrt((pnt2.gry - pnt1.gry) ** 2 + (pnt2.grx - pnt1.grx) ** 2) * args.t, - a2 = Math.atan2(pnt2.dgry, pnt2.dgrx), - a1 = Math.atan2(sign * (pnt2.gry - pnt1.gry), sign * (pnt2.grx - pnt1.grx)); + a2 = Math.atan2(pnt2.dgry, pnt2.dgrx), + a1 = Math.atan2(sign * (pnt2.gry - pnt1.gry), sign * (pnt2.grx - pnt1.grx)); pnt1.dgrx = len * Math.cos(2 * a1 - a2); pnt1.dgry = len * Math.sin(2 * a1 - a2); @@ -420,8 +420,8 @@ function buildSvgCurve(p, args) { for (let n = 1; n < npnts; ++n) { const bin = p[n], - dx = Math.round(bin.grx) - currx, - dy = Math.round(bin.gry) - curry; + dx = Math.round(bin.grx) - currx, + dy = Math.round(bin.gry) - curry; if (dx && dy) { flush(); path += `l${dx},${dy}`; @@ -442,13 +442,13 @@ function buildSvgCurve(p, args) { } else { // build line with trying optimize many vertical moves let currx = Math.round(p[0].grx), curry = Math.round(p[0].gry), - cminy = curry, cmaxy = curry, prevy = curry; + cminy = curry, cmaxy = curry, prevy = curry; for (let n = 1; n < npnts; ++n) { const bin = p[n], - lastx = Math.round(bin.grx), - lasty = Math.round(bin.gry), - dx = lastx - currx; + lastx = Math.round(bin.grx), + lasty = Math.round(bin.gry), + dx = lastx - currx; if (dx === 0) { // if X not change, just remember amplitude and cminy = Math.min(cminy, lasty); @@ -495,14 +495,14 @@ function buildSvgCurve(p, args) { * @private */ function compressSVG(svg) { svg = svg.replace(/url\("#(\w+)"\)/g, 'url(#$1)') // decode all URL - .replace(/ class="\w*"/g, '') // remove all classes - .replace(/ pad="\w*"/g, '') // remove all pad ids - .replace(/ title=""/g, '') // remove all empty titles - .replace(/ style=""/g, '') // remove all empty styles - .replace(/<\/g>/g, '') // remove all empty groups with transform - .replace(/<\/g>/g, '') // remove hidden title - .replace(/<\/g>/g, ''); // remove all empty groups + .replace(/ class="\w*"/g, '') // remove all classes + .replace(/ pad="\w*"/g, '') // remove all pad ids + .replace(/ title=""/g, '') // remove all empty titles + .replace(/ style=""/g, '') // remove all empty styles + .replace(/<\/g>/g, '') // remove all empty groups with transform + .replace(/<\/g>/g, '') // remove hidden title + .replace(/<\/g>/g, ''); // remove all empty groups // remove all empty frame svg, typically appears in 3D drawings, maybe should be improved in frame painter itself svg = svg.replace(/<\/svg>/g, ''); @@ -575,8 +575,8 @@ class BasePainter { return res; const use_enlarge = res.property('use_enlarge'), - layout = res.property('layout') || 'simple', - layout_selector = (layout === 'simple') ? '' : res.property('layout_selector'); + layout = res.property('layout') || 'simple', + layout_selector = (layout === 'simple') ? '' : res.property('layout_selector'); if (layout_selector) res = res.select(layout_selector); @@ -644,17 +644,17 @@ class BasePainter { * @private */ testMainResize(check_level, new_size, height_factor) { const enlarge = this.enlargeMain('state'), - origin = this.selectDom('origin'), - main = this.selectDom(), - lmt = 5; // minimal size + origin = this.selectDom('origin'), + main = this.selectDom(), + lmt = 5; // minimal size if ((enlarge !== 'on') && new_size?.width && new_size?.height) { origin.style('width', new_size.width + 'px') - .style('height', new_size.height + 'px'); + .style('height', new_size.height + 'px'); } const rect_origin = getElementRect(origin, true), - can_resize = origin.attr('can_resize'); + can_resize = origin.attr('can_resize'); let do_resize = false; if ((can_resize === 'height') && height_factor && Math.abs(rect_origin.width * height_factor - rect_origin.height) > 0.1 * rect_origin.width) @@ -674,8 +674,8 @@ class BasePainter { } const rect = getElementRect(main), - old_h = main.property('_jsroot_height'), - old_w = main.property('_jsroot_width'); + old_h = main.property('_jsroot_height'), + old_w = main.property('_jsroot_width'); rect.changed = false; @@ -717,8 +717,8 @@ class BasePainter { * @protected */ enlargeMain(action, skip_warning) { const main = this.selectDom(true), - origin = this.selectDom('origin'), - doc = getDocument(); + origin = this.selectDom('origin'), + doc = getDocument(); if (main.empty() || !settings.CanEnlarge || (origin.property('can_enlarge') === false)) return false; @@ -746,7 +746,7 @@ class BasePainter { .attr('style', 'position: fixed; margin: 0px; border: 0px; padding: 0px; left: 1px; top: 1px; bottom: 1px; right: 1px; background: white; opacity: 0.95; z-index: 100; overflow: hidden;'); const rect1 = getElementRect(main), - rect2 = getElementRect(enlarge); + rect2 = getElementRect(enlarge); // if new enlarge area not big enough, do not do it if ((rect2.width <= rect1.width) || (rect2.height <= rect1.height)) { @@ -846,11 +846,11 @@ function makeTranslate(g, x, y, scale = 1) { function addHighlightStyle(elem, drag) { if (drag) { elem.style('stroke', 'steelblue') - .style('fill-opacity', '0.1'); + .style('fill-opacity', '0.1'); } else { elem.style('stroke', '#4572A7') - .style('fill', '#4572A7') - .style('opacity', '0'); + .style('fill', '#4572A7') + .style('opacity', '0'); } } @@ -882,19 +882,37 @@ async function svgToImage(svg, image_format, args) { }); const img_src = 'data:image/svg+xml;base64,' + btoa_func(decodeURIComponent(svg)); + const RESVG_FEATURE_FLAG = true; + + if (RESVG_FEATURE_FLAG) { + return import('@resvg/resvg-js').then(({ Resvg }) => { + const rawSvg = decodeURIComponent(svg); - return import('canvas').then(async handle => { - return handle.default.loadImage(img_src).then(img => { - const canvas = handle.default.createCanvas(img.width, img.height); + const resvg = new Resvg(rawSvg); - canvas.getContext('2d').drawImage(img, 0, 0, img.width, img.height); + const pngData = resvg.render(); + const pngBuffer = pngData.asPng(); - if (args?.as_buffer) - return canvas.toBuffer('image/' + image_format); + if (args?.as_buffer) { + return pngBuffer; + } - return image_format ? canvas.toDataURL('image/' + image_format) : canvas; + return 'data:image/png;base64,' + pngBuffer.toString('base64'); }); - }); + } else { + 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); + + if (args?.as_buffer) + return canvas.toBuffer('image/' + image_format); + + 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' })); @@ -930,11 +948,11 @@ async function svgToImage(svg, image_format, args) { * @desc Always use UTC to avoid any variation between timezones */ function getTDatime(dt) { const y = (dt.fDatime >>> 26) + 1995, - m = ((dt.fDatime << 6) >>> 28) - 1, - d = (dt.fDatime << 10) >>> 27, - h = (dt.fDatime << 15) >>> 27, - min = (dt.fDatime << 20) >>> 26, - s = (dt.fDatime << 26) >>> 26; + m = ((dt.fDatime << 6) >>> 28) - 1, + d = (dt.fDatime << 10) >>> 27, + h = (dt.fDatime << 15) >>> 27, + min = (dt.fDatime << 20) >>> 26, + s = (dt.fDatime << 26) >>> 26; return new Date(Date.UTC(y, m, d, h, min, s)); } @@ -957,11 +975,11 @@ function convertDate(dt) { * @private */ function getBoxDecorations(xx, yy, ww, hh, bmode, pww, phh) { const side1 = `M${xx},${yy}h${ww}l${-pww},${phh}h${2 * pww - ww}v${hh - 2 * phh}l${-pww},${phh}z`, - side2 = `M${xx + ww},${yy + hh}v${-hh}l${-pww},${phh}v${hh - 2 * phh}h${2 * pww - ww}l${-pww},${phh}z`; + side2 = `M${xx + ww},${yy + hh}v${-hh}l${-pww},${phh}v${hh - 2 * phh}h${2 * pww - ww}l${-pww},${phh}z`; return bmode > 0 ? [side1, side2] : [side2, side1]; } export { prSVG, prJSON, getElementRect, getAbsPosInCanvas, getTDatime, convertDate, - DrawOptions, TRandom, floatToString, buildSvgCurve, compressSVG, getBoxDecorations, + DrawOptions, TRandom, floatToString, buildSvgCurve, compressSVG, getBoxDecorations, BasePainter, _loadJSDOM, makeTranslate, addHighlightStyle, svgToImage }; From f273e9983dbeca08c32f4da342348181d0795b9b Mon Sep 17 00:00:00 2001 From: OmarMesqq Date: Fri, 30 Jan 2026 12:58:49 -0300 Subject: [PATCH 3/8] revert: :rewind: restore BasePainter mjs to status at `master` branch --- modules/base/BasePainter.mjs | 124 +++++++++++++++-------------------- 1 file changed, 53 insertions(+), 71 deletions(-) diff --git a/modules/base/BasePainter.mjs b/modules/base/BasePainter.mjs index 0c14a6c7c..f64398ca0 100644 --- a/modules/base/BasePainter.mjs +++ b/modules/base/BasePainter.mjs @@ -5,9 +5,9 @@ import { getColor, addColor } from './colors.mjs'; /** @summary Standard prefix for SVG file context as data url * @private */ const prSVG = 'data:image/svg+xml;charset=utf-8,', - /** @summary Standard prefix for JSON file context as data url - * @private */ - prJSON = 'data:application/json;charset=utf-8,'; +/** @summary Standard prefix for JSON file context as data url + * @private */ + prJSON = 'data:application/json;charset=utf-8,'; /** @summary Returns visible rect of element @@ -92,13 +92,13 @@ function floatToString(value, fmt, ret_fmt) { return ret_fmt ? [value.toFixed(4), '6.4f'] : value.toFixed(4); const kind = fmt[len - 1].toLowerCase(), - compact = (len > 1) && (fmt[len - 2] === 'c') ? 'c' : ''; + compact = (len > 1) && (fmt[len - 2] === 'c') ? 'c' : ''; fmt = fmt.slice(0, len - (compact ? 2 : 1)); if (kind === 'g') { const se = floatToString(value, fmt + 'ce', true), - sg = floatToString(value, fmt + 'cf', true), - res = se[0].length < sg[0].length || ((sg[0] === '0') && value) ? se : sg; + sg = floatToString(value, fmt + 'cf', true), + res = se[0].length < sg[0].length || ((sg[0] === '0') && value) ? se : sg; return ret_fmt ? res : res[0]; } @@ -124,7 +124,7 @@ function floatToString(value, fmt, ret_fmt) { if (compact) { const pnt = se.indexOf('.'), - pe = se.toLowerCase().indexOf('e'); + pe = se.toLowerCase().indexOf('e'); if ((pnt > 0) && (pe > pnt)) { let p = pe; while ((p > pnt) && (se[p - 1] === '0')) @@ -352,8 +352,8 @@ function buildSvgCurve(p, args) { const end_point = (pnt1, pnt2, sign) => { const len = Math.sqrt((pnt2.gry - pnt1.gry) ** 2 + (pnt2.grx - pnt1.grx) ** 2) * args.t, - a2 = Math.atan2(pnt2.dgry, pnt2.dgrx), - a1 = Math.atan2(sign * (pnt2.gry - pnt1.gry), sign * (pnt2.grx - pnt1.grx)); + a2 = Math.atan2(pnt2.dgry, pnt2.dgrx), + a1 = Math.atan2(sign * (pnt2.gry - pnt1.gry), sign * (pnt2.grx - pnt1.grx)); pnt1.dgrx = len * Math.cos(2 * a1 - a2); pnt1.dgry = len * Math.sin(2 * a1 - a2); @@ -420,8 +420,8 @@ function buildSvgCurve(p, args) { for (let n = 1; n < npnts; ++n) { const bin = p[n], - dx = Math.round(bin.grx) - currx, - dy = Math.round(bin.gry) - curry; + dx = Math.round(bin.grx) - currx, + dy = Math.round(bin.gry) - curry; if (dx && dy) { flush(); path += `l${dx},${dy}`; @@ -442,13 +442,13 @@ function buildSvgCurve(p, args) { } else { // build line with trying optimize many vertical moves let currx = Math.round(p[0].grx), curry = Math.round(p[0].gry), - cminy = curry, cmaxy = curry, prevy = curry; + cminy = curry, cmaxy = curry, prevy = curry; for (let n = 1; n < npnts; ++n) { const bin = p[n], - lastx = Math.round(bin.grx), - lasty = Math.round(bin.gry), - dx = lastx - currx; + lastx = Math.round(bin.grx), + lasty = Math.round(bin.gry), + dx = lastx - currx; if (dx === 0) { // if X not change, just remember amplitude and cminy = Math.min(cminy, lasty); @@ -495,14 +495,14 @@ function buildSvgCurve(p, args) { * @private */ function compressSVG(svg) { svg = svg.replace(/url\("#(\w+)"\)/g, 'url(#$1)') // decode all URL - .replace(/ class="\w*"/g, '') // remove all classes - .replace(/ pad="\w*"/g, '') // remove all pad ids - .replace(/ title=""/g, '') // remove all empty titles - .replace(/ style=""/g, '') // remove all empty styles - .replace(/<\/g>/g, '') // remove all empty groups with transform - .replace(/<\/g>/g, '') // remove hidden title - .replace(/<\/g>/g, ''); // remove all empty groups + .replace(/ class="\w*"/g, '') // remove all classes + .replace(/ pad="\w*"/g, '') // remove all pad ids + .replace(/ title=""/g, '') // remove all empty titles + .replace(/ style=""/g, '') // remove all empty styles + .replace(/<\/g>/g, '') // remove all empty groups with transform + .replace(/<\/g>/g, '') // remove hidden title + .replace(/<\/g>/g, ''); // remove all empty groups // remove all empty frame svg, typically appears in 3D drawings, maybe should be improved in frame painter itself svg = svg.replace(/<\/svg>/g, ''); @@ -575,8 +575,8 @@ class BasePainter { return res; const use_enlarge = res.property('use_enlarge'), - layout = res.property('layout') || 'simple', - layout_selector = (layout === 'simple') ? '' : res.property('layout_selector'); + layout = res.property('layout') || 'simple', + layout_selector = (layout === 'simple') ? '' : res.property('layout_selector'); if (layout_selector) res = res.select(layout_selector); @@ -644,17 +644,17 @@ class BasePainter { * @private */ testMainResize(check_level, new_size, height_factor) { const enlarge = this.enlargeMain('state'), - origin = this.selectDom('origin'), - main = this.selectDom(), - lmt = 5; // minimal size + origin = this.selectDom('origin'), + main = this.selectDom(), + lmt = 5; // minimal size if ((enlarge !== 'on') && new_size?.width && new_size?.height) { origin.style('width', new_size.width + 'px') - .style('height', new_size.height + 'px'); + .style('height', new_size.height + 'px'); } const rect_origin = getElementRect(origin, true), - can_resize = origin.attr('can_resize'); + can_resize = origin.attr('can_resize'); let do_resize = false; if ((can_resize === 'height') && height_factor && Math.abs(rect_origin.width * height_factor - rect_origin.height) > 0.1 * rect_origin.width) @@ -674,8 +674,8 @@ class BasePainter { } const rect = getElementRect(main), - old_h = main.property('_jsroot_height'), - old_w = main.property('_jsroot_width'); + old_h = main.property('_jsroot_height'), + old_w = main.property('_jsroot_width'); rect.changed = false; @@ -717,8 +717,8 @@ class BasePainter { * @protected */ enlargeMain(action, skip_warning) { const main = this.selectDom(true), - origin = this.selectDom('origin'), - doc = getDocument(); + origin = this.selectDom('origin'), + doc = getDocument(); if (main.empty() || !settings.CanEnlarge || (origin.property('can_enlarge') === false)) return false; @@ -746,7 +746,7 @@ class BasePainter { .attr('style', 'position: fixed; margin: 0px; border: 0px; padding: 0px; left: 1px; top: 1px; bottom: 1px; right: 1px; background: white; opacity: 0.95; z-index: 100; overflow: hidden;'); const rect1 = getElementRect(main), - rect2 = getElementRect(enlarge); + rect2 = getElementRect(enlarge); // if new enlarge area not big enough, do not do it if ((rect2.width <= rect1.width) || (rect2.height <= rect1.height)) { @@ -846,11 +846,11 @@ function makeTranslate(g, x, y, scale = 1) { function addHighlightStyle(elem, drag) { if (drag) { elem.style('stroke', 'steelblue') - .style('fill-opacity', '0.1'); + .style('fill-opacity', '0.1'); } else { elem.style('stroke', '#4572A7') - .style('fill', '#4572A7') - .style('opacity', '0'); + .style('fill', '#4572A7') + .style('opacity', '0'); } } @@ -882,37 +882,19 @@ async function svgToImage(svg, image_format, args) { }); const img_src = 'data:image/svg+xml;base64,' + btoa_func(decodeURIComponent(svg)); - const RESVG_FEATURE_FLAG = true; - - if (RESVG_FEATURE_FLAG) { - return import('@resvg/resvg-js').then(({ Resvg }) => { - const rawSvg = decodeURIComponent(svg); - const resvg = new Resvg(rawSvg); + return import('canvas').then(async handle => { + return handle.default.loadImage(img_src).then(img => { + const canvas = handle.default.createCanvas(img.width, img.height); - const pngData = resvg.render(); - const pngBuffer = pngData.asPng(); + canvas.getContext('2d').drawImage(img, 0, 0, img.width, img.height); - if (args?.as_buffer) { - return pngBuffer; - } + if (args?.as_buffer) + return canvas.toBuffer('image/' + image_format); - return 'data:image/png;base64,' + pngBuffer.toString('base64'); + return image_format ? canvas.toDataURL('image/' + image_format) : canvas; }); - } else { - 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); - - if (args?.as_buffer) - return canvas.toBuffer('image/' + image_format); - - 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' })); @@ -948,11 +930,11 @@ async function svgToImage(svg, image_format, args) { * @desc Always use UTC to avoid any variation between timezones */ function getTDatime(dt) { const y = (dt.fDatime >>> 26) + 1995, - m = ((dt.fDatime << 6) >>> 28) - 1, - d = (dt.fDatime << 10) >>> 27, - h = (dt.fDatime << 15) >>> 27, - min = (dt.fDatime << 20) >>> 26, - s = (dt.fDatime << 26) >>> 26; + m = ((dt.fDatime << 6) >>> 28) - 1, + d = (dt.fDatime << 10) >>> 27, + h = (dt.fDatime << 15) >>> 27, + min = (dt.fDatime << 20) >>> 26, + s = (dt.fDatime << 26) >>> 26; return new Date(Date.UTC(y, m, d, h, min, s)); } @@ -975,11 +957,11 @@ function convertDate(dt) { * @private */ function getBoxDecorations(xx, yy, ww, hh, bmode, pww, phh) { const side1 = `M${xx},${yy}h${ww}l${-pww},${phh}h${2 * pww - ww}v${hh - 2 * phh}l${-pww},${phh}z`, - side2 = `M${xx + ww},${yy + hh}v${-hh}l${-pww},${phh}v${hh - 2 * phh}h${2 * pww - ww}l${-pww},${phh}z`; + side2 = `M${xx + ww},${yy + hh}v${-hh}l${-pww},${phh}v${hh - 2 * phh}h${2 * pww - ww}l${-pww},${phh}z`; return bmode > 0 ? [side1, side2] : [side2, side1]; } export { prSVG, prJSON, getElementRect, getAbsPosInCanvas, getTDatime, convertDate, - DrawOptions, TRandom, floatToString, buildSvgCurve, compressSVG, getBoxDecorations, + DrawOptions, TRandom, floatToString, buildSvgCurve, compressSVG, getBoxDecorations, BasePainter, _loadJSDOM, makeTranslate, addHighlightStyle, svgToImage }; From 70f9a1808f8833dccea3a33b13fa84326a7b4372 Mon Sep 17 00:00:00 2001 From: OmarMesqq Date: Fri, 30 Jan 2026 12:59:40 -0300 Subject: [PATCH 4/8] refactor: :recycle: add feature flag for using resvgjs to correctly generate pngs --- modules/base/BasePainter.mjs | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/modules/base/BasePainter.mjs b/modules/base/BasePainter.mjs index f64398ca0..d616dc976 100644 --- a/modules/base/BasePainter.mjs +++ b/modules/base/BasePainter.mjs @@ -882,19 +882,37 @@ async function svgToImage(svg, image_format, args) { }); const img_src = 'data:image/svg+xml;base64,' + btoa_func(decodeURIComponent(svg)); + const RESVG_FEATURE_FLAG = true; + + if (RESVG_FEATURE_FLAG) { + return import('@resvg/resvg-js').then(({ Resvg }) => { + const rawSvg = decodeURIComponent(svg); - return import('canvas').then(async handle => { - return handle.default.loadImage(img_src).then(img => { - const canvas = handle.default.createCanvas(img.width, img.height); + const resvg = new Resvg(rawSvg); - canvas.getContext('2d').drawImage(img, 0, 0, img.width, img.height); + const pngData = resvg.render(); + const pngBuffer = pngData.asPng(); - if (args?.as_buffer) - return canvas.toBuffer('image/' + image_format); + if (args?.as_buffer) { + return pngBuffer; + } - return image_format ? canvas.toDataURL('image/' + image_format) : canvas; + return 'data:image/png;base64,' + pngBuffer.toString('base64'); }); - }); + } else { + 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); + + if (args?.as_buffer) + return canvas.toBuffer('image/' + image_format); + + 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' })); From 675c71147c8bde59c2a81817aa81c414baf988f2 Mon Sep 17 00:00:00 2001 From: OmarMesqq Date: Fri, 30 Jan 2026 13:45:20 -0300 Subject: [PATCH 5/8] docs: :memo: add comments to new `resvg-js` branch in SVG->PNG transformation branch --- modules/base/BasePainter.mjs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/modules/base/BasePainter.mjs b/modules/base/BasePainter.mjs index d616dc976..918cb5d8a 100644 --- a/modules/base/BasePainter.mjs +++ b/modules/base/BasePainter.mjs @@ -884,12 +884,13 @@ async function svgToImage(svg, image_format, args) { const img_src = 'data:image/svg+xml;base64,' + btoa_func(decodeURIComponent(svg)); const RESVG_FEATURE_FLAG = true; + // Use the newer and stabler `resvg-js` backend for converting SVG to PNG if (RESVG_FEATURE_FLAG) { return import('@resvg/resvg-js').then(({ Resvg }) => { - const rawSvg = decodeURIComponent(svg); + const rawSvg = decodeURIComponent(svg); // raw SVG XML + // Initialize Resvg and create the PNG buffer const resvg = new Resvg(rawSvg); - const pngData = resvg.render(); const pngBuffer = pngData.asPng(); @@ -899,7 +900,7 @@ async function svgToImage(svg, image_format, args) { return 'data:image/png;base64,' + pngBuffer.toString('base64'); }); - } else { + } 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); From 663e264e7699b5e5e0eec205beb6db835440aa85 Mon Sep 17 00:00:00 2001 From: OmarMesqq Date: Sun, 1 Feb 2026 14:34:48 -0300 Subject: [PATCH 6/8] refactor: :recycle: add `UseResvgJs` to global `constants` object --- modules/base/BasePainter.mjs | 5 ++--- modules/core.mjs | 4 ++++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/modules/base/BasePainter.mjs b/modules/base/BasePainter.mjs index 918cb5d8a..377a1a1e7 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,10 +882,9 @@ async function svgToImage(svg, image_format, args) { }); const img_src = 'data:image/svg+xml;base64,' + btoa_func(decodeURIComponent(svg)); - const RESVG_FEATURE_FLAG = true; // Use the newer and stabler `resvg-js` backend for converting SVG to PNG - if (RESVG_FEATURE_FLAG) { + if (constants.Embed3D.UseResvgJs || constants.Render3D.UseResvgJs) { return import('@resvg/resvg-js').then(({ Resvg }) => { const rawSvg = decodeURIComponent(svg); // raw SVG XML 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') From eef01d0c61486b11d4e965c9bf2e292a6eba90a5 Mon Sep 17 00:00:00 2001 From: OmarMesqq Date: Sun, 1 Feb 2026 14:36:25 -0300 Subject: [PATCH 7/8] refactor: :recycle: accept `'rgba'` as `image_format` arg in `svgToImage` this returns the raw RGBA pixels made by `resvg-js` --- modules/base/BasePainter.mjs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/modules/base/BasePainter.mjs b/modules/base/BasePainter.mjs index 377a1a1e7..b5f289535 100644 --- a/modules/base/BasePainter.mjs +++ b/modules/base/BasePainter.mjs @@ -890,8 +890,17 @@ async function svgToImage(svg, image_format, args) { // Initialize Resvg and create the PNG buffer const resvg = new Resvg(rawSvg); - const pngData = resvg.render(); - const pngBuffer = pngData.asPng(); + 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; From 3ec323c58d66aa82c826d390154c11258c6b77d6 Mon Sep 17 00:00:00 2001 From: OmarMesqq Date: Sun, 1 Feb 2026 14:37:16 -0300 Subject: [PATCH 8/8] fix: :bug: make `autoPlaceLegend` handle both `node-canvas` and `resvg-js` images --- modules/hist/TPavePainter.mjs | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) 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;