generateImages_browserless.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  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.generate] init')
  21. let [osmdBuildDir, sampleDir, imageDir, pageWidth, pageHeight, filterRegex, debugFlag, 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] [debugSleepTime]')
  25. console.log(' (use "all" to skip filterRegex parameter. "allSmall" 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 sampleDir and imageDir. Exiting.')
  28. process.exit(1)
  29. }
  30. const DEBUG = debugFlag === '--debug'
  31. // const debugSleepTime = Number.parseInt(process.env.GENERATE_DEBUG_SLEEP_TIME) || 0; // 5000 works for me [sschmidTU]
  32. if (DEBUG) {
  33. console.log(' (note that --debug slows down the script by about 0.3s per file, through logging)')
  34. const debugSleepTimeMs = Number.parseInt(debugSleepTimeString)
  35. if (debugSleepTimeMs > 0) {
  36. console.log('debug sleep time: ' + debugSleepTimeString)
  37. await sleep(Number.parseInt(debugSleepTimeMs))
  38. // [VSCode] apparently this is necessary for the debugger to attach itself in time before the program closes.
  39. // sometimes this is not enough, so you may have to try multiple times or increase the sleep timer. Unfortunately debugging nodejs isn't easy.
  40. }
  41. }
  42. debug('sampleDir: ' + sampleDir, DEBUG)
  43. debug('imageDir: ' + imageDir, DEBUG)
  44. let pageFormat = 'Endless'
  45. pageWidth = Number.parseInt(pageWidth)
  46. pageHeight = Number.parseInt(pageHeight)
  47. const endlessPage = !(pageHeight > 0 && pageWidth > 0)
  48. if (!endlessPage) {
  49. pageFormat = `${pageWidth}x${pageHeight}`
  50. }
  51. // ---- hacks to fake Browser elements OSMD and Vexflow need, like window, document, and a canvas HTMLElement ----
  52. const { JSDOM } = require('jsdom')
  53. const dom = new JSDOM('<!DOCTYPE html></html>')
  54. // eslint-disable-next-line no-global-assign
  55. window = dom.window
  56. // eslint-disable-next-line no-global-assign
  57. document = dom.window.document
  58. // eslint-disable-next-line no-global-assign
  59. global.window = dom.window
  60. // eslint-disable-next-line no-global-assign
  61. global.document = window.document
  62. global.HTMLElement = window.HTMLElement
  63. global.HTMLAnchorElement = window.HTMLAnchorElement
  64. global.XMLHttpRequest = window.XMLHttpRequest
  65. global.DOMParser = window.DOMParser
  66. global.Node = window.Node
  67. global.Canvas = window.Canvas
  68. // fix Blob not found
  69. const Blob = require('cross-blob')
  70. // eslint-disable-next-line no-new
  71. new Blob([])
  72. // => Blob {size: 0, type: ''}
  73. // Global patch (to support external modules like is-blob).
  74. global.Blob = Blob
  75. const div = document.createElement('div')
  76. div.id = 'browserlessDiv'
  77. document.body.appendChild(div)
  78. // const canvas = document.createElement('canvas')
  79. // div.canvas = document.createElement('canvas')
  80. const zoom = 1.0
  81. // width of the div / PNG generated
  82. let width = pageWidth * zoom
  83. // TODO sometimes the width is way too small for the score, may need to adjust zoom.
  84. if (endlessPage) {
  85. width = 1440
  86. }
  87. let height = pageHeight
  88. if (endlessPage) {
  89. height = 32767
  90. }
  91. div.width = width
  92. div.height = height
  93. div.offsetWidth = width // doesn't work, offsetWidth is always 0 from this. see below
  94. div.clientWidth = width
  95. div.clientHeight = height
  96. div.scrollHeight = height
  97. div.scrollWidth = width
  98. div.setAttribute('width', width)
  99. div.setAttribute('height', height)
  100. div.setAttribute('offsetWidth', width)
  101. debug('div.offsetWidth: ' + div.offsetWidth, DEBUG)
  102. debug('div.height: ' + div.height, DEBUG)
  103. // hack: set offsetWidth reliably
  104. Object.defineProperties(window.HTMLElement.prototype, {
  105. offsetLeft: {
  106. get: function () { return parseFloat(window.getComputedStyle(this).marginTop) || 0 }
  107. },
  108. offsetTop: {
  109. get: function () { return parseFloat(window.getComputedStyle(this).marginTop) || 0 }
  110. },
  111. offsetHeight: {
  112. get: function () { return height }
  113. },
  114. offsetWidth: {
  115. get: function () { return width }
  116. }
  117. })
  118. debug('div.offsetWidth: ' + div.offsetWidth, DEBUG)
  119. debug('div.height: ' + div.height, DEBUG)
  120. if (!DEBUG) {
  121. // deactivate console logs (mostly)
  122. console.log = (msg) => {}
  123. }
  124. // ---- end browser hacks (hopefully) ----
  125. const OSMD = require(`${osmdBuildDir}/opensheetmusicdisplay.min.js`)
  126. const fs = require('fs')
  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 (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' && 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: 'canvas',
  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 // 9.0 is default
  166. osmdInstance.EngravingRules.PageTopMargin = 5.0 // 5 is default
  167. osmdInstance.EngravingRules.PageBottomMargin = 5.0 // 5 is default. <5 can cut off scores that extend in the last staffline
  168. // note that for now the png and canvas will still have the height given in the script argument,
  169. // so even with a margin of 0 the image will be filled to the full height.
  170. osmdInstance.EngravingRules.PageLeftMargin = 5.0 // 5 is default
  171. osmdInstance.EngravingRules.PageRightMargin = 5.0 // 5 is default
  172. // osmdInstance.EngravingRules.MetronomeMarkXShift = -8; // -6 is default
  173. // osmdInstance.EngravingRules.DistanceBetweenVerticalSystemLines = 0.15; // 0.35 is default
  174. // for more options check EngravingRules.ts (though not all of these are meant and fully supported to be changed at will)
  175. // await sleep(5000)
  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('generateImages', DEBUG)
  185. for (let i = 0; i < samplesToProcess.length; i++) {
  186. var sampleFilename = samplesToProcess[i]
  187. debug('sampleFilename: ' + sampleFilename, DEBUG)
  188. let loadParameter = fs.readFileSync(sampleDir + '/' + sampleFilename)
  189. if (sampleFilename.endsWith('.mxl')) {
  190. loadParameter = await OSMD.MXLHelper.MXLtoXMLstring(loadParameter)
  191. } else {
  192. loadParameter = loadParameter.toString()
  193. }
  194. // console.log('loadParameter: ' + loadParameter)
  195. // console.log('typeof loadParameter: ' + typeof loadParameter)
  196. await osmdInstance.load(loadParameter).then(function () {
  197. debug('xml loaded', DEBUG)
  198. try {
  199. osmdInstance.render()
  200. } catch (ex) {
  201. console.log('renderError: ' + ex)
  202. }
  203. debug('rendered', DEBUG)
  204. const dataUrls = []
  205. let canvasImage
  206. for (let pageNumber = 1; pageNumber < 999; pageNumber++) {
  207. canvasImage = document.getElementById('osmdCanvasVexFlowBackendCanvas' + pageNumber)
  208. if (!canvasImage) {
  209. break
  210. }
  211. if (!canvasImage.toDataURL) {
  212. console.log(`error: could not get canvas image for page ${pageNumber} for file: ${sampleFilename}`)
  213. break
  214. }
  215. dataUrls.push(canvasImage.toDataURL())
  216. }
  217. for (let urlIndex = 0; urlIndex < dataUrls.length; urlIndex++) {
  218. const pageNumberingString = `_${urlIndex + 1}`
  219. // pageNumberingString = dataUrls.length > 0 ? pageNumberingString : '' // don't put '_1' at the end if only one page. though that may cause more work
  220. var pageFilename = `${imageDir}/${sampleFilename}${pageNumberingString}.png`
  221. const dataUrl = dataUrls[urlIndex]
  222. if (!dataUrl || !dataUrl.split) {
  223. console.log(`error: could not get dataUrl (imageData) for page ${urlIndex + 1} of sample: ${sampleFilename}`)
  224. continue
  225. }
  226. const imageData = dataUrl.split(';base64,').pop()
  227. const imageBuffer = Buffer.from(imageData, 'base64')
  228. debug('got image data, saving to: ' + pageFilename, DEBUG)
  229. fs.writeFileSync(pageFilename, imageBuffer, { encoding: 'base64' })
  230. }
  231. }) // end render then
  232. // },
  233. // function (e) {
  234. // console.log('error while rendering: ' + e)
  235. // }) // end load then
  236. // }) // end read file
  237. }
  238. console.log('[OSMD.generate_browserless] exit')
  239. }
  240. function debug (msg, debugEnabled) {
  241. if (debugEnabled) {
  242. console.log(msg)
  243. }
  244. }
  245. init()