2

I have a serverless function which returns a SVG based on a text sent as a query parameter.

module.exports = (req, res) => {
  const { yourName } = req.query;

  res.setHeader("Content-Type", "image/svg+xml");

  const svg = `
    <svg fill="none" viewBox="0 0 600 400" width="600" height="400" xmlns="http://www.w3.org/2000/svg">
      <foreignObject width="100%" height="100%">
        <div xmlns="http://www.w3.org/1999/xhtml">
          <style>
            .title {
              font-size: 10vh;
              font-weight: regular;
              background-color: #ffddff;
            }
          </style>
            
          <p class="title">Hello ${yourName}</p> 
          
        </div>
      </foreignObject>
    </svg>
  `;

  res.send(svg);

};

As an example, when I call /api?yourName=world, it returns me the following SVG;

hello world by a serveless api

(Deployed URL: https://vercel-test-ruddy.vercel.app/api?world)

What I need?

I need to return the SVG as an image (PNG/JPEG). So, the my function would look like,

module.exports = (req, res) => {
  const { yourName } = req.query;

  res.setHeader("Content-Type", "image/png");

  const svg = `
    <svg fill="none" viewBox="0 0 600 400" width="600" height="400" xmlns="http://www.w3.org/2000/svg">
      <foreignObject width="100%" height="100%">
        <div xmlns="http://www.w3.org/1999/xhtml">
          <style>
            .title {
              font-size: 10vh;
              font-weight: regular;
              background-color: #ffddff;
            }
          </style>
            
          <p class="title">Hello ${yourName}</p> 
          
        </div>
      </foreignObject>
    </svg>
  `;

  res.send( svgToPng(svg) );

};

svgToPng = (svg) => {
   // not defined
}

Did I refer other SO questions?

Yes. There are few questions regarding converting SVG to PNG:

Convert SVG to image (JPEG, PNG, etc.) in the browser
How to Exporting SVG as PNG format

Almost every answer talks about converting SVG to canvas and then canvas to PNG. And also answers are much older too. Can we do it without using a canvas?

In my project I actually don't have a html document, I have only one single .js file (As I know we need HTML5 to use canvas).

Notes

I have used using Vercel to deploy my serverless function which is;

Deployed at: https://vercel-test-ruddy.vercel.app/api?yourName=YOUR_NAME_HERE
GitRepo: https://github.com/tharindusathis/vercel-test

Tharindu Sathischandra
  • 1,654
  • 1
  • 15
  • 37
  • To convert SVG to bitmap it needs to be rasterized ( https://en.wikipedia.org/wiki/Rasterisation ) Canvas is only a pixel store while encoding image file via `toBlob` or `toDataURL` . Rasterization is extremely complex (if you want quality), and would be agonisingly slow in JS without a GPU (or equivalent processing power). Encoding a bitmap to an img file is easy. There are likely many JS open source encoders you can use. Rasterize SVG consult vercel documentation ask What type of back end? Is there a SVG rasterize component? – Blindman67 Sep 13 '20 at 14:47
  • To add to what @Blindman67 said, here you don't even need *just* an svg renderer, you need an HTML + CSS renderer (to render the foreignObject). The only SVG renderers I know of that are able to do this are web browsers. What does "In my project I actually don't have a html document," mean? Where is this project ran from? If only from the server, then you could maybe consider running an headless browser like PhantomJS, though I don't know if vercel can do something like that (I really don't know Vercel). – Kaiido Oct 15 '20 at 07:05

2 Answers2

1

The best way to deal with this that I have found so far is by using a headless chrome and getting a screenshot. As an example by using puppeteer I could get a screenshot of the viewport like follows,

const browser = await puppeteer.launch();
const page = await browser.newPage();

let yourName = 'world'

const svg = `
    <svg fill="none" viewBox="0 0 600 400" width="600" height="400" xmlns="http://www.w3.org/2000/svg">
      <foreignObject width="100%" height="100%">
        <div xmlns="http://www.w3.org/1999/xhtml">
          <style>
            .title {
              font-size: 10vh;
              font-weight: regular;
              background-color: #ffddff;
            }
          </style>
            
          <p class="title">Hello ${yourName}</p> 
          
        </div>
      </foreignObject>
    </svg>
  `;

await page.setViewport({ width: 2048, height: 1170 });
await page.setContent(svg);

console.log(await page.content());
await page.screenshot({path: 'screenshot.png'});
await browser.close();

Then it will be easy to send the png as the response.

Tharindu Sathischandra
  • 1,654
  • 1
  • 15
  • 37
  • 1
    This is the one I've been looking for for 3 days. This is the real solution to the foreignObject tainted canvas problem. – MrOli3000 May 14 '21 at 02:40
  • What's the performance of this like? Do you launch and close the browser for every request to prevent memory leak? – yoyodunno May 26 '23 at 02:00
0

Looks like your deconstructing your query param, in which case its searching for that specific object property within the query.

It should work if you explicitly use yourName as query param.

https://vercel-test-ruddy.vercel.app/api?yourName=world

or don't decouple the req.query object property.

<iframe src="https://vercel-test-ruddy.vercel.app/api?yourName=world" width="450px" height="450px">
TheoNeUpKID
  • 763
  • 7
  • 11
  • Thanks for the suggestion, but it totally works fine without sending `yourName` as a query param. In that case it will just show as `undefined`. [https://vercel-test-ruddy.vercel.app/api](https://vercel-test-ruddy.vercel.app/api). Actually that is not a much problem here. – Tharindu Sathischandra Sep 13 '20 at 15:04
  • if this was helpful please mark as answer. And it doesn work unless you add yourName as a query param. Otherwise it returns undefined. My assumption is that it should supply a value. If this is not the case then please update your question. Thanks – TheoNeUpKID Sep 13 '20 at 15:09
  • I think you have misunderstand the question. What I need is to return a `png` image of instead of `svg`. Its currently return a `svg` as expected. So, its better if you could read the question again. Anyway thanks for the effort. – Tharindu Sathischandra Sep 13 '20 at 15:46