diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8630dac4..99e166aa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,8 @@
# Changelog
## [Unreleased]
+### Add
+- Add `IMGPROXY_SVG_FIX_UNSUPPORTED` config.
## [3.8.0] - 2022-10-06
### Add
diff --git a/config/config.go b/config/config.go
index d5850a7d..1be76014 100644
--- a/config/config.go
+++ b/config/config.go
@@ -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")
diff --git a/docs/configuration.md b/docs/configuration.md
index 087b5938..8bf4b753 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -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
diff --git a/processing_handler.go b/processing_handler.go
index fa0c5221..67625b28 100644
--- a/processing_handler.go
+++ b/processing_handler.go
@@ -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)
diff --git a/svg/svg.go b/svg/svg.go
index a9a02aa3..f36fa3a4 100644
--- a/svg/svg.go
+++ b/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 = `
+
+
+
+
+
+
+
+
+
+`
+
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)
+ }
+ }
+}