Home > OS >  Capture chrome browser engine output using javascript in order to capture rasterized frames of an SM
Capture chrome browser engine output using javascript in order to capture rasterized frames of an SM

Time:06-26

My ultimate goal is that I'm trying to convert an animated SMIL SVG into an APNG file. I have found no easy way to do this, and so I'm doing something a roundabout: I've written a node.js express.js app that hosts a simple backend to get svg images on my local filesystem, and I've written a vue.js app that will go and pull those images and render them on a google chrome browser. I then play the SVG and try to capture rendered "frames", and save those frames as static png files (about 30 static PNG files for each second of SVG animation). I then plan to take those static png files & convert them over to a single animated png / apng file using another program. The part that I'm stuck on: actually trying to capture a rasterized "frame" of the svg.

Here's a snippet of code from my vue.js app which requests an SVG file, and renders it to a div, and then it tries to call a function takeSnap().

    const file = await RequestsService.getFile(i);
    const div = document.getElementById("svgContainer");
    div.innerHTML = file.svg;

    const { width, height } = div.children[0].getBBox();
    console.debug(`width: ${width}, height: ${height}`);
    const svg = div.children[0];
    await svg.pauseAnimations();
    let time = 0.0;
    const interval = 1.0 / numFrames; // interval in seconds.
    let count = 0;
    while (time < file.duration) {
      console.log(`time=${time}`);
      await svg.setCurrentTime(time);
      await this.takeSnap(svg, width, height);
      time  = interval;
      console.debug(`file: ${file.fileName}_${count}`);
    }
    await svg.setCurrentTime(file.duration);
    await this.takeSnap(svg, width, height);

I haven't been able to make a proper implementation of takeSnap(). I know that there are a slew of tools such as Canvg or HTML2png that go and directly render a webpage from the DOM. I've tried many different libraries, but none of them seem to be able to correctly render the frame of the SVG that chrome is correctly rendering. I don't blame the libraries: going from animated SVG XML file to actually rasterized pixels is a very difficult problem I think. But Chrome can do it, and what I'm wondering is... can I capture the browser engine output of chrome somehow?

Is there a way that I can get the rasterized pixel data produced by the blink browser engine in chrome & then save that rasterized pixel data into a png file? I know that I'll lose the transparency data of the SVG, but that's okay, I'll work around that later.

CodePudding user response:

OK, this got a bit complicated. The script can now take SMIL animations with both <animate> and <animateTransform>. Essentially I take a snap shot of the SVG using Window.getComputedStyle() (for <animate> elements) and the matrix value using SVGAnimatedString.animVal (for <animateTransform> elements). A copy of the SVG is turned into a data URL and inserted into a <canvas>. From here it is exported as a PNG image.

In this example I use a data URL in the fetch function, but this can be replaced by a URL. The script has been tested with the SVG that OP provided.

var svgcontainer, svg, canvas, ctx, output, interval;
var num = 101;

const nsResolver = prefix => {
  var ns = {
    'svg': 'http://www.w3.org/2000/svg',
    'xlink': 'http://www.w3.org/1999/xlink'
  };
  return ns[prefix] || null;
};

