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.13k stars 1.3k forks source link

Adding a shadow #1490

Closed ilicmarko closed 5 years ago

ilicmarko commented 5 years ago

Is it even possible to add a shadow to an image?

I have been trying to add a shadow to an image but I doesn't seem possible.

I couldn't get the simple shadow running because when you create a shadow with SVG, for example:

<svg width="200" height="200">
  <defs>
    <filter id="shadow">
      <feDropShadow dx="4" dy="8" stdDeviation="4"/>
    </filter>
  </defs>

  <circle cx="50%" cy="50%" r="80" style="filter:url(#shadow);"/>
</svg>

and overlay that with overlayWith(Buffer.from(<ABOVE SVG>), { cutout: true }) it will simply ignore the filter (shadow) and cut it out like i wasn't there.

The idea I have as a workaround is make an SVG that is the same shape and size, position it behind the image and add a blur to the SVG.

<svg width="200" height="200">
  <defs>
    <filter id="f1" x="0" y="0">
      <feGaussianBlur in="SourceGraphic" stdDeviation="15" />
    </filter>
  </defs>
  <rect width="90" height="90" fill="yellow" filter="url(#f1)" />
</svg>

Here is an HTML example of how that would look (JSFiddle):

<svg width="200" height="200">

  <filter id="blurMe">
    <feGaussianBlur in="SourceGraphic" stdDeviation="5" />
  </filter>

  <circle cx="60" cy="60" r="50" fill="black" filter="url(#blurMe)" id="shadow" />
  <circle cx="60"  cy="60" r="52" fill="dodgerblue" id="image" />
</svg>

The problem with this idea is I don't know how to stack layers in Sharp.

Thank you in advance.

lovell commented 5 years ago

Hello, feDropShadow is unsupported by librsvg - it's only at W3C draft status - https://drafts.fxtf.org/filter-effects/

See #728 for multiple composition, some of the linked issues provide possible workarounds for now.

lovell commented 5 years ago

Hope this helped, please feel free to re-open with more details if not.

pouretrebelle commented 4 years ago

Came across this issue while trying to do something similar, feDropShadow is still unsupported but I managed to achieve what I wanted to do by blurring a rectangle (an svg the same size as the input image, with a margin) to act as the shadow and then using composites. Hope this helps anyone looking for the same thing!

const OUTPUT_BACKGROUND = '#bada55'
const OUTPUT_WIDTH = 1200
const OUTPUT_HEIGHT = 630
const SHADOW_MARGIN = 40
const SHADOW_BLUR = 15
const SHADOW_OFFSET = 6
const SHADOW_OPACITY = 0.3

const stream = await sharp(inputPath)
const { width, height } = await stream.metadata()

const shadow = await sharp(
  Buffer.from(`
    <svg
      width="${width + SHADOW_MARGIN * 2}"
      height="${height + SHADOW_MARGIN * 2}"
    >
      <rect
        width="${width}"
        height="${height}"
        x="${SHADOW_MARGIN}"
        y="${SHADOW_MARGIN + SHADOW_OFFSET}"
        fill="rgba(0, 0, 0, ${SHADOW_OPACITY})"
      />
    </svg>`)
)
  .blur(SHADOW_BLUR)
  .toBuffer()

const image = await stream
  .resize({
    height,
    width,
  })
  .toBuffer()

await sharp({
  create: {
    width: OUTPUT_WIDTH,
    height: OUTPUT_HEIGHT,
    channels: 3,
    background: OUTPUT_BACKGROUND,
  },
})
  .composite([
    { input: shadow, blend: 'multiply' },
    { input: image, blend: 'over' },
  ])
  .jpeg()
  .toFile(outputPath)
urbanisierung commented 3 years ago

Wow, thanks @papandreou! This is exactly what I was looking for!

wchaws commented 2 years ago

@ilicmarko @urbanisierung @lovell According to https://gitlab.gnome.org/GNOME/librsvg/-/merge_requests/529

Now you can filter="drop-shadow(<color> <dx> <dy> < stdDeviation>)"

<?xml version="1.0" encoding="UTF-8"?>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="400" height="400">
  <rect x="100" y="100" width="200" height="200" fill="green" filter="drop-shadow(#ff0000 1px 4px 6px)"/>
</svg>
armandolio commented 2 years ago

Hello! @wchaws @lovell I'm trying to add shadow to an SVG image (black shadow). I'm doing this:

const imageData = await sharp(inputBuffer);
const { width, height } = await imageData.metadata();
const maxWidthHeight = Math.min(width, height);
const widthHeight = maxWidthHeight;
const imageRadio = widthHeight / 2.3;

const roundedCorners = Buffer.from(
    `<svg width="100%" height="100%">
        <circle 
        filter="drop-shadow(0 0 5px black)"
        fill="#black"
        style="padding: 10px;"
        cx="${imageRadio}" cy="${imageRadio}" r="${imageRadio}"/>
    </svg>`,
);

// Rounded
await sharp(inputBuffer)
    .composite([{
        input: roundedCorners,
        blend: 'dest-in',
    }])
    .toFile(imagePath)

and this is the result:

image

I'm adding the shadow but it is not black. Why?

Thanks, guys!

Armando

wchaws commented 2 years ago

@armandoarmando Actually, you're adding shadow to your circle mask not adding shadow to compositing result. That's why you can see blurry edge. I think you can add another circle with shadow in the background.

armandolio commented 2 years ago

@wchaws I'm trying to make it like this https://github.com/lovell/sharp/issues/1490#issuecomment-611779237 but the shadow is cut... I think I'm very close, but I can't figure out what is the problem... any idea?

image

const OUTPUT_WIDTH = 630
const OUTPUT_HEIGHT = 630
const SHADOW_MARGIN = 5
const SHADOW_BLUR = 5
const SHADOW_OFFSET = 6
const SHADOW_OPACITY = 0.5

const stream = await sharp(inputBuffer)
const { width, height } = await stream.metadata();

const radioShadowImage = (width + SHADOW_MARGIN * 2) / 2;
const radioImage = width / 2;

const shadow = await sharp(
    Buffer.from(`
        <svg
        width="${width + SHADOW_MARGIN}"
        height="${height + SHADOW_MARGIN}"
        >
        <circle
        fill="rgba(0, 0, 0, ${SHADOW_OPACITY})"

        cx="${radioShadowImage}" cy="${radioShadowImage}" r="${radioShadowImage}"
        />
        </svg>`)
    )
    .blur(SHADOW_BLUR)
    .toBuffer();

const roundedCorners = Buffer.from(
    `<svg width="550" height="550">
            <circle
            cx="${radioImage}" cy="${radioImage}" r="${radioImage}"/>
        </svg>`,
);

const image = await sharp(inputBuffer)
    .composite([
        {
            input: roundedCorners,
            blend: 'dest-in',
        },
    ])
    .toBuffer();

await sharp({
    create: {
        width: OUTPUT_WIDTH,
        height: OUTPUT_HEIGHT,
        channels: 4,
        background: { r: 255, g: 255, b: 255, alpha: 0 },
    },
})
    .composite([
        {
            input: shadow,
            blend: 'over',
            gravity: 'centre',
        },
        {
            input: image,
            blend: 'over',
            gravity: 'centre',
        },
    ])
    .toFile('image.png');
wchaws commented 2 years ago

@armandoarmando To give the shadow some room, I believe you should reduce the size of the circle a little bit.

chaoyangnz commented 1 month ago

drop-shadow is super slow