generateImages_browserless.mjs 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519
  1. import Blob from "cross-blob";
  2. import FS from "fs";
  3. import jsdom from "jsdom";
  4. //import headless_gl from "gl"; // this is now imported dynamically in a try catch, in case gl install fails, see #1160
  5. import OSMD from "../../build/opensheetmusicdisplay.min.js"; // window needs to be available before we can require OSMD
  6. /*
  7. Render each OSMD sample, grab the generated images, and
  8. dump them into a local directory as PNG or SVG files.
  9. inspired by Vexflow's generate_png_images and vexflow-tests.js
  10. This can be used to generate PNGs or SVGs from OSMD without a browser.
  11. It's also used with the visual regression test system (using PNGs) in
  12. `tools/visual_regression.sh`
  13. (see package.json, used with npm run generate:blessed and generate:current, then test:visual).
  14. Note: this script needs to "fake" quite a few browser elements, like window, document,
  15. and a Canvas HTMLElement (for PNG) or the DOM (for SVG) ,
  16. which otherwise are missing in pure nodejs, causing errors in OSMD.
  17. For PNG it needs the canvas package installed.
  18. There are also some hacks needed to set the container size (offsetWidth) correctly.
  19. Otherwise you'd need to run a headless browser, which is way slower,
  20. see the semi-obsolete generateDiffImagesPuppeteerLocalhost.js
  21. */
  22. function sleep (ms) {
  23. return new Promise((resolve) => {
  24. setTimeout(resolve, ms);
  25. });
  26. }
  27. // global variables
  28. // (without these being global, we'd have to pass many of these values to the generateSampleImage function)
  29. // eslint-disable-next-line prefer-const
  30. let [osmdBuildDir, sampleDir, imageDir, imageFormat, pageWidth, pageHeight, filterRegex, mode, debugSleepTimeString, skyBottomLinePreference] = process.argv.slice(2, 12);
  31. imageFormat = imageFormat?.toLowerCase();
  32. if (!osmdBuildDir || !sampleDir || !imageDir || (imageFormat !== "png" && imageFormat !== "svg")) {
  33. console.log("usage: " +
  34. // eslint-disable-next-line max-len
  35. "node test/Util/generateImages_browserless.mjs osmdBuildDir sampleDirectory imageDirectory svg|png [width|0] [height|0] [filterRegex|all|allSmall] [--debug|--osmdtesting] [debugSleepTime]");
  36. console.log(" (use pageWidth and pageHeight 0 to not divide the rendering into pages (endless page))");
  37. console.log(' (use "all" to skip filterRegex parameter. "allSmall" with --osmdtesting skips two huge OSMD samples that take forever to render)');
  38. console.log("example: node test/Util/generateImages_browserless.mjs ../../build ./test/data/ ./export png");
  39. console.log("Error: need osmdBuildDir, sampleDir, imageDir and svg|png arguments. Exiting.");
  40. process.exit(1);
  41. }
  42. const useWhiteTabNumberBackground = true;
  43. // use white instead of transparent background for tab numbers for PNG export.
  44. // can fix black rectangles displayed, depending on your image viewer / program.
  45. // though this is unnecessary if your image viewer displays transparent as white
  46. let pageFormat;
  47. if (!mode) {
  48. mode = "";
  49. }
  50. // let OSMD; // can only be required once window was simulated
  51. // eslint-disable-next-line @typescript-eslint/no-var-requires
  52. async function init () {
  53. debug("init");
  54. const osmdTestingMode = mode.includes("osmdtesting"); // can also be --debugosmdtesting
  55. const osmdTestingSingleMode = mode.includes("osmdtestingsingle");
  56. const DEBUG = mode.startsWith("--debug");
  57. // const debugSleepTime = Number.parseInt(process.env.GENERATE_DEBUG_SLEEP_TIME) || 0; // 5000 works for me [sschmidTU]
  58. if (DEBUG) {
  59. // debug(' (note that --debug slows down the script by about 0.3s per file, through logging)')
  60. const debugSleepTimeMs = Number.parseInt(debugSleepTimeString, 10);
  61. if (debugSleepTimeMs > 0) {
  62. debug("debug sleep time: " + debugSleepTimeString);
  63. await sleep(Number.parseInt(debugSleepTimeMs, 10));
  64. // [VSCode] apparently this is necessary for the debugger to attach itself in time before the program closes.
  65. // sometimes this is not enough, so you may have to try multiple times or increase the sleep timer. Unfortunately debugging nodejs isn't easy.
  66. }
  67. }
  68. debug("sampleDir: " + sampleDir, DEBUG);
  69. debug("imageDir: " + imageDir, DEBUG);
  70. debug("imageFormat: " + imageFormat, DEBUG);
  71. pageFormat = "Endless";
  72. pageWidth = Number.parseInt(pageWidth, 10);
  73. pageHeight = Number.parseInt(pageHeight, 10);
  74. const endlessPage = !(pageHeight > 0 && pageWidth > 0);
  75. if (!endlessPage) {
  76. pageFormat = `${pageWidth}x${pageHeight}`;
  77. }
  78. // ---- hacks to fake Browser elements OSMD and Vexflow need, like window, document, and a canvas HTMLElement ----
  79. // eslint-disable-next-line @typescript-eslint/no-var-requires
  80. const dom = new jsdom.JSDOM("<!DOCTYPE html></html>");
  81. // eslint-disable-next-line no-global-assign
  82. // window = dom.window;
  83. // eslint-disable-next-line no-global-assign
  84. // document = dom.window.document;
  85. // eslint-disable-next-line no-global-assign
  86. global.window = dom.window;
  87. // eslint-disable-next-line no-global-assign
  88. global.document = window.document;
  89. //window.console = console; // probably does nothing
  90. global.HTMLElement = window.HTMLElement;
  91. global.HTMLAnchorElement = window.HTMLAnchorElement;
  92. global.XMLHttpRequest = window.XMLHttpRequest;
  93. global.DOMParser = window.DOMParser;
  94. global.Node = window.Node;
  95. if (imageFormat === "png") {
  96. global.Canvas = window.Canvas;
  97. }
  98. // For WebGLSkyBottomLineCalculatorBackend: Try to import gl dynamically
  99. // this is so that the script doesn't fail if gl could not be installed,
  100. // which can happen in some linux setups where gcc-11 is installed, see #1160
  101. try {
  102. const { default: headless_gl } = await import("gl");
  103. const oldCreateElement = document.createElement.bind(document);
  104. document.createElement = function (tagName, options) {
  105. if (tagName.toLowerCase() === "canvas") {
  106. const canvas = oldCreateElement(tagName, options);
  107. const oldGetContext = canvas.getContext.bind(canvas);
  108. canvas.getContext = function (contextType, contextAttributes) {
  109. if (contextType.toLowerCase() === "webgl" || contextType.toLowerCase() === "experimental-webgl") {
  110. const gl = headless_gl(canvas.width, canvas.height, contextAttributes);
  111. gl.canvas = canvas;
  112. return gl;
  113. } else {
  114. return oldGetContext(contextType, contextAttributes);
  115. }
  116. };
  117. return canvas;
  118. } else {
  119. return oldCreateElement(tagName, options);
  120. }
  121. };
  122. } catch {
  123. if (skyBottomLinePreference === "--webgl") {
  124. debug("WebGL image generation was requested but gl is not installed; using non-WebGL generation.");
  125. }
  126. }
  127. // fix Blob not found (to support external modules like is-blob)
  128. global.Blob = Blob;
  129. const div = document.createElement("div");
  130. div.id = "browserlessDiv";
  131. document.body.appendChild(div);
  132. // const canvas = document.createElement('canvas')
  133. // div.canvas = document.createElement('canvas')
  134. const zoom = 1.0;
  135. // width of the div / PNG generated
  136. let width = pageWidth * zoom;
  137. // TODO sometimes the width is way too small for the score, may need to adjust zoom.
  138. if (endlessPage) {
  139. width = 1440;
  140. }
  141. let height = pageHeight;
  142. if (endlessPage) {
  143. height = 32767;
  144. }
  145. div.width = width;
  146. div.height = height;
  147. // div.offsetWidth = width; // doesn't work, offsetWidth is always 0 from this. see below
  148. // div.clientWidth = width;
  149. // div.clientHeight = height;
  150. // div.scrollHeight = height;
  151. // div.scrollWidth = width;
  152. div.setAttribute("width", width);
  153. div.setAttribute("height", height);
  154. div.setAttribute("offsetWidth", width);
  155. // debug('div.offsetWidth: ' + div.offsetWidth, DEBUG) // 0 here, set correctly later
  156. // debug('div.height: ' + div.height, DEBUG)
  157. // hack: set offsetWidth reliably
  158. Object.defineProperties(window.HTMLElement.prototype, {
  159. offsetLeft: {
  160. get: function () { return parseFloat(window.getComputedStyle(this).marginTop) || 0; }
  161. },
  162. offsetTop: {
  163. get: function () { return parseFloat(window.getComputedStyle(this).marginTop) || 0; }
  164. },
  165. offsetHeight: {
  166. get: function () { return height; }
  167. },
  168. offsetWidth: {
  169. get: function () { return width; }
  170. }
  171. });
  172. debug("div.offsetWidth: " + div.offsetWidth, DEBUG);
  173. debug("div.height: " + div.height, DEBUG);
  174. // ---- end browser hacks (hopefully) ----
  175. // load globally
  176. // Create the image directory if it doesn't exist.
  177. FS.mkdirSync(imageDir, { recursive: true });
  178. const sampleDirFilenames = FS.readdirSync(sampleDir);
  179. let samplesToProcess = []; // samples we want to process/generate pngs of, excluding the filtered out files/filenames
  180. for (const sampleFilename of sampleDirFilenames) {
  181. if (osmdTestingMode && filterRegex === "allSmall") {
  182. if (sampleFilename.match("^(Actor)|(Gounod)")) { // TODO maybe filter by file size instead
  183. debug("filtering big file: " + sampleFilename, DEBUG);
  184. continue;
  185. }
  186. }
  187. // eslint-disable-next-line no-useless-escape
  188. if (sampleFilename.match("^.*(\.xml)|(\.musicxml)|(\.mxl)$")) {
  189. // debug('found musicxml/mxl: ' + sampleFilename)
  190. samplesToProcess.push(sampleFilename);
  191. } else {
  192. debug("discarded file/directory: " + sampleFilename, DEBUG);
  193. }
  194. }
  195. // filter samples to process by regex if given
  196. if (filterRegex && filterRegex !== "" && filterRegex !== "all" && !(osmdTestingMode && filterRegex === "allSmall")) {
  197. debug("filtering samples for regex: " + filterRegex, DEBUG);
  198. samplesToProcess = samplesToProcess.filter((filename) => filename.match(filterRegex));
  199. debug(`found ${samplesToProcess.length} matches: `, DEBUG);
  200. for (let i = 0; i < samplesToProcess.length; i++) {
  201. debug(samplesToProcess[i], DEBUG);
  202. }
  203. }
  204. const backend = imageFormat === "png" ? "canvas" : "svg";
  205. const osmdInstance = new OSMD.OpenSheetMusicDisplay(div, {
  206. autoResize: false,
  207. backend: backend,
  208. pageBackgroundColor: "#FFFFFF",
  209. pageFormat: pageFormat
  210. // defaultFontFamily: 'Arial',
  211. // drawTitle: false
  212. });
  213. // for more options check OSMDOptions.ts
  214. // you can set finer-grained rendering/engraving settings in EngravingRules:
  215. // osmdInstance.EngravingRules.TitleTopDistance = 5.0 // 5.0 is default
  216. // (unless in osmdTestingMode, these will be reset with drawingParameters default)
  217. // osmdInstance.EngravingRules.PageTopMargin = 5.0 // 5 is default
  218. // osmdInstance.EngravingRules.PageBottomMargin = 5.0 // 5 is default. <5 can cut off scores that extend in the last staffline
  219. // note that for now the png and canvas will still have the height given in the script argument,
  220. // so even with a margin of 0 the image will be filled to the full height.
  221. // osmdInstance.EngravingRules.PageLeftMargin = 5.0 // 5 is default
  222. // osmdInstance.EngravingRules.PageRightMargin = 5.0 // 5 is default
  223. // osmdInstance.EngravingRules.MetronomeMarkXShift = -8; // -6 is default
  224. // osmdInstance.EngravingRules.DistanceBetweenVerticalSystemLines = 0.15; // 0.35 is default
  225. // for more options check EngravingRules.ts (though not all of these are meant and fully supported to be changed at will)
  226. if (useWhiteTabNumberBackground && backend === "png") {
  227. osmdInstance.EngravingRules.pageBackgroundColor = "#FFFFFF";
  228. // fix for tab number having black background depending on image viewer
  229. // otherwise, the rectangle is transparent, which can be displayed as black in certain programs
  230. }
  231. if (DEBUG) {
  232. osmdInstance.setLogLevel("debug");
  233. // debug(`osmd PageFormat: ${osmdInstance.EngravingRules.PageFormat.width}x${osmdInstance.EngravingRules.PageFormat.height}`)
  234. debug(`osmd PageFormat idString: ${osmdInstance.EngravingRules.PageFormat.idString}`);
  235. debug("PageHeight: " + osmdInstance.EngravingRules.PageHeight);
  236. } else {
  237. osmdInstance.setLogLevel("info"); // doesn't seem to work, log.debug still logs
  238. }
  239. debug("[OSMD.generateImages] starting loop over samples, saving images to " + imageDir, DEBUG);
  240. for (let i = 0; i < samplesToProcess.length; i++) {
  241. const sampleFilename = samplesToProcess[i];
  242. debug("sampleFilename: " + sampleFilename, DEBUG);
  243. await generateSampleImage(sampleFilename, sampleDir, osmdInstance, osmdTestingMode, {}, DEBUG);
  244. if (osmdTestingMode) {
  245. if (!osmdTestingSingleMode && sampleFilename.startsWith("Beethoven") && sampleFilename.includes("Geliebte")) {
  246. // generate one more testing image with skyline and bottomline. (startsWith 'Beethoven' don't catch the function test)
  247. await generateSampleImage(sampleFilename, sampleDir, osmdInstance, osmdTestingMode, {skyBottomLine: true}, DEBUG);
  248. // generate one more testing image with GraphicalNote positions
  249. await generateSampleImage(sampleFilename, sampleDir, osmdInstance, osmdTestingMode, {boundingBoxes: "VexFlowGraphicalNote"}, DEBUG);
  250. } else if (sampleFilename.startsWith("test_tab_x-alignment_triplet_plus_bracket_below_above")) {
  251. // generate one more testing image in dark mode
  252. await generateSampleImage(sampleFilename, sampleDir, osmdInstance, osmdTestingMode, {darkMode: true}, DEBUG);
  253. }
  254. }
  255. }
  256. debug("done, exiting.");
  257. }
  258. // eslint-disable-next-line
  259. // let maxRss = 0, maxRssFilename = '' // to log memory usage (debug)
  260. async function generateSampleImage (sampleFilename, directory, osmdInstance, osmdTestingMode,
  261. options = {}, DEBUG = false) {
  262. function makeSkyBottomLineOptions() {
  263. const preference = skyBottomLinePreference ?? "";
  264. if (preference === "--batch") {
  265. return {
  266. preferredSkyBottomLineBatchCalculatorBackend: 0, // plain
  267. skyBottomLineBatchCriteria: 0, // use batch algorithm only
  268. };
  269. } else if (preference === "--webgl") {
  270. return {
  271. preferredSkyBottomLineBatchCalculatorBackend: 1, // webgl
  272. skyBottomLineBatchCriteria: 0, // use batch algorithm only
  273. };
  274. } else {
  275. return {
  276. preferredSkyBottomLineBatchCalculatorBackend: 0, // plain
  277. skyBottomLineBatchCriteria: Infinity, // use non-batch algorithm only
  278. };
  279. }
  280. }
  281. const samplePath = directory + "/" + sampleFilename;
  282. let loadParameter = FS.readFileSync(samplePath);
  283. if (sampleFilename.endsWith(".mxl")) {
  284. loadParameter = await OSMD.MXLHelper.MXLtoXMLstring(loadParameter);
  285. } else {
  286. loadParameter = loadParameter.toString();
  287. }
  288. // debug('loadParameter: ' + loadParameter)
  289. // debug('typeof loadParameter: ' + typeof loadParameter)
  290. // set sample-specific options for OSMD visual regression testing
  291. let includeSkyBottomLine = false;
  292. let drawBoundingBoxString;
  293. let isTestOctaveShiftInvisibleInstrument;
  294. let isTestInvisibleMeasureNotAffectingLayout;
  295. if (osmdTestingMode) {
  296. const isFunctionTestAutobeam = sampleFilename.startsWith("OSMD_function_test_autobeam");
  297. const isFunctionTestAutoColoring = sampleFilename.startsWith("OSMD_function_test_auto-custom-coloring");
  298. const isFunctionTestSystemAndPageBreaks = sampleFilename.startsWith("OSMD_Function_Test_System_and_Page_Breaks");
  299. const isFunctionTestDrawingRange = sampleFilename.startsWith("OSMD_function_test_measuresToDraw_");
  300. const defaultOrCompactTightMode = sampleFilename.startsWith("OSMD_Function_Test_Container_height") ? "compacttight" : "default";
  301. const isTestFlatBeams = sampleFilename.startsWith("test_drum_tuplet_beams");
  302. const isTestEndClefStaffEntryBboxes = sampleFilename.startsWith("test_end_measure_clefs_staffentry_bbox");
  303. const isTestPageBreakImpliesSystemBreak = sampleFilename.startsWith("test_pagebreak_implies_systembreak");
  304. const isTestPageBottomMargin0 = sampleFilename.includes("PageBottomMargin0");
  305. const isTestTupletBracketTupletNumber = sampleFilename.includes("test_tuplet_bracket_tuplet_number");
  306. const isTestCajon2NoteSystem = sampleFilename.includes("test_cajon_2-note-system");
  307. isTestOctaveShiftInvisibleInstrument = sampleFilename.includes("test_octaveshift_first_instrument_invisible");
  308. const isTextOctaveShiftExtraGraphicalMeasure = sampleFilename.includes("test_octaveshift_extragraphicalmeasure");
  309. isTestInvisibleMeasureNotAffectingLayout = sampleFilename.includes("test_invisible_measure_not_affecting_layout");
  310. const isTestWedgeMultilineCrescendo = sampleFilename.includes("test_wedge_multiline_crescendo");
  311. const isTestWedgeMultilineDecrescendo = sampleFilename.includes("test_wedge_multiline_decrescendo");
  312. const isTestTabs4Strings = sampleFilename.includes("test_tabs_4_strings");
  313. osmdInstance.EngravingRules.loadDefaultValues(); // note this may also be executed in setOptions below via drawingParameters default
  314. if (isTestEndClefStaffEntryBboxes) {
  315. drawBoundingBoxString = "VexFlowStaffEntry";
  316. } else {
  317. drawBoundingBoxString = options.boundingBoxes; // undefined is also a valid value: no bboxes
  318. }
  319. osmdInstance.setOptions({
  320. autoBeam: isFunctionTestAutobeam, // only set to true for function test autobeam
  321. coloringMode: isFunctionTestAutoColoring ? 2 : 0,
  322. // eslint-disable-next-line max-len
  323. coloringSetCustom: isFunctionTestAutoColoring ? ["#d82c6b", "#F89D15", "#FFE21A", "#4dbd5c", "#009D96", "#43469d", "#76429c", "#ff0000"] : undefined,
  324. colorStemsLikeNoteheads: isFunctionTestAutoColoring,
  325. drawingParameters: defaultOrCompactTightMode, // note: default resets all EngravingRules. could be solved differently
  326. drawFromMeasureNumber: isFunctionTestDrawingRange ? 9 : 1,
  327. drawUpToMeasureNumber: isFunctionTestDrawingRange ? 12 : Number.MAX_SAFE_INTEGER,
  328. newSystemFromXML: isFunctionTestSystemAndPageBreaks,
  329. newSystemFromNewPageInXML: isTestPageBreakImpliesSystemBreak,
  330. newPageFromXML: isFunctionTestSystemAndPageBreaks,
  331. pageBackgroundColor: "#FFFFFF", // reset by drawingparameters default
  332. pageFormat: pageFormat, // reset by drawingparameters default,
  333. ...makeSkyBottomLineOptions()
  334. });
  335. if (options.darkMode) {
  336. osmdInstance.setOptions({darkMode: true}); // note that we set pageBackgroundColor above, so we need to overwrite it here.
  337. }
  338. // note that loadDefaultValues() may be executed in setOptions with drawingParameters default
  339. //osmdInstance.EngravingRules.RenderSingleHorizontalStaffline = true; // to use this option here, place it after setOptions(), see above
  340. osmdInstance.EngravingRules.AlwaysSetPreferredSkyBottomLineBackendAutomatically = false; // this would override the command line options (--plain etc)
  341. includeSkyBottomLine = options.skyBottomLine ? options.skyBottomLine : false; // apparently es6 doesn't have ?? operator
  342. osmdInstance.drawSkyLine = includeSkyBottomLine; // if includeSkyBottomLine, draw skyline and bottomline, else not
  343. osmdInstance.drawBottomLine = includeSkyBottomLine;
  344. osmdInstance.setDrawBoundingBox(drawBoundingBoxString, false); // false: don't render (now). also (re-)set if undefined!
  345. if (isTestFlatBeams) {
  346. osmdInstance.EngravingRules.FlatBeams = true;
  347. // osmdInstance.EngravingRules.FlatBeamOffset = 30;
  348. osmdInstance.EngravingRules.FlatBeamOffset = 10;
  349. osmdInstance.EngravingRules.FlatBeamOffsetPerBeam = 10;
  350. } else {
  351. osmdInstance.EngravingRules.FlatBeams = false;
  352. }
  353. if (isTestPageBottomMargin0) {
  354. osmdInstance.EngravingRules.PageBottomMargin = 0;
  355. }
  356. if (isTestTupletBracketTupletNumber) {
  357. osmdInstance.EngravingRules.TupletNumberLimitConsecutiveRepetitions = true;
  358. osmdInstance.EngravingRules.TupletNumberMaxConsecutiveRepetitions = 2;
  359. osmdInstance.EngravingRules.TupletNumberAlwaysDisableAfterFirstMax = true; // necessary to trigger bug
  360. }
  361. if (isTestCajon2NoteSystem) {
  362. osmdInstance.EngravingRules.PercussionUseCajon2NoteSystem = true;
  363. }
  364. if (isTextOctaveShiftExtraGraphicalMeasure ||
  365. isTestOctaveShiftInvisibleInstrument ||
  366. isTestWedgeMultilineCrescendo ||
  367. isTestWedgeMultilineDecrescendo) {
  368. osmdInstance.EngravingRules.NewSystemAtXMLNewSystemAttribute = true;
  369. }
  370. if (isTestTabs4Strings) {
  371. osmdInstance.EngravingRules.TabKeySignatureSpacingAdded = false;
  372. osmdInstance.EngravingRules.TabTimeSignatureSpacingAdded = false;
  373. // more compact rendering. These are basically just aesthetic options, as a showcase.
  374. }
  375. }
  376. try {
  377. debug("loading sample " + sampleFilename, DEBUG);
  378. await osmdInstance.load(loadParameter, sampleFilename); // if using load.then() without await, memory will not be freed up between renders
  379. if (isTestOctaveShiftInvisibleInstrument) {
  380. osmdInstance.Sheet.Instruments[0].Visible = false;
  381. }
  382. if (isTestInvisibleMeasureNotAffectingLayout) {
  383. if (osmdInstance.Sheet.Instruments[1]) { // some systems can't handle ?. in this script (just a safety check anyways)
  384. osmdInstance.Sheet.Instruments[1].Visible = false;
  385. }
  386. }
  387. } catch (ex) {
  388. debug("couldn't load sample " + sampleFilename + ", skipping. Error: \n" + ex);
  389. return;
  390. }
  391. debug("xml loaded", DEBUG);
  392. try {
  393. osmdInstance.render();
  394. // there were reports that await could help here, but render isn't a synchronous function, and it seems to work. see #932
  395. } catch (ex) {
  396. debug("renderError: " + ex);
  397. }
  398. debug("rendered", DEBUG);
  399. const markupStrings = []; // svg
  400. const dataUrls = []; // png
  401. let canvasImage;
  402. for (let pageNumber = 1; pageNumber < Number.POSITIVE_INFINITY; pageNumber++) {
  403. if (imageFormat === "png") {
  404. canvasImage = document.getElementById("osmdCanvasVexFlowBackendCanvas" + pageNumber);
  405. if (!canvasImage) {
  406. break;
  407. }
  408. if (!canvasImage.toDataURL) {
  409. debug(`error: could not get canvas image for page ${pageNumber} for file: ${sampleFilename}`);
  410. break;
  411. }
  412. dataUrls.push(canvasImage.toDataURL());
  413. } else if (imageFormat === "svg") {
  414. const svgElement = document.getElementById("osmdSvgPage" + pageNumber);
  415. if (!svgElement) {
  416. break;
  417. }
  418. // The important xmlns attribute is not serialized unless we set it here
  419. svgElement.setAttribute("xmlns", "http://www.w3.org/2000/svg");
  420. markupStrings.push(svgElement.outerHTML);
  421. }
  422. }
  423. for (let pageIndex = 0; pageIndex < Math.max(dataUrls.length, markupStrings.length); pageIndex++) {
  424. const pageNumberingString = `${pageIndex + 1}`;
  425. const skybottomlineString = includeSkyBottomLine ? "skybottomline_" : "";
  426. const darkmodeString = options.darkMode ? "darkmode_" : "";
  427. const graphicalNoteBboxesString = drawBoundingBoxString ? "bbox" + drawBoundingBoxString + "_" : "";
  428. // pageNumberingString = dataUrls.length > 0 ? pageNumberingString : '' // don't put '_1' at the end if only one page. though that may cause more work
  429. const pageFilename = `${imageDir}/${sampleFilename}_${darkmodeString}${skybottomlineString}${graphicalNoteBboxesString}${pageNumberingString}.${imageFormat}`;
  430. if (imageFormat === "png") {
  431. const dataUrl = dataUrls[pageIndex];
  432. if (!dataUrl || !dataUrl.split) {
  433. debug(`error: could not get dataUrl (imageData) for page ${pageIndex + 1} of sample: ${sampleFilename}`);
  434. continue;
  435. }
  436. const imageData = dataUrl.split(";base64,").pop();
  437. const imageBuffer = Buffer.from(imageData, "base64");
  438. debug("got image data, saving to: " + pageFilename, DEBUG);
  439. FS.writeFileSync(pageFilename, imageBuffer, { encoding: "base64" });
  440. } else if (imageFormat === "svg") {
  441. const markup = markupStrings[pageIndex];
  442. if (!markup) {
  443. debug(`error: could not get markup (SVG data) for page ${pageIndex + 1} of sample: ${sampleFilename}`);
  444. continue;
  445. }
  446. debug("got svg markup data, saving to: " + pageFilename, DEBUG);
  447. FS.writeFileSync(pageFilename, markup, { encoding: "utf-8" });
  448. }
  449. // debug: log memory usage
  450. // const usage = process.memoryUsage()
  451. // for (const entry of Object.entries(usage)) {
  452. // if (entry[0] === 'rss') {
  453. // if (entry[1] > maxRss) {
  454. // maxRss = entry[1]
  455. // maxRssFilename = pageFilename
  456. // }
  457. // }
  458. // debug(entry[0] + ': ' + entry[1] / (1024 * 1024) + 'mb')
  459. // }
  460. // debug('maxRss: ' + (maxRss / 1024 / 1024) + 'mb' + ' for ' + maxRssFilename)
  461. }
  462. // debug('maxRss total: ' + (maxRss / 1024 / 1024) + 'mb' + ' for ' + maxRssFilename)
  463. // await sleep(5000)
  464. // }) // end read file
  465. }
  466. function debug (msg, debugEnabled = true) {
  467. if (debugEnabled) {
  468. console.log("[generateImages] " + msg);
  469. }
  470. }
  471. init();