generateImages_browserless.js 11 KB

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