A Go package for progressive JPEG encoding with custom scan script support (and default scan scripts, of course).
Why? You like JPEG (webp is too recent for you), but sometimes the network is slow and you prefer a fast, blurry preview to nothing. Encode images like it's 1999 !!1!
This package includes large portions of source code derived and/or copied from the Go standard library (image/jpeg), licensed under the BSD 3-Clause License.
Spectral selection is implemented (going from low frequencies to high frequencies), but not successive approximations (going from most significant bits to least significant bits).
This development started around 2018-2019, 95% of the work was done, it was relatively easy (the hard work had been done by the Go team already, it was just a matter of reading/deciphering JPEG/JFIF reference material and stdlib code ;) but I was stuck for a long time on some bugs, that Claude Sonnet 4 corrected (I'm so grateful!):
- an off-by-one error in the spectral loop in writePartialBlock (zig <= se)
- missing flush & reset of the bit buffer at the end of a progressive scan, in writeProgressiveSOS
Additionally, AI did a refactoring (dedup) of writeSOS/writePartialSOS, and the custom scan script support (I was going to release with default scripts only, but it wasn't complicated, just tedious, to support custom scripts), including most of the documentation.
package main
import (
"os"
"image/png"
"github.com/dlecorfec/progjpeg"
)
func main() {
// Read input image
file, _ := os.Open("input.png")
defer file.Close()
img, _ := png.Decode(file)
// Create output file
output, _ := os.Create("output.jpg")
defer output.Close()
// Encode as progressive JPEG
err := progjpeg.Encode(output, img, &progjpeg.Options{
Quality: 80,
Progressive: true,
})
if err != nil {
panic(err)
}
}Progressive JPEG encoding allows images to be displayed incrementally as they load. The custom scan script feature lets you control exactly how this progressive loading happens by specifying which DCT coefficients are encoded in each scan.
DCT coefficients go from 0 to 63, 0 being the lowest frequency and 63 the highest. Coefficient 0 is called DC, the others are called AC.
// Use default progressive encoding
err := progjpeg.Encode(w, img, &progjpeg.Options{
Quality: 80,
Progressive: true,
})
// Use a predefined scan script
err := progjpeg.Encode(w, img, &progjpeg.Options{
Quality: 80,
Progressive: true,
ScanScript: progjpeg.DefaultColorScanScript(),
})
// Use a completely custom scan script
customScript := progjpeg.ScanScript{
{Component: -1, SpectralStart: 0, SpectralEnd: 0}, // DC scan
{Component: 0, SpectralStart: 1, SpectralEnd: 5}, // Y low AC
{Component: 1, SpectralStart: 1, SpectralEnd: 5}, // Cb low AC
{Component: 2, SpectralStart: 1, SpectralEnd: 5}, // Cr low AC
{Component: 0, SpectralStart: 6, SpectralEnd: 63}, // Y high AC
{Component: 1, SpectralStart: 6, SpectralEnd: 63}, // Cb high AC
{Component: 2, SpectralStart: 6, SpectralEnd: 63}, // Cr high AC
}
err := progjpeg.Encode(w, img, &progjpeg.Options{
Quality: 80,
Progressive: true,
ScanScript: customScript,
})Each ProgressiveScan in a ScanScript has these fields:
-1: All components (only valid for DC scans)0: Y (luminance) component1: Cb (blue chrominance) component2: Cr (red chrominance) component
- Range:
0-63(DCT coefficient indices in zigzag order) 0,0: DC coefficient only1,5: Low frequency AC coefficients6,63: High frequency AC coefficients1,63: All AC coefficients
- DC scan
- Low frequency AC (1-9)
- High frequency AC (10-63)
- DC scan for all components
- Very low frequency AC for Y only (1-2)
- Slightly more Y detail (3-9)
- Add color information (Cb, Cr low frequencies)
- Complete remaining frequencies
Scan scripts are validated to ensure they produce valid JPEG files:
- Component ranges: Must be -1 to (nComponent-1)
- Spectral ranges: 0-63, SpectralEnd >= SpectralStart
- DC scan constraints: Component -1 only valid for SpectralStart=SpectralEnd=0
- AC scan constraints: Component -1 not allowed for AC scans
Invalid scan scripts automatically fall back to default scripts.
- DC first: Always start with DC coefficients for fastest preview
- Y before Cb/Cr: Luminance is more visually important than chrominance
- Low frequencies first: Low frequency AC coefficients contribute more to perceived image quality
- Fewer scans: Faster encoding, less progressive benefit
- More scans: Better progressive loading, slower encoding
- Component separation: Better progressive loading, more overhead
Fast Preview: Prioritize getting any recognizable image quickly
ScanScript{
{Component: -1, SpectralStart: 0, SpectralEnd: 0}, // DC all
{Component: 0, SpectralStart: 1, SpectralEnd: 2}, // Y minimal AC
{Component: 0, SpectralStart: 3, SpectralEnd: 63}, // Y remaining
{Component: 1, SpectralStart: 1, SpectralEnd: 63}, // Cb all AC
{Component: 2, SpectralStart: 1, SpectralEnd: 63}, // Cr all AC
}High Quality Progressive: Maximum progressive steps for smooth loading
ScanScript{
{Component: -1, SpectralStart: 0, SpectralEnd: 0}, // DC all
{Component: 0, SpectralStart: 1, SpectralEnd: 1}, // Y AC 1
{Component: 0, SpectralStart: 2, SpectralEnd: 3}, // Y AC 2-3
{Component: 0, SpectralStart: 4, SpectralEnd: 7}, // Y AC 4-7
{Component: 1, SpectralStart: 1, SpectralEnd: 3}, // Cb AC 1-3
{Component: 2, SpectralStart: 1, SpectralEnd: 3}, // Cr AC 1-3
{Component: 0, SpectralStart: 8, SpectralEnd: 63}, // Y AC remaining
{Component: 1, SpectralStart: 4, SpectralEnd: 63}, // Cb AC remaining
{Component: 2, SpectralStart: 4, SpectralEnd: 63}, // Cr AC remaining
}