You've already forked imgproxy
mirror of
https://github.com/imgproxy/imgproxy.git
synced 2025-12-03 23:19:17 +02:00
Fix feDropShadow SVG filter when IMGPROXY_SVG_FIX_UNSUPPORTED is true
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
### Add
|
||||
- Add `IMGPROXY_SVG_FIX_UNSUPPORTED` config.
|
||||
|
||||
## [3.8.0] - 2022-10-06
|
||||
### Add
|
||||
|
||||
@@ -53,6 +53,7 @@ var (
|
||||
AutoRotate bool
|
||||
EnforceThumbnail bool
|
||||
ReturnAttachment bool
|
||||
SvgFixUnsupported bool
|
||||
|
||||
EnableWebpDetection bool
|
||||
EnforceWebp bool
|
||||
@@ -228,6 +229,7 @@ func Reset() {
|
||||
AutoRotate = true
|
||||
EnforceThumbnail = false
|
||||
ReturnAttachment = false
|
||||
SvgFixUnsupported = false
|
||||
|
||||
EnableWebpDetection = false
|
||||
EnforceWebp = false
|
||||
@@ -402,6 +404,7 @@ func Configure() error {
|
||||
configurators.Bool(&AutoRotate, "IMGPROXY_AUTO_ROTATE")
|
||||
configurators.Bool(&EnforceThumbnail, "IMGPROXY_ENFORCE_THUMBNAIL")
|
||||
configurators.Bool(&ReturnAttachment, "IMGPROXY_RETURN_ATTACHMENT")
|
||||
configurators.Bool(&SvgFixUnsupported, "IMGPROXY_SVG_FIX_UNSUPPORTED")
|
||||
|
||||
configurators.Bool(&EnableWebpDetection, "IMGPROXY_ENABLE_WEBP_DETECTION")
|
||||
configurators.Bool(&EnforceWebp, "IMGPROXY_ENFORCE_WEBP")
|
||||
|
||||
@@ -465,5 +465,6 @@ imgproxy can send logs to syslog, but this feature is disabled by default. To en
|
||||
* `IMGPROXY_AUTO_ROTATE`: when `true`, imgproxy will automatically rotate images based on the EXIF Orientation parameter (if available in the image meta data). The orientation tag will be removed from the image in all cases. Default: `true`
|
||||
* `IMGPROXY_ENFORCE_THUMBNAIL`: when `true` and the source image has an embedded thumbnail, imgproxy will always use the embedded thumbnail instead of the main image. Currently, only thumbnails embedded in `heic` and `avif` are supported. Default: `false`
|
||||
* `IMGPROXY_RETURN_ATTACHMENT`: when `true`, response header `Content-Disposition` will include `attachment`. Default: `false`
|
||||
* `IMGPROXY_SVG_FIX_UNSUPPORTED`: when `true`, imgproxy will try to replace SVG features unsupported by librsvg to minimize SVG rendering error. This config only takes effect on SVG rasterization. Default: `false`
|
||||
* `IMGPROXY_HEALTH_CHECK_MESSAGE`:  the content of the health check response. Default: `imgproxy is running`
|
||||
* `IMGPROXY_HEALTH_CHECK_PATH`: an additional path of the health check. Default: blank
|
||||
|
||||
@@ -383,6 +383,21 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
|
||||
))
|
||||
}
|
||||
|
||||
// We're going to rasterize SVG. Since librsvg lacks the support of some SVG
|
||||
// features, we're going to replace them to minimize rendering error
|
||||
if originData.Type == imagetype.SVG && config.SvgFixUnsupported {
|
||||
fixed, changed, svgErr := svg.FixUnsupported(originData)
|
||||
checkErr(ctx, "svg_processing", svgErr)
|
||||
|
||||
if changed {
|
||||
// Since we'll replace origin data, it's better to close it to return
|
||||
// it's buffer to the pool
|
||||
originData.Close()
|
||||
|
||||
originData = fixed
|
||||
}
|
||||
}
|
||||
|
||||
resultData, err := func() (*imagedata.ImageData, error) {
|
||||
defer metrics.StartProcessingSegment(ctx)()
|
||||
return processing.ProcessImage(ctx, originData, po)
|
||||
|
||||
130
svg/svg.go
130
svg/svg.go
@@ -2,15 +2,31 @@ package svg
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
nanoid "github.com/matoous/go-nanoid/v2"
|
||||
"github.com/tdewolff/parse/v2"
|
||||
"github.com/tdewolff/parse/v2/xml"
|
||||
|
||||
"github.com/imgproxy/imgproxy/v3/imagedata"
|
||||
)
|
||||
|
||||
var feDropShadowName = []byte("feDropShadow")
|
||||
|
||||
const feDropShadowTemplate = `
|
||||
<feMerge result="dsin-%[1]s"><feMergeNode %[3]s /></feMerge>
|
||||
<feGaussianBlur %[4]s />
|
||||
<feOffset %[5]s result="dsof-%[2]s" />
|
||||
<feFlood %[6]s />
|
||||
<feComposite in2="dsof-%[2]s" operator="in" />
|
||||
<feMerge %[7]s>
|
||||
<feMergeNode />
|
||||
<feMergeNode in="dsin-%[1]s" />
|
||||
</feMerge>
|
||||
`
|
||||
|
||||
func Satitize(data *imagedata.ImageData) (*imagedata.ImageData, error) {
|
||||
r := bytes.NewReader(data.Data)
|
||||
l := xml.NewLexer(parse.NewInput(r))
|
||||
@@ -63,3 +79,117 @@ func Satitize(data *imagedata.ImageData) (*imagedata.ImageData, error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func replaceDropShadowNode(l *xml.Lexer, buf *bytes.Buffer) error {
|
||||
inAttrs := new(bytes.Buffer)
|
||||
blurAttrs := new(bytes.Buffer)
|
||||
offsetAttrs := new(bytes.Buffer)
|
||||
floodAttrs := new(bytes.Buffer)
|
||||
finalAttrs := new(bytes.Buffer)
|
||||
|
||||
inID, _ := nanoid.New(8)
|
||||
offsetID, _ := nanoid.New(8)
|
||||
|
||||
hasStdDeviation := false
|
||||
hasDx := false
|
||||
hasDy := false
|
||||
|
||||
TOKEN_LOOP:
|
||||
for {
|
||||
tt, tdata := l.Next()
|
||||
|
||||
switch tt {
|
||||
case xml.ErrorToken:
|
||||
if l.Err() != io.EOF {
|
||||
return l.Err()
|
||||
}
|
||||
break TOKEN_LOOP
|
||||
case xml.EndTagToken, xml.StartTagCloseVoidToken:
|
||||
break TOKEN_LOOP
|
||||
case xml.AttributeToken:
|
||||
switch strings.ToLower(string(l.Text())) {
|
||||
case "in":
|
||||
inAttrs.Write(tdata)
|
||||
case "stddeviation":
|
||||
blurAttrs.Write(tdata)
|
||||
hasStdDeviation = true
|
||||
case "dx":
|
||||
offsetAttrs.Write(tdata)
|
||||
hasDx = true
|
||||
case "dy":
|
||||
offsetAttrs.Write(tdata)
|
||||
hasDy = true
|
||||
case "flood-color", "flood-opacity":
|
||||
floodAttrs.Write(tdata)
|
||||
default:
|
||||
finalAttrs.Write(tdata)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !hasStdDeviation {
|
||||
blurAttrs.WriteString(` stdDeviation="2"`)
|
||||
}
|
||||
|
||||
if !hasDx {
|
||||
offsetAttrs.WriteString(` dx="2"`)
|
||||
}
|
||||
|
||||
if !hasDy {
|
||||
offsetAttrs.WriteString(` dy="2"`)
|
||||
}
|
||||
|
||||
fmt.Fprintf(
|
||||
buf, feDropShadowTemplate,
|
||||
inID, offsetID,
|
||||
inAttrs.String(),
|
||||
blurAttrs.String(),
|
||||
offsetAttrs.String(),
|
||||
floodAttrs.String(),
|
||||
finalAttrs.String(),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func FixUnsupported(data *imagedata.ImageData) (*imagedata.ImageData, bool, error) {
|
||||
if !bytes.Contains(data.Data, feDropShadowName) {
|
||||
return data, false, nil
|
||||
}
|
||||
|
||||
r := bytes.NewReader(data.Data)
|
||||
l := xml.NewLexer(parse.NewInput(r))
|
||||
|
||||
buf, cancel := imagedata.BorrowBuffer()
|
||||
|
||||
for {
|
||||
tt, tdata := l.Next()
|
||||
|
||||
switch tt {
|
||||
case xml.ErrorToken:
|
||||
if l.Err() != io.EOF {
|
||||
cancel()
|
||||
return nil, false, l.Err()
|
||||
}
|
||||
|
||||
newData := imagedata.ImageData{
|
||||
Data: buf.Bytes(),
|
||||
Type: data.Type,
|
||||
}
|
||||
newData.SetCancel(cancel)
|
||||
|
||||
return &newData, true, nil
|
||||
case xml.StartTagToken:
|
||||
if bytes.Equal(l.Text(), feDropShadowName) {
|
||||
if err := replaceDropShadowNode(l, buf); err != nil {
|
||||
cancel()
|
||||
return nil, false, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
buf.Write(tdata)
|
||||
default:
|
||||
buf.Write(tdata)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user