const takeSnap = function() {
  // get all animateTransform elements
  let animateXPath = document.evaluate('//svg:*[svg:animateTransform]', svg, nsResolver, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);

  // store all animateTransform animVal.matrix in a dataset attribute
  Object.keys([...Array(animateXPath.snapshotLength)]).forEach(i => {
    let node = animateXPath.snapshotItem(i);
    let mStr = [...node.transform.animVal].map(animVal => {
      let m = animVal.matrix;
      return `matrix(${m.a} ${m.b} ${m.c} ${m.d} ${m.e} ${m.f})`;
    }).join(' ');
    node.dataset.transform = mStr;
  });

  // get all animate elements
  animateXPath = document.evaluate('//svg:animate', svg, nsResolver, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);

  // store all animate properties in a dataset attribute on the target for the animation
  Object.keys([...Array(animateXPath.snapshotLength)]).forEach(i => {
    let node = animateXPath.snapshotItem(i);
    let propName = node.getAttribute('attributeName');
    let target = node.targetElement;
    let computedVal = getComputedStyle(target)[propName];
    target.dataset[propName] = computedVal;
  });

  // create a copy of the SVG DOM
  let parser = new DOMParser();
  let svgcopy = parser.parseFromString(svg.outerHTML, "application/xml");

  // find all elements with a dataset attribute
  animateXPath = svgcopy.evaluate('//svg:*[@*[starts-with(name(), "data")]]', svgcopy, nsResolver, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);

  // copy the animated property to a style or attribute on the same element
  Object.keys([...Array(animateXPath.snapshotLength)]).forEach(i => {
    let node = animateXPath.snapshotItem(i);
    // for each data-
    for (key in node.dataset) {
      if (key == 'transform') {
        node.setAttribute(key, node.dataset[key]);
      } else {
        node.style[key] = node.dataset[key];
      }
    }
  });

  // find all animate and animateTransform elements from the copy document
  animateXPath = svgcopy.evaluate('//svg:*[starts-with(name(), "animate")]', svgcopy, nsResolver, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);

  // remove all animate and animateTransform elements from the copy document
  Object.keys([...Array(animateXPath.snapshotLength)]).forEach(i => {
    let node = animateXPath.snapshotItem(i);
    node.remove();
  });

  // create a File object
  let file = new File([svgcopy.rootElement.outerHTML], 'svg.svg', {
    type: "image/svg xml"
  });
  // and a reader
  let reader = new FileReader();

  reader.addEventListener('load', e => {
    /* create a new image assign the result of the filereader
    to the image src */
    let img = new Image();
    // wait got load
    img.addEventListener('load', e => {
      // update canvas with new image
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      ctx.fillStyle = 'white';
      ctx.fillRect(0, 0, canvas.width, canvas.height);
      ctx.drawImage(e.target, 0, 0);
      // create PNG image based on canvas
      let img = new Image();
      img.src = canvas.toDataURL("image/png");
      output.append(img);
      //let a = document.createElement('A');
      //a.textContent = `Image-${num}`;
      //a.href = canvas.toDataURL("image/png");
      //a.download = `Image-${num}`; 
      //num  ;
      //output.append(a);
    });
    img.src = e.target.result;
  });
  // read the file as a data URL
  reader.readAsDataURL(file);
};

document.addEventListener('DOMContentLoaded', e => {
  svgcontainer = document.getElementById('svgcontainer');
  canvas = document.getElementById('canvas');
  output = document.getElementById('output');
  ctx = canvas.getContext('2d');

  fetch('data:image/svg xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMCAxMCI CiAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjQiIGhlaWdodD0iNCIgZmlsbD0ibmF2eSI CiAgICA8YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSJmaWxsIiBkdXI9IjNzIiBrZXlUaW1lcz0iMDsuMTsuMjsxIiB2YWx1ZXM9Im5hdnk7bGlnaHRibHVlO2JsdWU7bmF2eSIvPgogICAgPGFuaW1hdGVUcmFuc2Zvcm0gYWRkaXRpdmU9InN1bSIgYXR0cmlidXRlTmFtZT0idHJhbnNmb3JtIiBkdXI9IjNzIiBrZXlUaW1lcz0iMDsuNTsxIiB0eXBlPSJ0cmFuc2xhdGUiIHZhbHVlcz0iMCwwOzIsMjs0LDYiLz4KICAgIDxhbmltYXRlVHJhbnNmb3JtIGFkZGl0aXZlPSJzdW0iIGF0dHJpYnV0ZU5hbWU9InRyYW5zZm9ybSIgZHVyPSIzcyIga2V5VGltZXM9IjA7LjU7MSIgdHlwZT0icm90YXRlIiB2YWx1ZXM9IjAsMiwyOzMwLDIsMjstMTAsMiwyOyIvPgogIDwvcmVjdD4KPC9zdmc CgoK').then(res => res.text()).then(text => {
    let parser = new DOMParser();
    let svgdoc = parser.parseFromString(text, "application/xml");
    canvas.width = svgdoc.rootElement.getAttribute('width');
    canvas.height = svgdoc.rootElement.getAttribute('height');

    svgcontainer.innerHTML = svgdoc.rootElement.outerHTML;
    svg = svgcontainer.querySelector('svg');

    // set interval
    interval = setInterval(takeSnap, 50);

    // get all 
    let animateXPath = document.evaluate('//svg:*[starts-with(name(), "animate")]', svg, nsResolver, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);

    let animationArr = Object.keys([...Array(animateXPath.snapshotLength)]).map(i => {
      let node = animateXPath.snapshotItem(i);
      return new Promise((resolve, reject) => {
        node.addEventListener('endEvent', e => {
          resolve();
        });
      });
    });
    Promise.all(animationArr).then(value => {
      clearInterval(interval);
    });
  });
});
<div style="display:flex">
  <div id="svgcontainer"></div>
  <canvas id="canvas" width="200" height="200"></canvas>
</div>
<p>Exported PNGs:</p>
<div id="output"></div>

  • Related