import { select as d3_select } from '../d3.mjs';
import { settings, internals, isNodeJs, isFunc, isStr, isObject, btoa_func, getDocument, source_dir, loadScript, httpRequest } from '../core.mjs';
import { detectFont, addCustomFont, getCustomFont, FontHandler } from './FontHandler.mjs';
import { approximateLabelWidth, replaceSymbolsInTextNode } from './latex.mjs';
import { getColor } from './colors.mjs';
/** @summary Returns visible rect of element
* @param {object} elem - d3.select object with element
* @param {string} [kind] - which size method is used
* @desc kind = 'bbox' use getBBox, works only with SVG
* kind = 'full' - full size of element, using getBoundingClientRect function
* kind = 'nopadding' - excludes padding area
* With node.js can use 'width' and 'height' attributes when provided in element
* @private */
function getElementRect(elem, sizearg) {
if (!elem || elem.empty())
return { x: 0, y: 0, width: 0, height: 0 };
if ((isNodeJs() && (sizearg !== 'bbox')) || elem.property('_batch_mode'))
return { x: 0, y: 0, width: parseInt(elem.attr('width')), height: parseInt(elem.attr('height')) };
const styleValue = name => {
let value = elem.style(name);
if (!value || !isStr(value)) return 0;
value = parseFloat(value.replace('px', ''));
return !Number.isFinite(value) ? 0 : Math.round(value);
};
let rect = elem.node().getBoundingClientRect();
if ((sizearg === 'bbox') && (parseFloat(rect.width) > 0))
rect = elem.node().getBBox();
const res = { x: 0, y: 0, width: parseInt(rect.width), height: parseInt(rect.height) };
if (rect.left !== undefined) {
res.x = parseInt(rect.left);
res.y = parseInt(rect.top);
} else if (rect.x !== undefined) {
res.x = parseInt(rect.x);
res.y = parseInt(rect.y);
}
if ((sizearg === undefined) || (sizearg === 'nopadding')) {
// this is size exclude padding area
res.width -= styleValue('padding-left') + styleValue('padding-right');
res.height -= styleValue('padding-top') + styleValue('padding-bottom');
}
return res;
}
/** @summary Calculate absolute position of provided element in canvas
* @private */
function getAbsPosInCanvas(sel, pos) {
if (!pos) return pos;
while (!sel.empty() && !sel.classed('root_canvas')) {
const cl = sel.attr('class');
if (cl && ((cl.indexOf('root_frame') >= 0) || (cl.indexOf('__root_pad_') >= 0))) {
pos.x += sel.property('draw_x') || 0;
pos.y += sel.property('draw_y') || 0;
}
sel = d3_select(sel.node().parentNode);
}
return pos;
}
/** @summary Converts numeric value to string according to specified format.
* @param {number} value - value to convert
* @param {string} [fmt='6.4g'] - format can be like 5.4g or 4.2e or 6.4f
* @param {boolean} [ret_fmt] - when true returns array with value and actual format like ['0.1','6.4f']
* @return {string|Array} - converted value or array with value and actual format
* @private */
function floatToString(value, fmt, ret_fmt) {
if (!fmt) fmt = '6.4g';
fmt = fmt.trim();
const len = fmt.length;
if (len < 2)
return ret_fmt ? [value.toFixed(4), '6.4f'] : value.toFixed(4);
const last = fmt[len-1];
fmt = fmt.slice(0, len-1);
let isexp, prec = fmt.indexOf('.');
prec = (prec < 0) ? 4 : parseInt(fmt.slice(prec+1));
if (!Number.isInteger(prec) || (prec <= 0)) prec = 4;
let significance = false;
if ((last === 'e') || (last === 'E')) isexp = true; else
if (last === 'Q') { isexp = true; significance = true; } else
if ((last === 'f') || (last === 'F')) isexp = false; else
if (last === 'W') { isexp = false; significance = true; } else
if ((last === 'g') || (last === 'G')) {
const se = floatToString(value, fmt+'Q', true);
let sg = floatToString(value, fmt+'W', true);
if (se[0].length < sg[0].length) sg = se;
return ret_fmt ? sg : sg[0];
} else {
isexp = false;
prec = 4;
}
if (isexp) {
// for exponential representation only one significant digit befor point
if (significance) prec--;
if (prec < 0) prec = 0;
const se = value.toExponential(prec);
return ret_fmt ? [se, `5.${prec}e`] : se;
}
let sg = value.toFixed(prec);
if (significance) {
// when using fixed representation, one could get 0
if ((value !== 0) && (Number(sg) === 0) && (prec > 0)) {
prec = 20; sg = value.toFixed(prec);
}
let l = 0;
while ((l < sg.length) && (sg[l] === '0' || sg[l] === '-' || sg[l] === '.')) l++;
let diff = sg.length - l - prec;
if (sg.indexOf('.') > l) diff--;
if (diff !== 0) {
prec -= diff;
if (prec < 0)
prec = 0;
else if (prec > 20)
prec = 20;
sg = value.toFixed(prec);
}
}
return ret_fmt ? [sg, '5.'+prec+'f'] : sg;
}
/** @summary Draw options interpreter
* @private */
class DrawOptions {
constructor(opt) {
this.opt = isStr(opt) ? opt.toUpperCase().trim() : '';
this.part = '';
}
/** @summary Returns true if remaining options are empty or contain only seperators symbols. */
empty() {
if (this.opt.length === 0) return true;
return this.opt.replace(/[ ;_,]/g, '').length === 0;
}
/** @summary Returns remaining part of the draw options. */
remain() { return this.opt; }
/** @summary Checks if given option exists */
check(name, postpart) {
const pos = this.opt.indexOf(name);
if (pos < 0) return false;
this.opt = this.opt.slice(0, pos) + this.opt.slice(pos + name.length);
this.part = '';
if (!postpart) return true;
let pos2 = pos;
while ((pos2 < this.opt.length) && (this.opt[pos2] !== ' ') && (this.opt[pos2] !== ',') && (this.opt[pos2] !== ';')) pos2++;
if (pos2 > pos) {
this.part = this.opt.slice(pos, pos2);
this.opt = this.opt.slice(0, pos) + this.opt.slice(pos2);
}
if (postpart !== 'color')
return true;
this.color = this.partAsInt(1) - 1;
if (this.color >= 0) return true;
for (let col = 0; col < 8; ++col) {
if (getColor(col).toUpperCase() === this.part) {
this.color = col;
return true;
}
}
return false;
}
/** @summary Returns remaining part of found option as integer. */
partAsInt(offset, dflt) {
let mult = 1;
const last = this.part ? this.part[this.part.length - 1] : '';
if (last === 'K')
mult = 1e3;
else if (last === 'M')
mult = 1e6;
else if (last === 'G')
mult = 1e9;
let val = this.part.replace(/^\D+/g, '');
val = val ? parseInt(val, 10) : Number.NaN;
return !Number.isInteger(val) ? (dflt || 0) : mult*val + (offset || 0);
}
/** @summary Returns remaining part of found option as float. */
partAsFloat(offset, dflt) {
let val = this.part.replace(/^\D+/g, '');
val = val ? parseFloat(val) : Number.NaN;
return !Number.isFinite(val) ? (dflt || 0) : val + (offset || 0);
}
} // class DrawOptions
/** @summary Simple random generator with controlled seed
* @private */
class TRandom {
constructor(i) {
if (i !== undefined) this.seed(i);
}
/** @summary Seed simple random generator */
seed(i) {
i = Math.abs(i);
if (i > 1e8)
i = Math.abs(1e8 * Math.sin(i));
else if (i < 1)
i *= 1e8;
this.m_w = Math.round(i);
this.m_z = 987654321;
}
/** @summary Produce random value between 0 and 1 */
random() {
if (this.m_z === undefined) return Math.random();
this.m_z = (36969 * (this.m_z & 65535) + (this.m_z >> 16)) & 0xffffffff;
this.m_w = (18000 * (this.m_w & 65535) + (this.m_w >> 16)) & 0xffffffff;
let result = ((this.m_z << 16) + this.m_w) & 0xffffffff;
result /= 4294967296;
return result + 0.5;
}
} // class TRandom
/** @summary Build smooth SVG curve uzing Bezier
* @desc Reuse code from https://stackoverflow.com/questions/62855310
* @private */
function buildSvgCurve(p, args) {
if (!args)
args = {};
if (!args.line)
args.calc = true;
else if (args.ndig === undefined)
args.ndig = 0;
let npnts = p.length;
if (npnts < 3) args.line = true;
args.t = args.t ?? 0.2;
if ((args.ndig === undefined) || args.height) {
args.maxy = p[0].gry;
args.mindiff = 100;
for (let i = 1; i < npnts; i++) {
args.maxy = Math.max(args.maxy, p[i].gry);
args.mindiff = Math.min(args.mindiff, Math.abs(p[i].grx - p[i-1].grx), Math.abs(p[i].gry - p[i-1].gry));
}
if (args.ndig === undefined)
args.ndig = args.mindiff > 20 ? 0 : (args.mindiff > 5 ? 1 : 2);
}
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));
pnt1.dgrx = len * Math.cos(2*a1 - a2);
pnt1.dgry = len * Math.sin(2*a1 - a2);
}, conv = val => {
if (!args.ndig || (Math.round(val) === val))
return val.toFixed(0);
let s = val.toFixed(args.ndig), p = s.length-1;
while (s[p] === '0') p--;
if (s[p] === '.') p--;
s = s.slice(0, p+1);
return (s === '-0') ? '0' : s;
};
if (args.calc) {
for (let i = 1; i < npnts - 1; i++) {
p[i].dgrx = (p[i+1].grx - p[i-1].grx) * args.t;
p[i].dgry = (p[i+1].gry - p[i-1].gry) * args.t;
}
if (npnts > 2) {
end_point(p[0], p[1], 1);
end_point(p[npnts - 1], p[npnts - 2], -1);
} else if (p.length === 2) {
p[0].dgrx = (p[1].grx - p[0].grx) * args.t;
p[0].dgry = (p[1].gry - p[0].gry) * args.t;
p[1].dgrx = -p[0].dgrx;
p[1].dgry = -p[0].dgry;
}
}
let path = `${args.cmd ?? 'M'}${conv(p[0].grx)},${conv(p[0].gry)}`;
if (!args.line) {
let i0 = 1;
if (args.qubic) {
npnts--; i0++;
path += `Q${conv(p[1].grx-p[1].dgrx)},${conv(p[1].gry-p[1].dgry)},${conv(p[1].grx)},${conv(p[1].gry)}`;
}
path += `C${conv(p[i0-1].grx+p[i0-1].dgrx)},${conv(p[i0-1].gry+p[i0-1].dgry)},${conv(p[i0].grx-p[i0].dgrx)},${conv(p[i0].gry-p[i0].dgry)},${conv(p[i0].grx)},${conv(p[i0].gry)}`;
// continue with simpler points
for (let i = i0 + 1; i < npnts; i++)
path += `S${conv(p[i].grx-p[i].dgrx)},${conv(p[i].gry-p[i].dgry)},${conv(p[i].grx)},${conv(p[i].gry)}`;
if (args.qubic)
path += `Q${conv(p[npnts].grx-p[npnts].dgrx)},${conv(p[npnts].gry-p[npnts].dgry)},${conv(p[npnts].grx)},${conv(p[npnts].gry)}`;
} else if (npnts < 10000) {
// build simple curve
let acc_x = 0, acc_y = 0, currx = Math.round(p[0].grx), curry = Math.round(p[0].gry);
const flush = () => {
if (acc_x) { path += 'h' + acc_x; acc_x = 0; }
if (acc_y) { path += 'v' + acc_y; acc_y = 0; }
};
for (let n = 1; n < npnts; ++n) {
const bin = p[n],
dx = Math.round(bin.grx) - currx,
dy = Math.round(bin.gry) - curry;
if (dx && dy) {
flush();
path += `l${dx},${dy}`;
} else if (!dx && dy) {
if ((acc_y === 0) || ((dy < 0) !== (acc_y < 0))) flush();
acc_y += dy;
} else if (dx && !dy) {
if ((acc_x === 0) || ((dx < 0) !== (acc_x < 0))) flush();
acc_x += dx;
}
currx += dx; curry += dy;
}
flush();
} 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;
for (let n = 1; n < npnts; ++n) {
const bin = p[n],
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);
cmaxy = Math.max(cmaxy, lasty);
prevy = lasty;
continue;
}
if (cminy !== cmaxy) {
if (cminy !== curry)
path += `v${cminy-curry}`;
path += `v${cmaxy-cminy}`;
if (cmaxy !== prevy)
path += `v${prevy-cmaxy}`;
curry = prevy;
}
const dy = lasty - curry;
if (dy)
path += `l${dx},${dy}`;
else
path += `h${dx}`;
currx = lastx; curry = lasty;
prevy = cminy = cmaxy = lasty;
}
if (cminy !== cmaxy) {
if (cminy !== curry)
path += `v${cminy-curry}`;
path += `v${cmaxy-cminy}`;
if (cmaxy !== prevy)
path += `v${prevy-cmaxy}`;
}
}
if (args.height)
args.close = `L${conv(p[p.length-1].grx)},${conv(Math.max(args.maxy, args.height))}H${conv(p[0].grx)}Z`;
return path;
}
/** @summary Compress SVG code, produced from drawing
* @desc removes extra info or empty elements
* @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(/<g objname="\w*" objtype="\w*"/g, '<g') // remove object ids
.replace(/<g transform="translate\(\d+,\d+\)"><\/g>/g, '') // remove all empty groups with transform
.replace(/<g transform="translate\(\d+,\d+\)" style="display: none;"><\/g>/g, '') // remove hidden title
.replace(/<g><\/g>/g, ''); // remove all empty groups
// remove all empty frame svgs, typically appears in 3D drawings, maybe should be improved in frame painter itself
svg = svg.replace(/<svg x="0" y="0" overflow="hidden" width="\d+" height="\d+" viewBox="0 0 \d+ \d+"><\/svg>/g, '');
return svg;
}
/**
* @summary Base painter class
*
*/
class BasePainter {
/** @summary constructor
* @param {object|string} [dom] - dom element or id of dom element */
constructor(dom) {
this.divid = null; // either id of DOM element or element itself
if (dom) this.setDom(dom);
}
/** @summary Assign painter to specified DOM element
* @param {string|object} elem - element ID or DOM Element
* @desc Normally DOM element should be already assigned in constructor
* @protected */
setDom(elem) {
if (elem !== undefined) {
this.divid = elem;
delete this._selected_main;
}
}
/** @summary Returns assigned dom element */
getDom() {
return this.divid;
}
/** @summary Selects main HTML element assigned for drawing
* @desc if main element was layouted, returns main element inside layout
* @param {string} [is_direct] - if 'origin' specified, returns original element even if actual drawing moved to some other place
* @return {object} d3.select object for main element for drawing */
selectDom(is_direct) {
if (!this.divid) return d3_select(null);
let res = this._selected_main;
if (!res) {
if (isStr(this.divid)) {
let id = this.divid;
if (id[0] !== '#') id = '#' + id;
res = d3_select(id);
if (!res.empty()) this.divid = res.node();
} else
res = d3_select(this.divid);
this._selected_main = res;
}
if (!res || res.empty() || (is_direct === 'origin')) return res;
const use_enlarge = res.property('use_enlarge'),
layout = res.property('layout') || 'simple',
layout_selector = (layout === 'simple') ? '' : res.property('layout_selector');
if (layout_selector)
res = res.select(layout_selector);
// one could redirect here
if (!is_direct && !res.empty() && use_enlarge)
res = d3_select(getDocument().getElementById('jsroot_enlarge_div'));
return res;
}
/** @summary Access/change top painter
* @private */
_accessTopPainter(on) {
const chld = this.selectDom().node()?.firstChild;
if (!chld) return null;
if (on === true)
chld.painter = this;
else if (on === false)
delete chld.painter;
return chld.painter;
}
/** @summary Set painter, stored in first child element
* @desc Only make sense after first drawing is completed and any child element add to configured DOM
* @protected */
setTopPainter() {
this._accessTopPainter(true);
}
/** @summary Return top painter set for the selected dom element
* @protected */
getTopPainter() {
return this._accessTopPainter();
}
/** @summary Clear reference on top painter
* @protected */
clearTopPainter() {
this._accessTopPainter(false);
}
/** @summary Generic method to cleanup painter
* @desc Removes all visible elements and all internal data */
cleanup(keep_origin) {
this.clearTopPainter();
const origin = this.selectDom('origin');
if (!origin.empty() && !keep_origin) origin.html('');
this.divid = null;
delete this._selected_main;
if (isFunc(this._hpainter?.removePainter))
this._hpainter.removePainter(this);
delete this._hitemname;
delete this._hdrawopt;
delete this._hpainter;
}
/** @summary Checks if draw elements were resized and drawing should be updated
* @return {boolean} true if resize was detected
* @protected
* @abstract */
checkResize(/* arg */) {}
/** @summary Function checks if geometry of main div was changed.
* @desc take into account enlarge state, used only in PadPainter class
* @return size of area when main div is drawn
* @private */
testMainResize(check_level, new_size, height_factor) {
const enlarge = this.enlargeMain('state'),
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');
}
const rect_origin = getElementRect(origin, true),
can_resize = origin.attr('can_resize');
let do_resize = false;
if (can_resize === 'height')
if (height_factor && Math.abs(rect_origin.width * height_factor - rect_origin.height) > 0.1 * rect_origin.width) do_resize = true;
if (((rect_origin.height <= lmt) || (rect_origin.width <= lmt)) &&
can_resize && can_resize !== 'false') do_resize = true;
if (do_resize && (enlarge !== 'on')) {
// if zero size and can_resize attribute set, change container size
if (rect_origin.width > lmt) {
height_factor = height_factor || 0.66;
origin.style('height', Math.round(rect_origin.width * height_factor) + 'px');
} else if (can_resize !== 'height')
origin.style('width', '200px').style('height', '100px');
}
const rect = getElementRect(main),
old_h = main.property('_jsroot_height'),
old_w = main.property('_jsroot_width');
rect.changed = false;
if (old_h && old_w && (old_h > 0) && (old_w > 0)) {
if ((old_h !== rect.height) || (old_w !== rect.width))
rect.changed = (check_level > 1) || (rect.width / old_w < 0.99) || (rect.width / old_w > 1.01) || (rect.height / old_h < 0.99) || (rect.height / old_h > 1.01);
} else
rect.changed = true;
if (rect.changed)
main.property('_jsroot_height', rect.height).property('_jsroot_width', rect.width);
// after change enlarge state always mark main element as resized
if (origin.property('did_enlarge')) {
rect.changed = true;
origin.property('did_enlarge', false);
}
return rect;
}
/** @summary Try enlarge main drawing element to full HTML page.
* @param {string|boolean} action - defines that should be done
* @desc Possible values for action parameter:
* - true - try to enlarge
* - false - revert enlarge state
* - 'toggle' - toggle enlarge state
* - 'state' - only returns current enlarge state
* - 'verify' - check if element can be enlarged
* if action not specified, just return possibility to enlarge main div
* @protected */
enlargeMain(action, skip_warning) {
const main = this.selectDom(true),
origin = this.selectDom('origin'),
doc = getDocument();
if (main.empty() || !settings.CanEnlarge || (origin.property('can_enlarge') === false)) return false;
if ((action === undefined) || (action === 'verify')) return true;
const state = origin.property('use_enlarge') ? 'on' : 'off';
if (action === 'state') return state;
if (action === 'toggle') action = (state === 'off');
let enlarge = d3_select(doc.getElementById('jsroot_enlarge_div'));
if ((action === true) && (state !== 'on')) {
if (!enlarge.empty()) return false;
enlarge = d3_select(doc.body)
.append('div')
.attr('id', 'jsroot_enlarge_div')
.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);
// if new enlarge area not big enough, do not do it
if ((rect2.width <= rect1.width) || (rect2.height <= rect1.height)) {
if (rect2.width * rect2.height < rect1.width * rect1.height) {
if (!skip_warning)
console.log(`Enlarged area ${rect2.width} x ${rect2.height} smaller then original drawing ${rect1.width} x ${rect1.height}`);
enlarge.remove();
return false;
}
}
while (main.node().childNodes.length > 0)
enlarge.node().appendChild(main.node().firstChild);
origin.property('use_enlarge', true);
origin.property('did_enlarge', true);
return true;
}
if ((action === false) && (state !== 'off')) {
while (enlarge.node() && enlarge.node().childNodes.length > 0)
main.node().appendChild(enlarge.node().firstChild);
enlarge.remove();
origin.property('use_enlarge', false);
origin.property('did_enlarge', true);
return true;
}
return false;
}
/** @summary Set item name, associated with the painter
* @desc Used by {@link HierarchyPainter}
* @private */
setItemName(name, opt, hpainter) {
if (isStr(name))
this._hitemname = name;
else
delete this._hitemname;
// only upate draw option, never delete.
if (isStr(opt))
this._hdrawopt = opt;
this._hpainter = hpainter;
}
/** @summary Returns assigned item name
* @desc Used with {@link HierarchyPainter} to identify drawn item name */
getItemName() { return this._hitemname ?? null; }
/** @summary Returns assigned item draw option
* @desc Used with {@link HierarchyPainter} to identify drawn item option */
getItemDrawOpt() { return this._hdrawopt ?? ''; }
} // class BasePainter
/** @summary Load and initialize JSDOM from nodes
* @return {Promise} with d3 selection for d3_body
* @private */
async function _loadJSDOM() {
return import('jsdom').then(handle => {
if (!internals.nodejs_window) {
internals.nodejs_window = (new handle.JSDOM('<!DOCTYPE html>hello')).window;
internals.nodejs_document = internals.nodejs_window.document; // used with three.js
internals.nodejs_body = d3_select(internals.nodejs_document).select('body'); // get d3 handle for body
}
return { JSDOM: handle.JSDOM, doc: internals.nodejs_document, body: internals.nodejs_body };
});
}
/** @summary Return translate string for transform attribute of some svg element
* @return string or null if x and y are zeros
* @private */
function makeTranslate(g, x, y) {
if (!isObject(g)) {
y = x; x = g; g = null;
}
const res = y ? `translate(${x},${y})` : (x ? `translate(${x})` : null);
return g ? g.attr('transform', res) : res;
}
/** @summary Configure special style used for highlight or dragging elements
* @private */
function addHighlightStyle(elem, drag) {
if (drag) {
elem.style('stroke', 'steelblue')
.style('fill-opacity', '0.1');
} else {
elem.style('stroke', '#4572A7')
.style('fill', '#4572A7')
.style('opacity', '0');
}
}
/** @summary Create pdf for existing SVG element
* @return {Promise} with produced PDF file as url string
* @private */
async function svgToPDF(args, as_buffer) {
const nodejs = isNodeJs();
let _jspdf, _svg2pdf, need_symbols = false;
const pr = nodejs
? import('../../scripts/jspdf.es.min.js').then(h1 => { _jspdf = h1; return import('../../scripts/svg2pdf.es.min.js'); }).then(h2 => { _svg2pdf = h2; })
: loadScript(source_dir + 'scripts/jspdf.umd.min.js').then(() => loadScript(source_dir + 'scripts/svg2pdf.umd.min.js')).then(() => { _jspdf = globalThis.jspdf; _svg2pdf = globalThis.svg2pdf; }),
restore_fonts = [], restore_dominant = [], restore_text = [],
node_transform = args.node.getAttribute('transform'), custom_fonts = {};
if (args.reset_tranform)
args.node.removeAttribute('transform');
return pr.then(() => {
d3_select(args.node).selectAll('g').each(function() {
if (this.hasAttribute('font-family')) {
const name = this.getAttribute('font-family');
if (name === 'Courier New') {
this.setAttribute('font-family', 'courier');
if (!args.can_modify) restore_fonts.push(this); // keep to restore it
}
}
});
d3_select(args.node).selectAll('text').each(function() {
if (this.hasAttribute('dominant-baseline')) {
this.setAttribute('dy', '.2em'); // slightly different as in plain text
this.removeAttribute('dominant-baseline');
if (!args.can_modify) restore_dominant.push(this); // keep to restore it
} else if (args.can_modify && nodejs && this.getAttribute('dy') === '.4em')
this.setAttribute('dy', '.2em'); // better allignment in PDF
if (replaceSymbolsInTextNode(this)) {
need_symbols = true;
if (!args.can_modify) restore_text.push(this); // keep to restore it
}
});
if (nodejs) {
const doc = internals.nodejs_document;
doc.oldFunc = doc.createElementNS;
globalThis.document = doc;
globalThis.CSSStyleSheet = internals.nodejs_window.CSSStyleSheet;
globalThis.CSSStyleRule = internals.nodejs_window.CSSStyleRule;
doc.createElementNS = function(ns, kind) {
const res = doc.oldFunc(ns, kind);
res.getBBox = function() {
let width = 50, height = 10;
if (this.tagName === 'text') {
// TODO: use jsDOC fonts for label width estimation
const font = detectFont(this);
width = approximateLabelWidth(this.textContent, font);
height = font.size;
}
return { x: 0, y: 0, width, height };
};
return res;
};
}
// eslint-disable-next-line new-cap
const doc = new _jspdf.jsPDF({
orientation: 'landscape',
unit: 'px',
format: [args.width + 10, args.height + 10]
});
// add custom fonts to PDF document, only TTF format supported
d3_select(args.node).selectAll('style').each(function() {
const fh = this.$fonthandler;
if (!fh || custom_fonts[fh.name] || (fh.format !== 'ttf')) return;
const filename = fh.name.toLowerCase().replace(/\s/g, '') + '.ttf';
doc.addFileToVFS(filename, fh.base64);
doc.addFont(filename, fh.name, 'normal', 'normal', (fh.name === 'symbol') ? 'StandardEncoding' : 'Identity-H');
custom_fonts[fh.name] = true;
});
let pr2 = Promise.resolve(true);
if (need_symbols && !custom_fonts.symbol) {
if (!getCustomFont('symbol')) {
pr2 = nodejs
? import('fs').then(fs => {
const base64 = fs.readFileSync('../../fonts/symbol.ttf').toString('base64');
console.log('reading symbol.ttf', base64.length);
addCustomFont(25, 'symbol', 'ttf', base64);
})
: httpRequest(source_dir+'fonts/symbol.ttf', 'bin').then(buf => {
const base64 = btoa_func(buf);
addCustomFont(25, 'symbol', 'ttf', base64);
});
}
pr2 = pr2.then(() => {
const fh = getCustomFont('symbol'),
handler = new FontHandler(1242, 10);
handler.name = 'symbol';
handler.base64 = fh.base64;
handler.addCustomFontToSvg(d3_select(args.node));
doc.addFileToVFS('symbol.ttf', fh.base64);
doc.addFont('symbol.ttf', 'symbol', 'normal', 'normal', 'StandardEncoding' /* 'WinAnsiEncoding' */);
});
}
return pr2.then(() => _svg2pdf.svg2pdf(args.node, doc, { x: 5, y: 5, width: args.width, height: args.height }))
.then(() => {
if (args.reset_tranform && !args.can_modify && node_transform)
args.node.setAttribute('transform', node_transform);
restore_fonts.forEach(node => node.setAttribute('font-family', 'Courier New'));
restore_dominant.forEach(node => {
node.setAttribute('dominant-baseline', 'middle');
node.removeAttribute('dy');
});
restore_text.forEach(node => { node.innerHTML = node.$originalHTML; });
const res = as_buffer ? doc.output('arraybuffer') : doc.output('dataurlstring');
if (nodejs) {
globalThis.document = undefined;
globalThis.CSSStyleSheet = undefined;
globalThis.CSSStyleRule = undefined;
internals.nodejs_document.createElementNS = internals.nodejs_document.oldFunc;
if (as_buffer) return Buffer.from(res);
}
return res;
});
});
}
/** @summary Create image based on SVG
* @param {string} svg - svg code of the image
* @param {string} [image_format] - image format like 'png', 'jpeg' or 'webp'
* @param {boolean} [as_buffer] - return Buffer object for image
* @return {Promise} with produced image in base64 form or as Buffer (or canvas when no image_format specified)
* @private */
async function svgToImage(svg, image_format, as_buffer) {
if (image_format === 'svg')
return svg;
if (image_format === 'pdf')
return svgToPDF(svg, as_buffer);
// required with df104.py/df105.py example with RCanvas or any special symbols in TLatex
const doctype = '<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">';
svg = encodeURIComponent(doctype + svg);
svg = svg.replace(/%([0-9A-F]{2})/g, (match, p1) => {
const c = String.fromCharCode('0x'+p1);
return c === '%' ? '%25' : c;
});
svg = decodeURIComponent(svg);
const img_src = 'data:image/svg+xml;base64,' + btoa_func(svg);
if (isNodeJs()) {
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 (as_buffer) return canvas.toBuffer('image/' + image_format);
return image_format ? canvas.toDataURL('image/' + image_format) : canvas;
});
});
}
return new Promise(resolveFunc => {
const image = document.createElement('img');
image.onload = function() {
const canvas = document.createElement('canvas');
canvas.width = image.width;
canvas.height = image.height;
canvas.getContext('2d').drawImage(image, 0, 0);
if (as_buffer && image_format)
canvas.toBlob(blob => blob.arrayBuffer().then(resolveFunc), 'image/' + image_format);
else
resolveFunc(image_format ? canvas.toDataURL('image/' + image_format) : canvas);
};
image.onerror = function(arg) {
console.log(`IMAGE ERROR ${arg}`);
resolveFunc(null);
};
image.src = img_src;
});
}
/** @summary Convert Date object into string used preconfigured time zone
* @desc Time zone stored in settings.TimeZone */
function convertDate(dt) {
let res = '';
if (settings.TimeZone && isStr(settings.TimeZone)) {
try {
res = dt.toLocaleString('en-GB', { timeZone: settings.TimeZone });
} catch (err) {
res = '';
}
}
return res || dt.toLocaleString('en-GB');
}
export { getElementRect, getAbsPosInCanvas, convertDate,
DrawOptions, TRandom, floatToString, buildSvgCurve, compressSVG,
BasePainter, _loadJSDOM, makeTranslate, addHighlightStyle, svgToImage };