diff --git a/Dockerfile b/Dockerfile index b76df5b..915ea62 100644 --- a/Dockerfile +++ b/Dockerfile @@ -109,6 +109,10 @@ RUN mkdir -p /go/src/github.com/Pixboost/ WORKDIR /go/src/github.com/Pixboost/ RUN git clone https://github.com/Pixboost/transformimgs.git +WORKDIR /go/src/github.com/Pixboost/transformimgs/illustration + +RUN go install + WORKDIR /go/src/github.com/Pixboost/transformimgs/cmd RUN go build -o /transformimgs diff --git a/Dockerfile.dev b/Dockerfile.dev index 18dfec5..b61bd18 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -105,8 +105,15 @@ ENV PATH $GOPATH/bin:$PATH RUN mkdir -p "$GOPATH/src" "$GOPATH/bin" && chmod -R 1777 "$GOPATH" WORKDIR $GOPATH + + ENV IM_HOME /usr/local/bin VOLUME /go/src/github.com/Pixboost/transformimgs/ WORKDIR /go/src/github.com/Pixboost/transformimgs/ + +RUN cd illustration && \ + go install && \ + cd ../ + ENTRYPOINT ["sh", "/go/src/github.com/Pixboost/transformimgs/run.sh"] diff --git a/illustration/go.mod b/illustration/go.mod new file mode 100644 index 0000000..2194ab0 --- /dev/null +++ b/illustration/go.mod @@ -0,0 +1,5 @@ +module github.com/Pixboost/illustration + +go 1.18 + +require gopkg.in/gographics/imagick.v3 v3.7.0 // indirect diff --git a/illustration/go.sum b/illustration/go.sum new file mode 100644 index 0000000..303e0f3 --- /dev/null +++ b/illustration/go.sum @@ -0,0 +1,2 @@ +gopkg.in/gographics/imagick.v3 v3.7.0 h1:w8iQa58ikuqjX4l2OVML3pgqFcDMD8ywXJ9/cXa33fk= +gopkg.in/gographics/imagick.v3 v3.7.0/go.mod h1:+Q9nyA2xRZXrDyTtJ/eko+8V/5E7bWYs08ndkZp8UmA= diff --git a/illustration/main.go b/illustration/main.go new file mode 100644 index 0000000..7440597 --- /dev/null +++ b/illustration/main.go @@ -0,0 +1,114 @@ +package main + +import ( + "fmt" + "gopkg.in/gographics/imagick.v3/imagick" + "io" + "log" + "math" + "os" + "sort" +) + +type colorSlice []*imagick.PixelWand + +func (c colorSlice) Len() int { return len(c) } +func (c colorSlice) Less(i, j int) bool { return c[i].GetColorCount() < c[j].GetColorCount() } +func (c colorSlice) Swap(i, j int) { c[i], c[j] = c[j], c[i] } + +func main() { + imgData, err := io.ReadAll(os.Stdin) + if err != nil { + log.Fatal(err) + } + + var ( + colors colorSlice + colorsCnt uint + ) + + mw := imagick.NewMagickWand() + + err = mw.ReadImageBlob(imgData) + if err != nil { + log.Fatal(err) + } + + if (mw.GetImageWidth() * mw.GetImageHeight()) > 500*500 { + aspectRatio := float32(mw.GetImageWidth()) / float32(mw.GetImageHeight()) + err = mw.ScaleImage(500, uint(500/aspectRatio)) + if err != nil { + log.Fatal(err) + } + } + + colorsCnt, colors = mw.GetImageHistogram() + if colorsCnt > 30000 { + fmt.Print(false) + return + } + + sort.Sort(sort.Reverse(colors)) + + var ( + colorIdx int + count uint + currColor *imagick.PixelWand + pixelsCount = uint(0) + totalPixelsCount = float32(mw.GetImageHeight() * mw.GetImageWidth()) + tenPercent = uint(totalPixelsCount * 0.1) + fiftyPercent = uint(totalPixelsCount * 0.5) + isBackground = false + lastBackgroundColor *imagick.PixelWand + colorsInBackground = uint(0) + pixelsInBackground = uint(0) + ) + + for colorIdx, currColor = range colors { + if pixelsCount > fiftyPercent { + break + } + + count = currColor.GetColorCount() + + switch { + case colorIdx == 0: + isBackground = true + lastBackgroundColor = currColor + pixelsInBackground += count + colorsInBackground++ + case isBackground: + // Comparing colors to find out if it's still background or not. + // This logic addresses backgrounds with more than one similar color. + alphaDiff := currColor.GetAlpha() - lastBackgroundColor.GetAlpha() + redDiff := currColor.GetRed() - lastBackgroundColor.GetRed() + greenDiff := currColor.GetGreen() - lastBackgroundColor.GetGreen() + blueDiff := currColor.GetBlue() - lastBackgroundColor.GetBlue() + distance := + math.Max(math.Pow(redDiff, 2), math.Pow(redDiff-alphaDiff, 2)) + + math.Max(math.Pow(greenDiff, 2), math.Pow(greenDiff-alphaDiff, 2)) + + math.Max(math.Pow(blueDiff, 2), math.Pow(blueDiff-alphaDiff, 2)) + if distance < 0.1 { + lastBackgroundColor = currColor + pixelsInBackground += count + colorsInBackground++ + } else { + isBackground = false + if pixelsInBackground < tenPercent { + pixelsCount = pixelsInBackground + colorsInBackground = 0 + pixelsInBackground = 0 + } else { + pixelsCount += count + fiftyPercent = uint((totalPixelsCount - float32(pixelsInBackground)) * 0.5) + } + } + default: + pixelsCount += count + } + } + + colorsCntIn50Pct := uint(colorIdx) - colorsInBackground + + fmt.Print(colorsCntIn50Pct < 10 || (float32(colorsCntIn50Pct)/float32(colorsCnt)) <= 0.02) +} diff --git a/img/processor/imagemagick.go b/img/processor/imagemagick.go index 97c9291..4d6e470 100644 --- a/img/processor/imagemagick.go +++ b/img/processor/imagemagick.go @@ -5,12 +5,8 @@ import ( "fmt" "github.com/Pixboost/transformimgs/v8/img" "github.com/Pixboost/transformimgs/v8/img/processor/internal" - "gopkg.in/gographics/imagick.v3/imagick" - "math" - "os" + "io" "os/exec" - "os/signal" - "sort" "strconv" ) @@ -70,20 +66,6 @@ const ( AvifMime = "image/avif" ) -func init() { - imagick.Initialize() - - // time resource limit is static and doesn't work with long-running processes, hence disabling it - imagick.SetResourceLimit(imagick.RESOURCE_TIME, 0) - - c := make(chan os.Signal, 1) - signal.Notify(c, os.Interrupt) - go func() { - <-c - imagick.Terminate() - }() -} - // NewImageMagick creates a new ImageMagick processor. It does require // ImageMagick binaries to be installed on the local machine. // @@ -286,6 +268,27 @@ func (p *ImageMagick) execImagemagick(in *bytes.Reader, args []string, imgId str return out.Bytes(), nil } +func (p *ImageMagick) execIllustration(in io.Reader) bool { + var out, cmderr bytes.Buffer + cmd := exec.Command("illustration") + + cmd.Stdin = in + cmd.Stdout = &out + cmd.Stderr = &cmderr + + if Debug { + img.Log.Printf("Running resize command, args '%v'\n", cmd.Args) + } + err := cmd.Run() + if err != nil { + img.Log.Printf("Error executing illustration command: %s\n", err.Error()) + img.Log.Printf("ERROR: %s\n", cmderr.String()) + return false + } + + return string(out.Bytes()) == "true" +} + func (p *ImageMagick) LoadImageInfo(src *img.Image) (*img.Info, error) { var out, cmderr bytes.Buffer imgId := src.Id @@ -328,12 +331,6 @@ func (p *ImageMagick) LoadImageInfo(src *img.Image) (*img.Info, error) { return imageInfo, nil } -type colorSlice []*imagick.PixelWand - -func (c colorSlice) Len() int { return len(c) } -func (c colorSlice) Less(i, j int) bool { return c[i].GetColorCount() < c[j].GetColorCount() } -func (c colorSlice) Swap(i, j int) { c[i], c[j] = c[j], c[i] } - // isIllustration returns true if image is cartoon like, including // icons, logos, illustrations. // @@ -354,94 +351,7 @@ func (p *ImageMagick) isIllustration(src *img.Image, info *img.Info) (bool, erro return false, nil } - var ( - colors colorSlice - colorsCnt uint - ) - - mw := imagick.NewMagickWand() - - err := mw.ReadImageBlob(src.Data) - if err != nil { - return false, err - } - - if (info.Width * info.Height) > 500*500 { - aspectRatio := float32(info.Width) / float32(info.Height) - err = mw.ScaleImage(500, uint(500/aspectRatio)) - if err != nil { - return false, err - } - } - - colorsCnt, colors = mw.GetImageHistogram() - if colorsCnt > 30000 { - return false, nil - } - - sort.Sort(sort.Reverse(colors)) - - var ( - colorIdx int - count uint - currColor *imagick.PixelWand - pixelsCount = uint(0) - totalPixelsCount = float32(mw.GetImageHeight() * mw.GetImageWidth()) - tenPercent = uint(totalPixelsCount * 0.1) - fiftyPercent = uint(totalPixelsCount * 0.5) - isBackground = false - lastBackgroundColor *imagick.PixelWand - colorsInBackground = uint(0) - pixelsInBackground = uint(0) - ) - - for colorIdx, currColor = range colors { - if pixelsCount > fiftyPercent { - break - } - - count = currColor.GetColorCount() - - switch { - case colorIdx == 0: - isBackground = true - lastBackgroundColor = currColor - pixelsInBackground += count - colorsInBackground++ - case isBackground: - // Comparing colors to find out if it's still background or not. - // This logic addresses backgrounds with more than one similar color. - alphaDiff := currColor.GetAlpha() - lastBackgroundColor.GetAlpha() - redDiff := currColor.GetRed() - lastBackgroundColor.GetRed() - greenDiff := currColor.GetGreen() - lastBackgroundColor.GetGreen() - blueDiff := currColor.GetBlue() - lastBackgroundColor.GetBlue() - distance := - math.Max(math.Pow(redDiff, 2), math.Pow(redDiff-alphaDiff, 2)) + - math.Max(math.Pow(greenDiff, 2), math.Pow(greenDiff-alphaDiff, 2)) + - math.Max(math.Pow(blueDiff, 2), math.Pow(blueDiff-alphaDiff, 2)) - if distance < 0.1 { - lastBackgroundColor = currColor - pixelsInBackground += count - colorsInBackground++ - } else { - isBackground = false - if pixelsInBackground < tenPercent { - pixelsCount = pixelsInBackground - colorsInBackground = 0 - pixelsInBackground = 0 - } else { - pixelsCount += count - fiftyPercent = uint((totalPixelsCount - float32(pixelsInBackground)) * 0.5) - } - } - default: - pixelsCount += count - } - } - - colorsCntIn50Pct := uint(colorIdx) - colorsInBackground - - return colorsCntIn50Pct < 10 || (float32(colorsCntIn50Pct)/float32(colorsCnt)) <= 0.02, nil + return p.execIllustration(bytes.NewBuffer(src.Data)), nil } func getOutputFormat(src *img.Info, target *img.Info, supportedFormats []string) (string, string) { diff --git a/perf-test-webp.jmx b/perf-test-webp.jmx index 3f064fa..cabd35e 100644 --- a/perf-test-webp.jmx +++ b/perf-test-webp.jmx @@ -1,5 +1,5 @@ - + @@ -11,52 +11,62 @@ - + + 50 + 1 + true continue - + 10 false - 50 - 1 - 1497554796000 - 1497554796000 - false - - - true - false - - - - + localhost 8080 /img + + + + - + + 6 + /img/http%3A%2F%2Fnginx/transformations/big-jpeg.jpg/fit?size=1900x1900 + true + GET + true false - + - /img/http%3A%2F%2Fnginx/transformations/big-jpeg.jpg/fit?size=1900x1900 - GET + + + + + + Accept + image/webp + + + + + + + 6 + /img/http%3A%2F%2Fnginx/is_illustration/illustration-2.png/optimise true - false + GET true - false - false - false - false - 6 - false - 0 + false + + + - + Accept @@ -66,7 +76,7 @@ - + false saveConfig @@ -102,7 +112,7 @@ ./requests.jmx - + false saveConfig