1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
package main

import (
	"bytes"
	"fmt"
	"image"
	"image/jpeg"
	"image/png"
	"os/exec"
	"strconv"

	xdraw "golang.org/x/image/draw"
	_ "golang.org/x/image/webp" // decode-only; see encodeWebP for why
)

// The image pipeline
// ============================================================================
//
// Accepts PNG, JPEG and WebP; always serves WebP.
//
// Geometry (EXIF rotation and resizing) is done in Go, and cwebp is used purely
// as an encoder. The alternative — handing the raw upload to `cwebp -resize` —
// would be one process instead of three, but it cannot bake in EXIF rotation
// (cwebp has -crop but no -rotate), and it would leave us guessing the output
// dimensions we need for the <img width height> attributes. Doing the geometry
// here gives one code path regardless of input format and exact known sizes.
//
// The handoff to cwebp is a PAM (P7) blob on stdin: a 60-byte header followed
// by raw RGBA, which is exactly the memory layout of an image.NRGBA. That
// avoids the PNG compress/decompress round-trip that would otherwise sit
// between us and the encoder for no benefit.

// Target widths for the stored derivatives. An image is never upscaled: a
// 500px-wide screenshot yields a single 500px rendition rather than two blurry
// enlargements.
const (
	widthLarge = 1600
	widthSmall = 800

	// webpQuality is cwebp's lossy quality factor. 82 is slightly above the
	// default 75, chosen because these are illustrations in a blog post where
	// visible artefacts matter more than the last few kilobytes.
	webpQuality = "82"
)

// processedImage is the result of preparing an upload for storage.
type processedImage struct {
	// Original bytes exactly as uploaded, and the dimensions *after* EXIF
	// rotation, which is what the rendered <img> must advertise.
	Width, Height int
	Renditions    []Rendition
}

// processImage decodes an upload, applies EXIF orientation, and encodes the
// WebP derivatives.
func processImage(data []byte, mime string) (*processedImage, error) {
	src, _, err := image.Decode(bytes.NewReader(data))
	if err != nil {
		return nil, fmt.Errorf("decode image: %w", err)
	}
	if mime == "image/jpeg" {
		src = applyOrientation(src, jpegOrientation(data))
	}
	rgba := toNRGBA(src)
	w := rgba.Bounds().Dx()
	h := rgba.Bounds().Dy()
	if w == 0 || h == 0 {
		return nil, fmt.Errorf("image has zero dimension (%dx%d)", w, h)
	}

	out := &processedImage{Width: w, Height: h}
	for _, spec := range []struct {
		variant string
		target  int
	}{
		{VariantWebP1600, widthLarge},
		{VariantWebP800, widthSmall},
	} {
		scaled := resizeToWidth(rgba, spec.target)
		enc, err := encodeWebP(scaled)
		if err != nil {
			return nil, fmt.Errorf("encode %s: %w", spec.variant, err)
		}
		out.Renditions = append(out.Renditions, Rendition{
			Variant: spec.variant,
			MIME:    "image/webp",
			Width:   scaled.Bounds().Dx(),
			Height:  scaled.Bounds().Dy(),
			Bytes:   enc,
		})
	}
	return out, nil
}

// resizeToWidth scales an image down to the given width, preserving aspect
// ratio. Images already at or below the target are returned untouched, since
// upscaling only adds bytes and blur.
//
// CatmullRom is the sharpest of the resamplers in x/image/draw and the slowest;
// resizing happens once per upload, so quality is the right thing to buy.
func resizeToWidth(src *image.NRGBA, width int) *image.NRGBA {
	sw := src.Bounds().Dx()
	sh := src.Bounds().Dy()
	if sw <= width {
		return src
	}
	// Round to nearest rather than truncating, so a 1601x901 source does not
	// lose a row it did not have to.
	height := (sh*width + sw/2) / sw
	if height < 1 {
		height = 1
	}
	dst := image.NewNRGBA(image.Rect(0, 0, width, height))
	xdraw.CatmullRom.Scale(dst, dst.Bounds(), src, src.Bounds(), xdraw.Src, nil)
	return dst
}

// encodeWebP encodes an image to lossy WebP by piping PAM into cwebp.
//
// A subprocess rather than a library because there is no WebP *encoder* in the
// Go standard library or in golang.org/x/image (x/image/webp decodes only), and
// the pure-Go third-party encoders are lossless-only — which for photographs
// produces files several times larger than the JPEG they came from. cwebp is
// libwebp's reference encoder; the build wraps the binary with libwebp on PATH
// so it is guaranteed present rather than assumed.
func encodeWebP(img *image.NRGBA) ([]byte, error) {
	var in bytes.Buffer
	if err := writePAM(&in, img); err != nil {
		return nil, err
	}
	// "--" then "-" forces cwebp to read the input from stdin, and "-o -"
	// writes the result to stdout, so no temporary files are involved.
	cmd := exec.Command("cwebp", "-quiet", "-q", webpQuality, "-o", "-", "--", "-")
	cmd.Stdin = &in
	var out, errBuf bytes.Buffer
	cmd.Stdout = &out
	cmd.Stderr = &errBuf
	if err := cmd.Run(); err != nil {
		return nil, fmt.Errorf("cwebp: %w: %s", err, errBuf.String())
	}
	if out.Len() == 0 {
		return nil, fmt.Errorf("cwebp produced no output: %s", errBuf.String())
	}
	return out.Bytes(), nil
}

// writePAM writes an NRGBA image as a binary PAM (Netpbm P7) stream.
//
// PAM is the cheapest interchange format cwebp accepts: a short textual header
// followed by uncompressed RGBA rows, which for an NRGBA image is a header plus
// one copy of the pixel buffer.
func writePAM(buf *bytes.Buffer, img *image.NRGBA) error {
	w := img.Bounds().Dx()
	h := img.Bounds().Dy()
	buf.WriteString("P7\n")
	buf.WriteString("WIDTH " + strconv.Itoa(w) + "\n")
	buf.WriteString("HEIGHT " + strconv.Itoa(h) + "\n")
	buf.WriteString("DEPTH 4\n")
	buf.WriteString("MAXVAL 255\n")
	buf.WriteString("TUPLTYPE RGB_ALPHA\n")
	buf.WriteString("ENDHDR\n")
	// img.Pix may have a stride wider than the row, so rows are written
	// individually rather than dumping the whole buffer.
	for y := 0; y < h; y++ {
		start := y * img.Stride
		buf.Write(img.Pix[start : start+w*4])
	}
	return nil
}

// ensure the PNG and JPEG decoders are registered with image.Decode. They are
// referenced here rather than imported for side effects only, so that a future
// edit removing this cannot silently break format detection.
var _ = png.Decode
var _ = jpeg.Decode