base/FontHandler.mjs

  1. import { isNodeJs, httpRequest, btoa_func, source_dir, settings, isObject } from '../core.mjs';
  2. const kArial = 'Arial', kTimes = 'Times New Roman', kCourier = 'Courier New', kVerdana = 'Verdana', kSymbol = 'RootSymbol', kWingdings = 'Wingdings',
  3. // average width taken from symbols.html, counted only for letters and digits
  4. root_fonts = [null, // index 0 not exists
  5. { n: kTimes, s: 'italic', aw: 0.5314 },
  6. { n: kTimes, w: 'bold', aw: 0.5809 },
  7. { n: kTimes, s: 'italic', w: 'bold', aw: 0.5540 },
  8. { n: kArial, aw: 0.5778 },
  9. { n: kArial, s: 'oblique', aw: 0.5783 },
  10. { n: kArial, w: 'bold', aw: 0.6034 },
  11. { n: kArial, s: 'oblique', w: 'bold', aw: 0.6030 },
  12. { n: kCourier, aw: 0.6003 },
  13. { n: kCourier, s: 'oblique', aw: 0.6004 },
  14. { n: kCourier, w: 'bold', aw: 0.6003 },
  15. { n: kCourier, s: 'oblique', w: 'bold', aw: 0.6005 },
  16. { n: kSymbol, aw: 0.5521, file: 'symbol.ttf' },
  17. { n: kTimes, aw: 0.5521 },
  18. { n: kWingdings, aw: 0.5664, file: 'wingding.ttf' },
  19. { n: kSymbol, s: 'oblique', aw: 0.5314, file: 'symbol.ttf' },
  20. { n: kVerdana, aw: 0.5664 },
  21. { n: kVerdana, s: 'italic', aw: 0.5495 },
  22. { n: kVerdana, w: 'bold', aw: 0.5748 },
  23. { n: kVerdana, s: 'italic', w: 'bold', aw: 0.5578 }],
  24. // list of loaded fonts including handling of multiple simultaneous requests
  25. gFontFiles = {};
  26. /** @summary Read font file from some pre-configured locations
  27. * @return {Promise} with base64 code of the font
  28. * @private */
  29. async function loadFontFile(fname) {
  30. let entry = gFontFiles[fname];
  31. if (entry?.base64)
  32. return entry?.base64;
  33. if (entry?.promises !== undefined) {
  34. return new Promise(resolveFunc => {
  35. entry.promises.push(resolveFunc);
  36. });
  37. }
  38. entry = gFontFiles[fname] = { promises: [] };
  39. const locations = [];
  40. if (fname.indexOf('/') >= 0)
  41. locations.push(''); // just use file name as is
  42. else {
  43. locations.push(source_dir + 'fonts/');
  44. if (isNodeJs())
  45. locations.push('../../fonts/');
  46. else if (source_dir.indexOf('jsrootsys/') >= 0) {
  47. locations.unshift(source_dir.replace(/jsrootsys/g, 'rootsys_fonts'));
  48. locations.unshift(source_dir.replace(/jsrootsys/g, 'rootsys/fonts'));
  49. }
  50. }
  51. function completeReading(base64) {
  52. entry.base64 = base64;
  53. const arr = entry.promises;
  54. delete entry.promises;
  55. arr.forEach(func => func(base64));
  56. return base64;
  57. }
  58. async function tryNext() {
  59. if (!locations.length) {
  60. completeReading(null);
  61. throw new Error(`Fail to load ${fname} font`);
  62. }
  63. let path = locations.shift() + fname;
  64. console.log('loading font', path);
  65. const pr = isNodeJs() ? import('fs').then(fs => {
  66. const prefix = 'file://' + (process?.platform === 'win32' ? '/' : '');
  67. if (path.indexOf(prefix) === 0)
  68. path = path.slice(prefix.length);
  69. return fs.readFileSync(path).toString('base64');
  70. }) : httpRequest(path, 'bin').then(buf => btoa_func(buf));
  71. return pr.then(res => res ? completeReading(res) : tryNext()).catch(() => tryNext());
  72. }
  73. return tryNext();
  74. }
  75. /**
  76. * @summary Helper class for font handling
  77. * @private
  78. */
  79. class FontHandler {
  80. /** @summary constructor */
  81. constructor(fontIndex, size, scale) {
  82. if (scale && (size < 1)) {
  83. size *= scale;
  84. this.scaled = true;
  85. }
  86. this.size = Math.round(size);
  87. this.scale = scale;
  88. this.index = 0;
  89. this.func = this.setFont.bind(this);
  90. let cfg;
  91. if (fontIndex && isObject(fontIndex))
  92. cfg = fontIndex;
  93. else {
  94. if (fontIndex && Number.isInteger(fontIndex))
  95. this.index = Math.floor(fontIndex / 10);
  96. cfg = root_fonts[this.index];
  97. }
  98. if (cfg) {
  99. this.cfg = cfg;
  100. this.setNameStyleWeight(cfg.n, cfg.s, cfg.w, cfg.aw, cfg.format, cfg.base64);
  101. } else
  102. this.setNameStyleWeight(kArial);
  103. }
  104. /** @summary Should returns true if font has to be loaded before
  105. * @private */
  106. needLoad() { return this.cfg?.file && !this.isSymbol && !this.base64; }
  107. /** @summary Async function to load font
  108. * @private */
  109. async load() {
  110. if (!this.needLoad())
  111. return true;
  112. return loadFontFile(this.cfg.file).then(base64 => {
  113. this.cfg.base64 = this.base64 = base64;
  114. this.format = 'ttf';
  115. return Boolean(base64);
  116. });
  117. }
  118. /** @summary Directly set name, style and weight for the font
  119. * @private */
  120. setNameStyleWeight(name, style, weight, aver_width, format, base64) {
  121. this.name = name;
  122. this.style = style || null;
  123. this.weight = weight || null;
  124. this.aver_width = aver_width || (weight ? 0.58 : 0.55);
  125. this.format = format; // format of custom font, ttf by default
  126. this.base64 = base64; // indication of custom font
  127. if (!settings.LoadSymbolTtf && ((this.name === kSymbol) || (this.name === kWingdings))) {
  128. this.isSymbol = this.name;
  129. this.name = kTimes;
  130. } else
  131. this.isSymbol = '';
  132. }
  133. /** @summary Set painter for which font will be applied */
  134. setPainter(painter) {
  135. this.painter = painter;
  136. }
  137. /** @summary Force setting of style and weight, used in latex */
  138. setUseFullStyle(flag) {
  139. this.full_style = flag;
  140. }
  141. /** @summary Assigns font-related attributes */
  142. addCustomFontToSvg(svg) {
  143. if (!this.base64 || !this.name)
  144. return;
  145. const clname = 'custom_font_' + this.name, fmt = 'ttf';
  146. let defs = svg.selectChild('.canvas_defs');
  147. if (defs.empty())
  148. defs = svg.insert('svg:defs', ':first-child').attr('class', 'canvas_defs');
  149. const entry = defs.selectChild('.' + clname);
  150. if (entry.empty()) {
  151. defs.append('style')
  152. .attr('class', clname)
  153. .property('$fontcfg', this.cfg || null)
  154. .text(`@font-face { font-family: "${this.name}"; font-weight: normal; font-style: normal; src: url(data:application/font-${fmt};charset=utf-8;base64,${this.base64}); }`);
  155. }
  156. }
  157. /** @summary Assigns font-related attributes */
  158. setFont(selection) {
  159. if (this.base64 && this.painter)
  160. this.addCustomFontToSvg(this.painter.getCanvSvg());
  161. selection.attr('font-family', this.name)
  162. .attr('font-size', this.size)
  163. .attr(':xml:space', 'preserve');
  164. this.setFontStyle(selection);
  165. }
  166. /** @summary Assigns only font style attributes */
  167. setFontStyle(selection) {
  168. selection.attr('font-weight', this.weight || (this.full_style ? 'normal' : null))
  169. .attr('font-style', this.style || (this.full_style ? 'normal' : null));
  170. }
  171. /** @summary Set font size (optional) */
  172. setSize(size) { this.size = Math.round(size); }
  173. /** @summary Set text color (optional) */
  174. setColor(color) { this.color = color; }
  175. /** @summary Set text align (optional) */
  176. setAlign(align) { this.align = align; }
  177. /** @summary Set text angle (optional) */
  178. setAngle(angle) { this.angle = angle; }
  179. /** @summary Align angle to step raster, add optional offset */
  180. roundAngle(step, offset) {
  181. this.angle = parseInt(this.angle || 0);
  182. if (!Number.isInteger(this.angle)) this.angle = 0;
  183. this.angle = Math.round(this.angle/step) * step + (offset || 0);
  184. if (this.angle < 0)
  185. this.angle += 360;
  186. else if (this.angle >= 360)
  187. this.angle -= 360;
  188. }
  189. /** @summary Clears all font-related attributes */
  190. clearFont(selection) {
  191. selection.attr('font-family', null)
  192. .attr('font-size', null)
  193. .attr(':xml:space', null)
  194. .attr('font-weight', null)
  195. .attr('font-style', null);
  196. }
  197. /** @summary Returns true in case of monospace font
  198. * @private */
  199. isMonospace() {
  200. const n = this.name.toLowerCase();
  201. return (n.indexOf('courier') === 0) || (n === 'monospace') || (n === 'monaco');
  202. }
  203. /** @summary Return full font declaration which can be set as font property like '12pt Arial bold'
  204. * @private */
  205. getFontHtml() {
  206. let res = Math.round(this.size) + 'pt ' + this.name;
  207. if (this.weight) res += ' ' + this.weight;
  208. if (this.style) res += ' ' + this.style;
  209. return res;
  210. }
  211. /** @summary Returns font name */
  212. getFontName() {
  213. return this.isSymbol || this.name || 'none';
  214. }
  215. } // class FontHandler
  216. /** @summary Register custom font
  217. * @private */
  218. function addCustomFont(index, name, format, base64) {
  219. if (!Number.isInteger(index))
  220. console.error(`Wrong index ${index} for custom font`);
  221. else
  222. root_fonts[index] = { n: name, format, base64 };
  223. }
  224. /** @summary Return handle with custom font
  225. * @private */
  226. function getCustomFont(name) {
  227. return root_fonts.find(h => (h?.n === name) && h?.base64);
  228. }
  229. /** @summary Try to detect and create font handler for SVG text node
  230. * @private */
  231. function detectPdfFont(node) {
  232. const sz = node.getAttribute('font-size'),
  233. p = sz.indexOf('px'),
  234. sz_pixels = p > 0 ? Number.parseInt(sz.slice(0, p)) : 12;
  235. let family = node.getAttribute('font-family'),
  236. style = node.getAttribute('font-style'),
  237. weight = node.getAttribute('font-weight');
  238. if (family === 'times')
  239. family = kTimes;
  240. else if (family === 'symbol')
  241. family = kSymbol;
  242. else if (family === 'arial')
  243. family = kArial;
  244. else if (family === 'verdana')
  245. family = kVerdana;
  246. if (weight === 'normal')
  247. weight = '';
  248. if (style === 'normal')
  249. style = '';
  250. const fcfg = root_fonts.find(elem => {
  251. return (elem?.n === family) &&
  252. ((!weight && !elem.w) || (elem.w === weight)) &&
  253. ((!style && !elem.s) || (elem.s === style));
  254. });
  255. return new FontHandler(fcfg || root_fonts[13], sz_pixels);
  256. }
  257. export { kArial, kCourier, kSymbol, kWingdings, kTimes,
  258. FontHandler, addCustomFont, getCustomFont, detectPdfFont };