Poor Image Quality with Rounded Corners Using sharp
dioKaratzas opened this issue · comments
Possible bug
Is this a possible bug in a feature of sharp, unrelated to installation?
- Running
npm install sharp
completes without error. - Running
node -e "require('sharp')"
completes without error.
Are you using the latest version of sharp?
- I am using the latest version of
sharp
as reported bynpm view sharp dist-tags.latest
.
What is the output of running npx envinfo --binaries --system --npmPackages=sharp --npmGlobalPackages=sharp
?
System:
OS: macOS 14.5
CPU: (12) arm64 Apple M2 Pro
Memory: 1.16 GB / 32.00 GB
Shell: 5.9 - /bin/zsh
Binaries:
Node: 20.15.0 - /usr/local/bin/node
npm: 10.7.0 - /usr/local/bin/npm
npmPackages:
sharp: ^0.33.4 => 0.33.4
Does this problem relate to file caching?
- Adding
sharp.cache(false)
does not fix this problem.
Does this problem relate to images appearing to have been rotated by 90 degrees?
- Using
rotate()
orkeepExif()
does not fix this problem.
What are the steps to reproduce?
- Use the below code to generate an image with rounded corners.
- Observe the poor quality and non-smooth corners in the generated image.
What is the expected behaviour?
The generated image should have smooth, high-quality rounded corners.
Please provide a minimal, standalone code sample, without other dependencies, that demonstrates this problem
const sharp = require('sharp');
const { Buffer } = require('safe-buffer');
const createImage = async () => {
const width = 600;
const height = 200;
const lightColorHex = "#cccccc"; // Fixed light color
const darkColorHex = "#333333"; // Fixed dark color
const lightText = "Light"; // Fixed light text
const darkText = "Dark"; // Fixed dark text
const radius = 12; // Fixed radius
const lightTextColor = "#000000"; // Fixed light text color
const darkTextColor = "#FFFFFF"; // Fixed dark text color
// Create the base SVG with the gradient and border
const svgImage = `
<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
<defs>
<style type="text/css">
<![CDATA[
@import url('https://fonts.googleapis.com/css2?family=Barlow:wght@400;700&display=swap');
.barlow-font { font-family: 'Barlow', sans-serif; }
]]>
</style>
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:${lightColorHex};stop-opacity:1" />
<stop offset="50%" style="stop-color:${lightColorHex};stop-opacity:1" />
<stop offset="50%" style="stop-color:${darkColorHex};stop-opacity:1" />
<stop offset="100%" style="stop-color:${darkColorHex};stop-opacity:1" />
</linearGradient>
</defs>
<rect x="0" y="0" width="100%" height="100%" fill="url(#grad1)" rx="${radius}" ry="${radius}" />
<text x="25%" y="45%" font-size="20" class="barlow-font" dominant-baseline="middle" text-anchor="middle" fill="${lightTextColor}">${lightText}</text>
<text x="25%" y="65%" font-size="20" class="barlow-font" dominant-baseline="middle" text-anchor="middle" fill="${lightTextColor}">${lightColorHex}</text>
<text x="75%" y="45%" font-size="20" class="barlow-font" dominant-baseline="middle" text-anchor="middle" fill="${darkTextColor}">${darkText}</text>
<text x="75%" y="65%" font-size="20" class="barlow-font" dominant-baseline="middle" text-anchor="middle" fill="${darkTextColor}">${darkColorHex}</text>
</svg>`;
const maskSvg = `
<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
<rect x="0" y="0" width="100%" height="100%" rx="${radius}" ry="${radius}" fill="white"/>
</svg>`;
const buffer = Buffer.from(svgImage);
const maskBuffer = Buffer.from(maskSvg);
// Composite the base SVG and the mask SVG
return sharp(buffer)
.composite([{ input: maskBuffer, blend: 'dest-in' }])
.png({ quality: 100 }) // Ensure maximum quality
.toBuffer();
};
createImage().then((buffer) => {
const fs = require('fs');
fs.writeFileSync('output.png', buffer);
}).catch((err) => {
console.error(err);
});
Please provide sample image(s) that help explain this problem
![image](https://private-user-images.githubusercontent.com/7944881/347329236-bf6504fb-ca46-4a22-997f-e3bb11b1eb99.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MjQzNzMyMTQsIm5iZiI6MTcyNDM3MjkxNCwicGF0aCI6Ii83OTQ0ODgxLzM0NzMyOTIzNi1iZjY1MDRmYi1jYTQ2LTRhMjItOTk3Zi1lM2JiMTFiMWViOTkucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI0MDgyMyUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNDA4MjNUMDAyODM0WiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9ZmIyZjRiNjY3NWMzOGFkZDgyNzEzZmY2OWI3ZDE4YzA1NWM5YjM1NmYyZDFkMjc0MGYyMmQ5ZTM3NmFmNmYxNSZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QmYWN0b3JfaWQ9MCZrZXlfaWQ9MCZyZXBvX2lkPTAifQ.gYP5RHmAJhoq_o9mGUT4rWUUJUlVWlJIew4qcw3DCxc)
.png({ quality: 100 }) // Ensure maximum quality
Did you see https://sharp.pixelplumbing.com/api-output#png ?
[options.quality]
use the lowest number of colours needed to achieve given quality, setspalette
totrue
When you provide a value for quality
you are choosing to use lossy, palette-based PNG output. If you want lossless PNG output, do not provide a quality
value.
Hey @lovell thanks for your fast response.
I removed the quality
and the result is the same
Are you referring to the aliasing? Did you see the shape-rendering property?
Updated my code with your suggestion:
const svgImage = `
<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg" shape-rendering="geometricPrecision">
<defs>
<style type="text/css">
<![CDATA[
@import url('https://fonts.googleapis.com/css2?family=Barlow:wght@400;700&display=swap');
.barlow-font { font-family: 'Barlow', sans-serif; }
]]>
</style>
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:${lightColorHex};stop-opacity:1" />
<stop offset="50%" style="stop-color:${lightColorHex};stop-opacity:1" />
<stop offset="50%" style="stop-color:${darkColorHex};stop-opacity:1" />
<stop offset="100%" style="stop-color:${darkColorHex};stop-opacity:1" />
</linearGradient>
</defs>
<rect x="0" y="0" width="100%" height="100%" fill="url(#grad1)" rx="${radius}" ry="${radius}" shape-rendering="geometricPrecision" />
<text x="25%" y="45%" font-size="20" class="barlow-font" dominant-baseline="middle" text-anchor="middle" fill="${lightTextColor}" shape-rendering="geometricPrecision">${lightText}</text>
<text x="25%" y="65%" font-size="20" class="barlow-font" dominant-baseline="middle" text-anchor="middle" fill="${lightTextColor}" shape-rendering="geometricPrecision">${lightColorHex}</text>
<text x="75%" y="45%" font-size="20" class="barlow-font" dominant-baseline="middle" text-anchor="middle" fill="${darkTextColor}" shape-rendering="geometricPrecision">${darkText}</text>
<text x="75%" y="65%" font-size="20" class="barlow-font" dominant-baseline="middle" text-anchor="middle" fill="${darkColorHex}" shape-rendering="geometricPrecision">${darkColorHex}</text>
</svg>`;
const maskSvg = `
<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg" shape-rendering="geometricPrecision">
<rect x="0" y="0" width="100%" height="100%" rx="${radius}" ry="${radius}" fill="white"/>
</svg>`;
But unfortunately same result
I think geometricPrecision
is usually the default. A value of crispEdges
will increase aliasing, which is my best guess as to what you're looking to achieve.
https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/shape-rendering#crispedges
I'm unsure what your expected output is here, as this is how a vector SVG is rasterised to a bitmap PNG.
Perhaps you are comparing an SVG rendered in a web browser on an retina display to the output of a SVG rasterised to PNG? If so, you are not comparing like with like - you'll need to double or triple the output dimensions to begin to make a fair comparison.
@lovell you are right its the Retina thing! thank you