Browse Source

Fix feDropShadow SVG filter when IMGPROXY_SVG_FIX_UNSUPPORTED is true

DarthSim 2 years ago
parent
commit
4ab415fd9b
5 changed files with 151 additions and 0 deletions
  1. 2 0
      CHANGELOG.md
  2. 3 0
      config/config.go
  3. 1 0
      docs/configuration.md
  4. 15 0
      processing_handler.go
  5. 130 0
      svg/svg.go

+ 2 - 0
CHANGELOG.md

@@ -1,6 +1,8 @@
 # Changelog
 
 ## [Unreleased]
+### Add
+- Add `IMGPROXY_SVG_FIX_UNSUPPORTED` config.
 
 ## [3.8.0] - 2022-10-06
 ### Add

+ 3 - 0
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")

+ 1 - 0
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`: ![pro](/assets/pro.svg) the content of the health check response. Default: `imgproxy is running`
 * `IMGPROXY_HEALTH_CHECK_PATH`: an additional path of the health check. Default: blank

+ 15 - 0
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)

+ 130 - 0
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)
+		}
+	}
+}