generateSvg_browserless.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. /*
  2. Render each OSMD sample, grab the generated images, and
  3. dump them into a local directory as SVG files.
  4. inspired by Vexflow's generate_png_images and vexflow-tests.js
  5. This can be used to generate SVGs from OSMD without a browser.
  6. Note: this script needs to "fake" quite a few browser elements, like window, document, and the DOM,
  7. which otherwise are missing in pure nodejs, causing errors in OSMD.
  8. There are also some hacks needed to set the container size (offsetWidth) correctly.
  9. Otherwise you'd need to run a headless browser, which is way slower,
  10. see the semi-obsolete generateDiffImagesPuppeteerLocalhost.js
  11. */
  12. function sleep (ms) {
  13. return new Promise((resolve) => {
  14. setTimeout(resolve, ms)
  15. })
  16. }
  17. // global variables
  18. // (without these being global, we'd have to pass many of these values to the generateSampleSvg function)
  19. let [osmdBuildDir, sampleDir, imageDir, pageWidth, pageHeight, filterRegex, mode, debugSleepTimeString] = process.argv.slice(2, 10)
  20. if (!osmdBuildDir || !sampleDir || !imageDir) {
  21. console.log('usage: ' +
  22. 'node test/Util/generateSvg_browserless.js osmdBuildDir sampleDirectory imageDirectory [width|0] [height|0] [filterRegex|all|allSmall] [--debug|--osmdtesting] [debugSleepTime]')
  23. console.log(' (use pageWidth and pageHeight 0 to not divide the rendering into pages (endless page))')
  24. console.log(' (use "all" to skip filterRegex parameter. "allSmall" with --osmdtesting skips two huge OSMD samples that take forever to render)')
  25. console.log('example: node test/Util/generateSvg_browserless.js ../../build ./test/data/ ./export 210 297 allSmall --debug 5000')
  26. console.log('Error: need osmdBuildDir, sampleDir and imageDir. Exiting.')
  27. process.exit(1)
  28. }
  29. let pageFormat
  30. if (!mode) {
  31. mode = ''
  32. }
  33. let OSMD // can only be required once window was simulated
  34. const FS = require('fs')
  35. async function init () {
  36. console.log('[OSMD.generateImages] init')
  37. const osmdTestingMode = mode.includes('osmdtesting') // can also be --debugosmdtesting
  38. const osmdTestingSingleMode = mode.includes('osmdtestingsingle')
  39. const DEBUG = mode.startsWith('--debug')
  40. // const debugSleepTime = Number.parseInt(process.env.GENERATE_DEBUG_SLEEP_TIME) || 0; // 5000 works for me [sschmidTU]
  41. if (DEBUG) {
  42. // console.log(' (note that --debug slows down the script by about 0.3s per file, through logging)')
  43. const debugSleepTimeMs = Number.parseInt(debugSleepTimeString)
  44. if (debugSleepTimeMs > 0) {
  45. console.log('debug sleep time: ' + debugSleepTimeString)
  46. await sleep(Number.parseInt(debugSleepTimeMs))
  47. // [VSCode] apparently this is necessary for the debugger to attach itself in time before the program closes.
  48. // sometimes this is not enough, so you may have to try multiple times or increase the sleep timer. Unfortunately debugging nodejs isn't easy.
  49. }
  50. }
  51. debug('sampleDir: ' + sampleDir, DEBUG)
  52. debug('imageDir: ' + imageDir, DEBUG)
  53. pageFormat = 'Endless'
  54. pageWidth = Number.parseInt(pageWidth)
  55. pageHeight = Number.parseInt(pageHeight)
  56. const endlessPage = !(pageHeight > 0 && pageWidth > 0)
  57. if (!endlessPage) {
  58. pageFormat = `${pageWidth}x${pageHeight}`
  59. }
  60. // ---- hacks to fake Browser elements OSMD and Vexflow need, like window, document, and a canvas HTMLElement ----
  61. var jsdom = require('jsdom')
  62. const dom = new jsdom.JSDOM('<!DOCTYPE html></html>')
  63. // eslint-disable-next-line no-global-assign
  64. window = dom.window
  65. // eslint-disable-next-line no-global-assign
  66. document = dom.window.document
  67. // eslint-disable-next-line no-global-assign
  68. global.window = dom.window
  69. // eslint-disable-next-line no-global-assign
  70. global.document = window.document
  71. window.console = console // probably does nothing
  72. global.HTMLElement = window.HTMLElement
  73. global.HTMLAnchorElement = window.HTMLAnchorElement
  74. global.XMLHttpRequest = window.XMLHttpRequest
  75. global.DOMParser = window.DOMParser
  76. global.Node = window.Node
  77. // fix Blob not found (to support external modules like is-blob)
  78. global.Blob = require('cross-blob')
  79. const div = document.createElement('div')
  80. div.id = 'browserlessDiv'
  81. document.body.appendChild(div)
  82. // const canvas = document.createElement('canvas')
  83. // div.canvas = document.createElement('canvas')
  84. const zoom = 1.0
  85. // width of the div / PNG generated
  86. let width = pageWidth * zoom
  87. // TODO sometimes the width is way too small for the score, may need to adjust zoom.
  88. if (endlessPage) {
  89. width = 1440
  90. }
  91. let height = pageHeight
  92. if (endlessPage) {
  93. height = 32767
  94. }
  95. div.width = width
  96. div.height = height
  97. div.offsetWidth = width // doesn't work, offsetWidth is always 0 from this. see below
  98. div.clientWidth = width
  99. div.clientHeight = height
  100. div.scrollHeight = height
  101. div.scrollWidth = width
  102. div.setAttribute('width', width)
  103. div.setAttribute('height', height)
  104. div.setAttribute('offsetWidth', width)
  105. // debug('div.offsetWidth: ' + div.offsetWidth, DEBUG) // 0 here, set correctly later
  106. // debug('div.height: ' + div.height, DEBUG)
  107. // hack: set offsetWidth reliably
  108. Object.defineProperties(window.HTMLElement.prototype, {
  109. offsetLeft: {
  110. get: function () { return parseFloat(window.getComputedStyle(this).marginTop) || 0 }
  111. },
  112. offsetTop: {
  113. get: function () { return parseFloat(window.getComputedStyle(this).marginTop) || 0 }
  114. },
  115. offsetHeight: {
  116. get: function () { return height }
  117. },
  118. offsetWidth: {
  119. get: function () { return width }
  120. }
  121. })
  122. debug('div.offsetWidth: ' + div.offsetWidth, DEBUG)
  123. debug('div.height: ' + div.height, DEBUG)
  124. // ---- end browser hacks (hopefully) ----
  125. // load globally
  126. OSMD = require(`${osmdBuildDir}/opensheetmusicdisplay.min.js`) // window needs to be available before we can require OSMD
  127. // Create the image directory if it doesn't exist.
  128. FS.mkdirSync(imageDir, { recursive: true })
  129. const sampleDirFilenames = FS.readdirSync(sampleDir)
  130. let samplesToProcess = [] // samples we want to process/generate pngs of, excluding the filtered out files/filenames
  131. for (const sampleFilename of sampleDirFilenames) {
  132. if (osmdTestingMode && filterRegex === 'allSmall') {
  133. if (sampleFilename.match('^(Actor)|(Gounod)')) { // TODO maybe filter by file size instead
  134. debug('filtering big file: ' + sampleFilename, DEBUG)
  135. continue
  136. }
  137. }
  138. // eslint-disable-next-line no-useless-escape
  139. if (sampleFilename.match('^.*(\.xml)|(\.musicxml)|(\.mxl)$')) {
  140. // console.log('found musicxml/mxl: ' + sampleFilename)
  141. samplesToProcess.push(sampleFilename)
  142. } else {
  143. debug('discarded file/directory: ' + sampleFilename, DEBUG)
  144. }
  145. }
  146. // filter samples to process by regex if given
  147. if (filterRegex && filterRegex !== '' && filterRegex !== 'all' && !(osmdTestingMode && filterRegex === 'allSmall')) {
  148. debug('filtering samples for regex: ' + filterRegex, DEBUG)
  149. samplesToProcess = samplesToProcess.filter((filename) => filename.match(filterRegex))
  150. debug(`found ${samplesToProcess.length} matches: `, DEBUG)
  151. for (let i = 0; i < samplesToProcess.length; i++) {
  152. debug(samplesToProcess[i], DEBUG)
  153. }
  154. }
  155. const osmdInstance = new OSMD.OpenSheetMusicDisplay(div, {
  156. autoResize: false,
  157. backend: 'svg',
  158. pageBackgroundColor: '#FFFFFF',
  159. pageFormat: pageFormat
  160. // defaultFontFamily: 'Arial',
  161. // drawTitle: false
  162. })
  163. // for more options check OSMDOptions.ts
  164. // you can set finer-grained rendering/engraving settings in EngravingRules:
  165. // osmdInstance.EngravingRules.TitleTopDistance = 5.0 // 5.0 is default
  166. // (unless in osmdTestingMode, these will be reset with drawingParameters default)
  167. // osmdInstance.EngravingRules.PageTopMargin = 5.0 // 5 is default
  168. // osmdInstance.EngravingRules.PageBottomMargin = 5.0 // 5 is default. <5 can cut off scores that extend in the last staffline
  169. // note that for now the png and canvas will still have the height given in the script argument,
  170. // so even with a margin of 0 the image will be filled to the full height.
  171. // osmdInstance.EngravingRules.PageLeftMargin = 5.0 // 5 is default
  172. // osmdInstance.EngravingRules.PageRightMargin = 5.0 // 5 is default
  173. // osmdInstance.EngravingRules.MetronomeMarkXShift = -8; // -6 is default
  174. // osmdInstance.EngravingRules.DistanceBetweenVerticalSystemLines = 0.15; // 0.35 is default
  175. // for more options check EngravingRules.ts (though not all of these are meant and fully supported to be changed at will)
  176. if (DEBUG) {
  177. osmdInstance.setLogLevel('debug')
  178. // console.log(`osmd PageFormat: ${osmdInstance.EngravingRules.PageFormat.width}x${osmdInstance.EngravingRules.PageFormat.height}`)
  179. console.log(`osmd PageFormat idString: ${osmdInstance.EngravingRules.PageFormat.idString}`)
  180. console.log('PageHeight: ' + osmdInstance.EngravingRules.PageHeight)
  181. } else {
  182. osmdInstance.setLogLevel('info') // doesn't seem to work, log.debug still logs
  183. }
  184. debug('[OSMD.generateImages] starting loop over samples, saving images to ' + imageDir, DEBUG)
  185. for (let i = 0; i < samplesToProcess.length; i++) {
  186. var sampleFilename = samplesToProcess[i]
  187. debug('sampleFilename: ' + sampleFilename, DEBUG)
  188. await generateSampleSvg(sampleFilename, sampleDir, osmdInstance, osmdTestingMode, false)
  189. if (osmdTestingMode && !osmdTestingSingleMode && sampleFilename.startsWith('Beethoven') && sampleFilename.includes('Geliebte')) {
  190. // generate one more testing image with skyline and bottomline. (startsWith 'Beethoven' don't catch the function test)
  191. await generateSampleSvg(sampleFilename, sampleDir, osmdInstance, osmdTestingMode, true, DEBUG)
  192. }
  193. }
  194. console.log('[OSMD.generateImages] done, exiting.')
  195. }
  196. // eslint-disable-next-line
  197. // let maxRss = 0, maxRssFilename = '' // to log memory usage (debug)
  198. async function generateSampleSvg (sampleFilename, directory, osmdInstance, osmdTestingMode,
  199. includeSkyBottomLine = false, DEBUG = false) {
  200. var samplePath = directory + '/' + sampleFilename
  201. let loadParameter = FS.readFileSync(samplePath)
  202. if (sampleFilename.endsWith('.mxl')) {
  203. loadParameter = await OSMD.MXLHelper.MXLtoXMLstring(loadParameter)
  204. } else {
  205. loadParameter = loadParameter.toString()
  206. }
  207. // console.log('loadParameter: ' + loadParameter)
  208. // console.log('typeof loadParameter: ' + typeof loadParameter)
  209. // set sample-specific options for OSMD visual regression testing
  210. if (osmdTestingMode) {
  211. const isFunctionTestAutobeam = sampleFilename.startsWith('OSMD_function_test_autobeam')
  212. const isFunctionTestAutoColoring = sampleFilename.startsWith('OSMD_function_test_auto-custom-coloring')
  213. const isFunctionTestSystemAndPageBreaks = sampleFilename.startsWith('OSMD_Function_Test_System_and_Page_Breaks')
  214. const isFunctionTestDrawingRange = sampleFilename.startsWith('OSMD_function_test_measuresToDraw_')
  215. const defaultOrCompactTightMode = sampleFilename.startsWith('OSMD_Function_Test_Container_height') ? 'compacttight' : 'default'
  216. const isTestFlatBeams = sampleFilename.startsWith('test_drum_tuplet_beams')
  217. osmdInstance.setOptions({
  218. autoBeam: isFunctionTestAutobeam, // only set to true for function test autobeam
  219. coloringMode: isFunctionTestAutoColoring ? 2 : 0,
  220. coloringSetCustom: isFunctionTestAutoColoring ? ['#d82c6b', '#F89D15', '#FFE21A', '#4dbd5c', '#009D96', '#43469d', '#76429c', '#ff0000'] : undefined,
  221. colorStemsLikeNoteheads: isFunctionTestAutoColoring,
  222. drawingParameters: defaultOrCompactTightMode, // note: default resets all EngravingRules. could be solved differently
  223. drawFromMeasureNumber: isFunctionTestDrawingRange ? 9 : 1,
  224. drawUpToMeasureNumber: isFunctionTestDrawingRange ? 12 : Number.MAX_SAFE_INTEGER,
  225. newSystemFromXML: isFunctionTestSystemAndPageBreaks,
  226. newPageFromXML: isFunctionTestSystemAndPageBreaks,
  227. pageBackgroundColor: '#FFFFFF', // reset by drawingparameters default
  228. pageFormat: pageFormat // reset by drawingparameters default
  229. })
  230. osmdInstance.drawSkyLine = includeSkyBottomLine // if includeSkyBottomLine, draw skyline and bottomline, else not
  231. osmdInstance.drawBottomLine = includeSkyBottomLine
  232. if (isTestFlatBeams) {
  233. osmdInstance.EngravingRules.FlatBeams = true
  234. // osmdInstance.EngravingRules.FlatBeamOffset = 30;
  235. osmdInstance.EngravingRules.FlatBeamOffset = 10
  236. osmdInstance.EngravingRules.FlatBeamOffsetPerBeam = 10
  237. } else {
  238. osmdInstance.EngravingRules.FlatBeams = false
  239. }
  240. }
  241. try {
  242. await osmdInstance.load(loadParameter) // if using load.then() without await, memory will not be freed up between renders
  243. } catch (ex) {
  244. console.error('load error: ' + ex)
  245. }
  246. debug('xml loaded', DEBUG)
  247. try {
  248. await osmdInstance.render()
  249. } catch (ex) {
  250. console.log('renderError: ' + ex)
  251. }
  252. debug('rendered', DEBUG)
  253. const markupStrings = []
  254. let svgElement
  255. // This loop supports all the pages you might possibly wish for, no?
  256. for (let pageNumber = 1; pageNumber < Number.POSITIVE_INFINITY; pageNumber++) {
  257. svgElement = document.getElementById('osmdSvgPage' + pageNumber)
  258. if (!svgElement) {
  259. break
  260. }
  261. // The important xmlns attribute is not serialized unless we set it here
  262. svgElement.setAttribute('xmlns', 'http://www.w3.org/2000/svg')
  263. markupStrings.push(svgElement.outerHTML)
  264. }
  265. for (let urlIndex = 0; urlIndex < markupStrings.length; urlIndex++) {
  266. const pageNumberingString = `${urlIndex + 1}`
  267. const skybottomlineString = includeSkyBottomLine ? 'skybottomline_' : ''
  268. // pageNumberingString = markupStrings.length > 0 ? pageNumberingString : '' // don't put '_1' at the end if only one page. though that may cause more work
  269. var pageFilename = `${imageDir}/${sampleFilename}_${skybottomlineString}${pageNumberingString}.svg`
  270. const markup = markupStrings[urlIndex]
  271. if (!markup) {
  272. console.log(`error: could not get markup (SVG data) for page ${urlIndex + 1} of sample: ${sampleFilename}`)
  273. continue
  274. }
  275. debug('got image data, saving to: ' + pageFilename, DEBUG)
  276. FS.writeFileSync(pageFilename, markup, { encoding: 'utf-8' })
  277. // debug: log memory usage
  278. // const usage = process.memoryUsage()
  279. // for (const entry of Object.entries(usage)) {
  280. // if (entry[0] === 'rss') {
  281. // if (entry[1] > maxRss) {
  282. // maxRss = entry[1]
  283. // maxRssFilename = pageFilename
  284. // }
  285. // }
  286. // console.log(entry[0] + ': ' + entry[1] / (1024 * 1024) + 'mb')
  287. // }
  288. // console.log('maxRss: ' + (maxRss / 1024 / 1024) + 'mb' + ' for ' + maxRssFilename)
  289. }
  290. // console.log('maxRss total: ' + (maxRss / 1024 / 1024) + 'mb' + ' for ' + maxRssFilename)
  291. // await sleep(5000)
  292. // }) // end read file
  293. }
  294. function debug (msg, debugEnabled) {
  295. if (debugEnabled) {
  296. console.log(msg)
  297. }
  298. }
  299. init()