Browse Source

feat(Export): Add SVG export to OSMD and generateImages_browserless.js -> png|svg option (#670)

integration of PR #932, SVG export code kindly contributed by @hallvors

in PR #932 SVG export was a separate generateSVG script,
but it was mostly identical to generateImages,
only changing some canvas/png elements to SVG,
so it made more sense to integrate the functionality into generateImages_browserless.js,
adding a new command line option png|svg.
sschmid 4 years ago
parent
commit
8cf4567e19

+ 12 - 10
package.json

@@ -20,16 +20,18 @@
     "build:webpack-sourcemap": "webpack --progress --colors --config webpack.sourcemap.js",
     "build:webpack-sourcemap": "webpack --progress --colors --config webpack.sourcemap.js",
     "start": "webpack-dev-server --progress --colors --config webpack.dev.js",
     "start": "webpack-dev-server --progress --colors --config webpack.dev.js",
     "start:local": "webpack-dev-server --progress --colors --config webpack.local.js",
     "start:local": "webpack-dev-server --progress --colors --config webpack.local.js",
-    "generatePNG": "node ./test/Util/generateImages_browserless.js ../../build ./test/data ./export 1000 1600 allSmall --debug",
-    "generatePNG:single": "node ./test/Util/generateImages_browserless.js ../../build ./test/data ./export 0 0 ^Beethoven",
-    "generatePNG:legacyslow": "node ./test/Util/generateDiffImagesPuppeteerLocalhost.js ./test/data ./export 0 0 all",
-    "generatePNG:paged": "node ./test/Util/generateImages_browserless.js ../../build ./test/data ./export 210 297 allSmall",
-    "generatePNG:paged:debug": "node ./test/Util/generateImages_browserless.js ../../build ./test/data ./export 210 297 all --debug 5000",
-    "generatePNG:paged:single": "node ./test/Util/generateImages_browserless.js ../../build ./test/data ./export 0 0 ^Beethoven",
-    "generate:current": "node ./test/Util/generateImages_browserless.js ../../build ./test/data ./visual_regression/current 0 0 allSmall --osmdtesting",
-    "generate:current:debug": "node ./test/Util/generateImages_browserless.js ../../build ./test/data ./visual_regression/current 0 0 allSmall --debugosmdtesting",
-    "generate:current:singletest": "node test/Util/generateImages_browserless.js ../../build ./test/data ./visual_regression/current 0 0 ^Beethoven --osmdtestingsingle",
-    "generate:blessed": "node ./test/Util/generateImages_browserless.js ../../build ./test/data ./visual_regression/blessed 0 0 allSmall --osmdtesting",
+    "generatePNG": "node ./test/Util/generateImages_browserless.js ../../build ./test/data ./export png 0 0 allSmall --osmdtesting",
+    "generateSVG": "node ./test/Util/generateImages_browserless.js ../../build ./test/data ./export/svg svg 0 0 allSmall --osmdtesting",
+    "generatePNG:debug": "node ./test/Util/generateImages_browserless.js ../../build ./test/data ./export png 0 0 allSmall --debugosmdtesting",
+    "generatePNG:single": "node ./test/Util/generateImages_browserless.js ../../build ./test/data ./export png 0 0 ^Beethoven",
+    "generatePNG:legacyslow": "node ./test/Util/generateDiffImagesPuppeteerLocalhost.js ./test/data ./export png 0 0 all",
+    "generatePNG:paged": "node ./test/Util/generateImages_browserless.js ../../build ./test/data ./export png 210 297 allSmall",
+    "generatePNG:paged:debug": "node ./test/Util/generateImages_browserless.js ../../build ./test/data ./export png 210 297 all --debug 5000",
+    "generatePNG:paged:single": "node ./test/Util/generateImages_browserless.js ../../build ./test/data ./export png 0 0 ^Beethoven",
+    "generate:current": "node ./test/Util/generateImages_browserless.js ../../build ./test/data ./visual_regression/current png 0 0 allSmall --osmdtesting",
+    "generate:current:debug": "node ./test/Util/generateImages_browserless.js ../../build ./test/data ./visual_regression/current png 0 0 allSmall --debugosmdtesting",
+    "generate:current:singletest": "node test/Util/generateImages_browserless.js ../../build ./test/data ./visual_regression/current png 0 0 ^Beethoven --osmdtestingsingle",
+    "generate:blessed": "node ./test/Util/generateImages_browserless.js ../../build ./test/data ./visual_regression/blessed png 0 0 allSmall --osmdtesting",
     "test:visual": "bash ./test/Util/visual_regression.sh ./visual_regression",
     "test:visual": "bash ./test/Util/visual_regression.sh ./visual_regression",
     "test:visual:singletest": "sh ./test/Util/visual_regression.sh ./visual_regression Beethoven",
     "test:visual:singletest": "sh ./test/Util/visual_regression.sh ./visual_regression Beethoven",
     "fix-memory-limit": "cross-env NODE_OPTIONS=--max_old_space_size=4096"
     "fix-memory-limit": "cross-env NODE_OPTIONS=--max_old_space_size=4096"

+ 7 - 3
src/OpenSheetMusicDisplay/OpenSheetMusicDisplay.ts

@@ -317,9 +317,13 @@ export class OpenSheetMusicDisplay {
         }
         }
     }
     }
 
 
