lovell / sharp

High performance Node.js image processing, the fastest module to resize JPEG, PNG, WebP, AVIF and TIFF images. Uses the libvips library.
https://sharp.pixelplumbing.com
Apache License 2.0
29.33k stars 1.3k forks source link

PNG from SVG Buffer fails with "Input buffer has corrupt header: glib: XML parse error" #4271

Closed AlbinoGeek closed 1 week ago

AlbinoGeek commented 1 week ago

Possible bug

Is this a possible bug in a feature of sharp, unrelated to installation?

Are you using the latest version of sharp?

What is the output of running npx envinfo --binaries --system --npmPackages=sharp --npmGlobalPackages=sharp?

  System:
    OS: Windows 10 10.0.19045
    CPU: (12) x64 Intel(R) Core(TM) i5-10500H CPU @ 2.50GHz
    Memory: 43.09 GB / 63.79 GB
  Binaries:
    Node: 22.11.0 - C:\Program Files\nodejs\node.EXE       
    Yarn: 4.5.1 - C:\Program Files (x86)\Yarn\bin\yarn.CMD 
    npm: 10.8.3 - C:\Program Files\nodejs\npm.CMD
  npmPackages:
    sharp: ^0.33.5 => 0.33.5

Does this problem relate to file caching?

Does this problem relate to images appearing to have been rotated by 90 degrees?

What are the steps to reproduce?

1) Use ReactDOMServer.renderToString, ReactDOM.render, ReactDOMServer.renderToStaticMarkup, or a literal string with <svg>... in it. 2) Attempt to convert the SVG to PNG using various sample codes.

  const svg = ReactDOMServer.renderToString(<SvgComponent />)
  console.log('Rendering composite...')
  const png = sharp(Buffer.from(svg))
    .png()
    .resize(24, 24)
  console.log('Converting to buffer...')
  const buf = await png.toBuffer() // errors here
  console.log('Returning base64...')

I've also tried the simpler

  const svg = ReactDOMServer.renderToString(<SvgComponent />)
  const buffer = await sharp(Buffer.from(svg))
    .png()
    .toBuffer()

Same error.

Error: Input buffer has corrupt header: glib: XML parse error: Error domain 1 code 5 on line 1 column 425 of data: Extra content at the end of the document

You may ask, "why not composite?" Well, see my previous issue.

What is the expected behaviour?

PNG output

Please provide a minimal, standalone code sample, without other dependencies, that demonstrates this problem

"Without dependencies" is hardly possible in the NPM ecosystem, come on now. However, this can be reproduced in the minimal create-react-app as well as the create-next-app (this is a NextJS application.)

Code directly from my app:

import Facebook from '@mui/icons-material/Facebook' // or any other icon
import ReactDOMServer from 'react-dom/server'
import sharp from 'sharp'

const svgToImage = async (SvgComponent: typeof Facebook): Promise<Buffer> => {
  // Also tried all the other render methods, output is valid SVG
  const svg = ReactDOMServer.renderToStaticMarkup(<SvgComponent />)
  return await sharp(Buffer.from(svg))
    .png()
    .toBuffer()
}

Please provide sample image(s) that help explain this problem

Any image from MaterialUI Icons, literally any of them (yes, I looped through the entire namespace to test.) What I want working atm however are <Facebook />, <Instagram /> etc brand logos.

lovell commented 1 week ago

or a literal string with \<svg>... in it.

Please can you provide an example string that represents a valid SVG that fails in this manner.

AlbinoGeek commented 1 week ago
<style data-emotion="css 1umw9bq-MuiSvgIcon-root">.css-1umw9bq-MuiSvgIcon-root{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;width:1em;height:1em;display:inline-block;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;-webkit-transition:fill 200ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;transition:fill 200ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;fill:currentColor;font-size:1.5rem;}</style><svg class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium css-1umw9bq-MuiSvgIcon-root" focusable="false" aria-hidden="true" viewBox="0 0 24 24" data-testid="FacebookIcon"><path d="M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2m13 2h-2.5A3.5 3.5 0 0 0 12 8.5V11h-2v3h2v7h3v-7h3v-3h-3V9a1 1 0 0 1 1-1h2V5z"></path></svg>

I have also tried stripping the <style> tag, although this is counter-productive for my use-case. Also, last I checked, SVGs supported both classes and inline styles. It's entirely fair if sharp doesn't, but I would expect a slightly more useful error message if that's the case.

lovell commented 1 week ago

Thanks, this sample string is not valid XML so that's why you're seeing an "XML parse error" message. As you suggest you'll need to (re)move the <style> element as valid SVG must be valid XML, and valid XML must have a single root node.

AlbinoGeek commented 1 week ago

Mmmm.... I'm having trouble understanding how both Resend.js and NextJS itself manage to display these SVGs, using the sharp library no less, while retaining the style. I suppose they have some magic surrounding forcing the styles into the SVG's individual path/rect/etc, if I had to guess?

How would you recommend I approach this issue then, considering that removing the styling destroys the images (in the case of these brand images, they do not render correctly without the styles.) My only other option appears to be an entire headless browser just to render an SVG, which seems painful and redundant.

The following absolute madhouse has gotten me close, but not quite:

Edit, Spaghetti rewrote to be.... readable?

const convertSvgToImage = async (
  MuiSvgIcon: typeof Facebook
): Promise<Buffer> => {
  const html = ReactDOMServer.renderToString(<MuiSvgIcon />)
  const css = RegExp(/MuiSvgIcon-root{(.*)}/).exec(html)?.[1] ?? ''
  return await sharp(Buffer.from(html
    .replace(/<style data-emotion[^>]*>.*<\/style>/, '')
    .replace(/class="[^"]*"/, `style="${css}"`)
  )).resize(48, 48).png().toBuffer()
}
AlbinoGeek commented 1 week ago

Well I suppose if anyone find this later...

The above snippet will get you "close enough", maybe?

The colours are wrong, the images are fuzzy, but hey, it "works"?

Closing as this is apparently just not a feature of sharp, which, fair.