lovell / sharp

High performance Node.js image processing, the fastest module to resize JPEG, PNG, WebP, AVIF and TIFF images. Uses the libvips library.

Home Page:https://sharp.pixelplumbing.com

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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 by npm 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() or keepExif() does not fix this problem.

What are the steps to reproduce?

  1. Use the below code to generate an image with rounded corners.
  2. 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

image
    .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, sets palette to true

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

image

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

Changed to crispEdges the corners slightly changed but still are bad both text and corners :'(
image

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