-    public export(): void {
-        if (this.backend instanceof SvgVexFlowBackend) {
-            (this.backend as SvgVexFlowBackend).export();
+    // for now SVG only, see generateImages_browserless (PNG/SVG)
+    public exportSVG(): void {
+        for (const backend of this.drawer?.Backends) {
+            if (backend instanceof SvgVexFlowBackend) {
+                (backend as SvgVexFlowBackend).export();
+            }
+            // if we add CanvasVexFlowBackend exporting, rename function to export() or exportImages() again
         }
         }
     }
     }
 
 

+ 66 - 34
test/Util/generateImages_browserless.js

@@ -1,17 +1,18 @@
 /*
 /*
   Render each OSMD sample, grab the generated images, and
   Render each OSMD sample, grab the generated images, and
-  dump them into a local directory as PNG files.
+  dump them into a local directory as PNG or SVG files.
 
 
   inspired by Vexflow's generate_png_images and vexflow-tests.js
   inspired by Vexflow's generate_png_images and vexflow-tests.js
 
 
-  This can be used to generate PNGs from OSMD without a browser.
-  It's also used with the visual regression test system in
+  This can be used to generate PNGs or SVGs from OSMD without a browser.
+  It's also used with the visual regression test system (using PNGs) in
   `tools/visual_regression.sh`
   `tools/visual_regression.sh`
   (see package.json, used with npm run generate:blessed and generate:current, then test:visual).
   (see package.json, used with npm run generate:blessed and generate:current, then test:visual).
 
 
-  Note: this script needs to "fake" quite a few browser elements, like window, document, and a Canvas HTMLElement,
+  Note: this script needs to "fake" quite a few browser elements, like window, document,
+  and a Canvas HTMLElement (for PNG) or the DOM (for SVG)   ,
   which otherwise are missing in pure nodejs, causing errors in OSMD.
   which otherwise are missing in pure nodejs, causing errors in OSMD.
-  For that it needs the canvas package installed.
+  For PNG it needs the canvas package installed.
   There are also some hacks needed to set the container size (offsetWidth) correctly.
   There are also some hacks needed to set the container size (offsetWidth) correctly.
 
 
   Otherwise you'd need to run a headless browser, which is way slower,
   Otherwise you'd need to run a headless browser, which is way slower,
@@ -26,14 +27,14 @@ function sleep (ms) {
 
 
 // global variables
 // global variables
 //   (without these being global, we'd have to pass many of these values to the generateSampleImage function)
 //   (without these being global, we'd have to pass many of these values to the generateSampleImage function)
-let [osmdBuildDir, sampleDir, imageDir, pageWidth, pageHeight, filterRegex, mode, debugSleepTimeString] = process.argv.slice(2, 10)
-if (!osmdBuildDir || !sampleDir || !imageDir) {
+let [osmdBuildDir, sampleDir, imageDir, imageFormat, pageWidth, pageHeight, filterRegex, mode, debugSleepTimeString] = process.argv.slice(2, 10)
+if (!osmdBuildDir || !sampleDir || !imageDir || (imageFormat !== 'png' && imageFormat !== 'svg')) {
     console.log('usage: ' +
     console.log('usage: ' +
-        'node test/Util/generateImages_browserless.js osmdBuildDir sampleDirectory imageDirectory [width|0] [height|0] [filterRegex|all|allSmall] [--debug|--osmdtesting] [debugSleepTime]')
+        'node test/Util/generateImages_browserless.js osmdBuildDir sampleDirectory imageDirectory svg|png [width|0] [height|0] [filterRegex|all|allSmall] [--debug|--osmdtesting] [debugSleepTime]')
     console.log('  (use pageWidth and pageHeight 0 to not divide the rendering into pages (endless page))')
     console.log('  (use pageWidth and pageHeight 0 to not divide the rendering into pages (endless page))')
     console.log('  (use "all" to skip filterRegex parameter. "allSmall" with --osmdtesting skips two huge OSMD samples that take forever to render)')
     console.log('  (use "all" to skip filterRegex parameter. "allSmall" with --osmdtesting skips two huge OSMD samples that take forever to render)')
-    console.log('example: node test/Util/generateImages_browserless.js ../../build ./test/data/ ./export 210 297 allSmall --debug 5000')
-    console.log('Error: need osmdBuildDir, sampleDir and imageDir. Exiting.')
+    console.log('example: node test/Util/generateImages_browserless.js ../../build ./test/data/ ./export png 210 297 allSmall --debug 5000')
+    console.log('Error: need osmdBuildDir, sampleDir, imageDir and svg|png arguments. Exiting.')
     process.exit(1)
     process.exit(1)
 }
 }
 let pageFormat
 let pageFormat
@@ -41,6 +42,9 @@ let pageFormat
 if (!mode) {
 if (!mode) {
     mode = ''
     mode = ''
 }
 }
+if (imageFormat !== 'svg') {
+    imageFormat = 'png'
+}
 
 
 let OSMD // can only be required once window was simulated
 let OSMD // can only be required once window was simulated
 const FS = require('fs')
 const FS = require('fs')
@@ -64,6 +68,7 @@ async function init () {
     }
     }
     debug('sampleDir: ' + sampleDir, DEBUG)
     debug('sampleDir: ' + sampleDir, DEBUG)
     debug('imageDir: ' + imageDir, DEBUG)
     debug('imageDir: ' + imageDir, DEBUG)
+    debug('imageFormat: ' + imageFormat, DEBUG)
 
 
     pageFormat = 'Endless'
     pageFormat = 'Endless'
     pageWidth = Number.parseInt(pageWidth)
     pageWidth = Number.parseInt(pageWidth)
@@ -91,7 +96,9 @@ async function init () {
     global.XMLHttpRequest = window.XMLHttpRequest
     global.XMLHttpRequest = window.XMLHttpRequest
     global.DOMParser = window.DOMParser
     global.DOMParser = window.DOMParser
     global.Node = window.Node
     global.Node = window.Node
-    global.Canvas = window.Canvas
+    if (imageFormat === 'png') {
+        global.Canvas = window.Canvas
+    }
 
 
     // fix Blob not found (to support external modules like is-blob)
     // fix Blob not found (to support external modules like is-blob)
     global.Blob = require('cross-blob')
     global.Blob = require('cross-blob')
@@ -179,9 +186,10 @@ async function init () {
         }
         }
     }
     }
 
 
+    const backend = imageFormat === 'png' ? 'canvas' : 'svg'
     const osmdInstance = new OSMD.OpenSheetMusicDisplay(div, {
     const osmdInstance = new OSMD.OpenSheetMusicDisplay(div, {
         autoResize: false,
         autoResize: false,
-        backend: 'canvas',
+        backend: backend,
         pageBackgroundColor: '#FFFFFF',
         pageBackgroundColor: '#FFFFFF',
         pageFormat: pageFormat
         pageFormat: pageFormat
         // defaultFontFamily: 'Arial',
         // defaultFontFamily: 'Arial',
@@ -279,41 +287,65 @@ async function generateSampleImage (sampleFilename, directory, osmdInstance, osm
     debug('xml loaded', DEBUG)
     debug('xml loaded', DEBUG)
     try {
     try {
         osmdInstance.render()
         osmdInstance.render()
+        // there were reports that await could help here, but render isn't a synchronous function, and it seems to work. see #932
     } catch (ex) {
     } catch (ex) {
         console.log('renderError: ' + ex)
         console.log('renderError: ' + ex)
     }
     }
     debug('rendered', DEBUG)
     debug('rendered', DEBUG)
 
 
-    const dataUrls = []
+    const markupStrings = [] // svg
+    const dataUrls = [] // png
     let canvasImage
     let canvasImage
 
 
-    for (let pageNumber = 1; pageNumber < 999; pageNumber++) {
-        canvasImage = document.getElementById('osmdCanvasVexFlowBackendCanvas' + pageNumber)
-        if (!canvasImage) {
-            break
-        }
-        if (!canvasImage.toDataURL) {
-            console.log(`error: could not get canvas image for page ${pageNumber} for file: ${sampleFilename}`)
-            break
+    for (let pageNumber = 1; pageNumber < Number.POSITIVE_INFINITY; pageNumber++) {
+        if (imageFormat === 'png') {
+            canvasImage = document.getElementById('osmdCanvasVexFlowBackendCanvas' + pageNumber)
+            if (!canvasImage) {
+                break
+            }
+            if (!canvasImage.toDataURL) {
+                console.log(`error: could not get canvas image for page ${pageNumber} for file: ${sampleFilename}`)
+                break
+            }
+            dataUrls.push(canvasImage.toDataURL())
+        } else if (imageFormat === 'svg') {
+            let svgElement = document.getElementById('osmdSvgPage' + pageNumber)
+            if (!svgElement) {
+                break
+            }
+            // The important xmlns attribute is not serialized unless we set it here
+            svgElement.setAttribute('xmlns', 'http://www.w3.org/2000/svg')
+            markupStrings.push(svgElement.outerHTML)
         }
         }
-        dataUrls.push(canvasImage.toDataURL())
     }
     }
-    for (let urlIndex = 0; urlIndex < dataUrls.length; urlIndex++) {
-        const pageNumberingString = `${urlIndex + 1}`
+
+    for (let pageIndex = 0; pageIndex < Math.max(dataUrls.length, markupStrings.length); pageIndex++) {
+        const pageNumberingString = `${pageIndex + 1}`
         const skybottomlineString = includeSkyBottomLine ? 'skybottomline_' : ''
         const skybottomlineString = includeSkyBottomLine ? 'skybottomline_' : ''
         // pageNumberingString = dataUrls.length > 0 ? pageNumberingString : '' // don't put '_1' at the end if only one page. though that may cause more work
         // pageNumberingString = dataUrls.length > 0 ? pageNumberingString : '' // don't put '_1' at the end if only one page. though that may cause more work
-        var pageFilename = `${imageDir}/${sampleFilename}_${skybottomlineString}${pageNumberingString}.png`
+        var pageFilename = `${imageDir}/${sampleFilename}_${skybottomlineString}${pageNumberingString}.${imageFormat}`
 
 
-        const dataUrl = dataUrls[urlIndex]
-        if (!dataUrl || !dataUrl.split) {
-            console.log(`error: could not get dataUrl (imageData) for page ${urlIndex + 1} of sample: ${sampleFilename}`)
-            continue
-        }
-        const imageData = dataUrl.split(';base64,').pop()
-        const imageBuffer = Buffer.from(imageData, 'base64')
+        if (imageFormat === 'png') {
+            const dataUrl = dataUrls[pageIndex]
+            if (!dataUrl || !dataUrl.split) {
+                console.log(`error: could not get dataUrl (imageData) for page ${pageIndex + 1} of sample: ${sampleFilename}`)
+                continue
+            }
+            const imageData = dataUrl.split(';base64,').pop()
+            const imageBuffer = Buffer.from(imageData, 'base64')
+
+            debug('got image data, saving to: ' + pageFilename, DEBUG)
+            FS.writeFileSync(pageFilename, imageBuffer, { encoding: 'base64' })
+        } else if (imageFormat === 'svg') {
+            const markup = markupStrings[pageIndex]
+            if (!markup) {
+                console.log(`error: could not get markup (SVG data) for page ${pageIndex + 1} of sample: ${sampleFilename}`)
+                continue
+            }
 
 
-        debug('got image data, saving to: ' + pageFilename, DEBUG)
-        FS.writeFileSync(pageFilename, imageBuffer, { encoding: 'base64' })
+            debug('got svg markup data, saving to: ' + pageFilename, DEBUG)
+            FS.writeFileSync(pageFilename, markup, { encoding: 'utf-8' })
+        }
 
 
         // debug: log memory usage
         // debug: log memory usage
         // const usage = process.memoryUsage()
         // const usage = process.memoryUsage()