generateImages_browserless.js 14 KB

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