Skip to content

Commit 542dd15

Browse files
committed
add dithering option and integrate pushd-dither tool
1 parent 309263e commit 542dd15

File tree

6 files changed

+148
-3
lines changed

6 files changed

+148
-3
lines changed

.gitmodules

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[submodule "plugins/pushd-dither"]
2+
path = plugins/pushd-dither
3+
url = git@github.com:pushd/pushd-dither.git

docker/Dockerfile

+21
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,27 @@ RUN apt-get update \
3131
&& ln -s /usr/lib/$(uname -m)-linux-gnu/libtcmalloc_minimal.so.4 /usr/local/lib/libtcmalloc_minimal.so \
3232
&& rm -rf /var/lib/apt/lists/*
3333

34+
# ====== PUSHD DITHER ========
35+
# This is a temporary solution to install pushd-dither
36+
RUN apt-get update \
37+
&& apt-get install -y --no-install-recommends \
38+
python3 \
39+
python3-pip \
40+
python3-dev \
41+
gfortran \
42+
libopencv-dev \
43+
curl
44+
45+
RUN pip3 install scikit-image opencv-python numpy Pillow --break-system-packages
46+
47+
COPY plugins/pushd-dither /opt/pushd-dither/
48+
49+
RUN cd /opt/pushd-dither \
50+
&& python3 -m pip install -e . --break-system-packages --target=/usr/local/lib/python3.11/
51+
52+
# ====== END PUSHD DITHER ========
53+
54+
3455
COPY docker/entrypoint.sh /usr/local/bin/
3556
COPY --from=build /usr/local/bin/imgproxy /usr/local/bin/
3657
COPY --from=build /usr/local/lib /usr/local/lib

options/processing_options.go

+30-3
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,18 @@ type TrimOptions struct {
5252
EqualVer bool
5353
}
5454

55-
type backgroundOptions struct {
55+
type BackgroundOptions struct {
5656
Color vips.Color
5757
Effect string
5858
}
5959

60+
type DitherType string
61+
62+
const (
63+
DitherNone DitherType = "none"
64+
DitherBNFS DitherType = "bnfs" // Blue noise, Floyd-Steinberg
65+
)
66+
6067
type WatermarkOptions struct {
6168
Enabled bool
6269
Opacity float64
@@ -87,10 +94,11 @@ type ProcessingOptions struct {
8794
FormatQuality map[imagetype.Type]int
8895
MaxBytes int
8996
Flatten bool
90-
Background backgroundOptions
97+
Background BackgroundOptions
9198
Blur float32
9299
Sharpen float32
93100
Pixelate int
101+
Dither DitherType
94102
StripMetadata bool
95103
KeepCopyright bool
96104
StripColorProfile bool
@@ -139,10 +147,11 @@ func NewProcessingOptions() *ProcessingOptions {
139147
Quality: 0,
140148
MaxBytes: 0,
141149
Format: imagetype.Unknown,
142-
Background: backgroundOptions{Color: vips.Color{R: 255, G: 255, B: 255}},
150+
Background: BackgroundOptions{Color: vips.Color{R: 255, G: 255, B: 255}},
143151
Blur: 0,
144152
Sharpen: 0,
145153
Dpr: 1,
154+
Dither: DitherNone,
146155
Watermark: WatermarkOptions{Opacity: 1, Replicate: false, Gravity: GravityOptions{Type: GravityCenter}},
147156
StripMetadata: config.StripMetadata,
148157
KeepCopyright: config.KeepCopyright,
@@ -700,6 +709,22 @@ func applyPixelateOption(po *ProcessingOptions, args []string) error {
700709
return nil
701710
}
702711

712+
func applyDitherOption(po *ProcessingOptions, args []string) error {
713+
if len(args) > 1 {
714+
return fmt.Errorf("Invalid dither arguments: %v", args)
715+
}
716+
717+
switch args[0] {
718+
case "bnfs":
719+
po.Dither = DitherBNFS
720+
case "none":
721+
po.Dither = DitherNone
722+
default:
723+
return fmt.Errorf("Invalid dither: %s", args[0])
724+
}
725+
return nil
726+
}
727+
703728
func applyPresetOption(po *ProcessingOptions, args []string) error {
704729
for _, preset := range args {
705730
if p, ok := presets[preset]; ok {
@@ -1033,6 +1058,8 @@ func applyURLOption(po *ProcessingOptions, name string, args []string) error {
10331058
return applySharpenOption(po, args)
10341059
case "pixelate", "pix":
10351060
return applyPixelateOption(po, args)
1061+
case "dither", "dit":
1062+
return applyDitherOption(po, args)
10361063
case "watermark", "wm":
10371064
return applyWatermarkOption(po, args)
10381065
case "strip_metadata", "sm":

plugins/pushd-dither

Submodule pushd-dither added at fb56297

processing/dither.go

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package processing
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"os/exec"
7+
8+
"github.com/imgproxy/imgproxy/v3/imagedata"
9+
"github.com/imgproxy/imgproxy/v3/imagetype"
10+
"github.com/imgproxy/imgproxy/v3/options"
11+
"github.com/imgproxy/imgproxy/v3/security"
12+
"github.com/imgproxy/imgproxy/v3/vips"
13+
log "github.com/sirupsen/logrus"
14+
)
15+
16+
func dither(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
17+
if po.Dither != options.DitherBNFS {
18+
return nil
19+
}
20+
21+
// Get a snapshot of current image
22+
if err := img.CopyMemory(); err != nil {
23+
return err
24+
}
25+
26+
// write to temp file
27+
f, err := os.CreateTemp("", "dither*.png")
28+
if err != nil {
29+
return err
30+
}
31+
32+
// clean up temp file with error logging
33+
defer func(name string) {
34+
if err := os.Remove(name); err != nil {
35+
log.Errorf("failed to remove temp file: %s", err)
36+
}
37+
}(f.Name())
38+
39+
pngData, err := img.Save(imagetype.PNG, 0)
40+
if err != nil {
41+
return err
42+
}
43+
defer pngData.Close()
44+
45+
if err := os.WriteFile(f.Name(), pngData.Data, 0644); err != nil {
46+
return err
47+
}
48+
49+
// the dirty business - will clobber the file
50+
if err := shellOutDither(f.Name()); err != nil {
51+
return err
52+
}
53+
54+
// read from dithered file
55+
ditheredData, err := imagedata.FromFile(f.Name(), "dithered image", security.DefaultOptions())
56+
if err != nil {
57+
return err
58+
}
59+
60+
ditheredImg := new(vips.Image)
61+
if err := ditheredImg.Load(ditheredData, 1, 1.0, 1); err != nil {
62+
return err
63+
}
64+
65+
defer ditheredImg.Clear()
66+
defer ditheredData.Close()
67+
68+
// always use png for output
69+
po.Format = imagetype.PNG
70+
71+
// replace original image
72+
// FIXME: use copy? embed image is a bit of a hack on a hack to not have to manage the png data lifecycle
73+
return img.EmbedImage(0, 0, ditheredImg)
74+
}
75+
76+
func shellOutDither(inFile string) error {
77+
// installed via Dockerfile in /opt/pushd-dither
78+
outFile := fmt.Sprintf("%s-dithered-tmp.png", inFile)
79+
cmd := exec.Command("python3",
80+
"test.py",
81+
"--pal-meter-13",
82+
"--image-in", inFile,
83+
"--image-out", outFile)
84+
cmd.Dir = "/opt/pushd-dither"
85+
output, err := cmd.CombinedOutput()
86+
if err != nil {
87+
return fmt.Errorf("dither failed: %s: %s", err, output)
88+
}
89+
90+
// clobber the original file
91+
return os.Rename(outFile, inFile)
92+
}

processing/processing.go

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ var mainPipeline = pipeline{
3535
fixSize,
3636
flatten,
3737
watermark,
38+
dither,
3839
exportColorProfile,
3940
stripMetadata,
4041
}

0 commit comments

Comments
 (0)