encode

package
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 30, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Example

Example demonstrates basic JPEG encoding using default settings.

package main

import (
	"fmt"
	"image"
	"image/color"

	"github.com/0verkilll/f5/encode"
)

// createExampleImage creates a simple test image for examples.
func createExampleImage(width, height int) *image.RGBA {
	img := image.NewRGBA(image.Rect(0, 0, width, height))
	for y := 0; y < height; y++ {
		for x := 0; x < width; x++ {
			img.Set(x, y, color.RGBA{
				R: uint8(x * 255 / width),
				G: uint8(y * 255 / height),
				B: 128,
				A: 255,
			})
		}
	}
	return img
}

func main() {
	// Create a simple test image
	img := createExampleImage(64, 64)

	// Encode using convenience function with quality 75
	data, err := encode.WeeksEncodeToBytes(img, 75)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	// The output is a valid JPEG
	fmt.Printf("Encoded %d bytes\n", len(data))
	fmt.Printf("Valid JPEG: %v\n", data[0] == 0xFF && data[1] == 0xD8)

}
Output:
Encoded 1028 bytes
Valid JPEG: true
Example (NewIJGQuantizer)

Example_newIJGQuantizer demonstrates creating a standalone quantizer.

package main

import (
	"fmt"

	"github.com/0verkilll/f5/encode"
)

func main() {
	// Create a quantizer with quality 50 (standard tables)
	q, err := encode.NewIJGQuantizer(50)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	// Get the luminance quantization table
	lumTable := q.GetQuantTable(true)

	// The DC coefficient quantization value (position 0)
	fmt.Printf("DC quant value: %d\n", lumTable[0])

	// Quality 50 produces the standard ITU-T T.81 table unchanged
	// The standard DC value is 16
	fmt.Printf("Is standard DC: %v\n", lumTable[0] == 16)

}
Output:
DC quant value: 16
Is standard DC: true
Example (NewWeeksEncoder)

Example_newWeeksEncoder demonstrates creating an encoder and encoding an image.

package main

import (
	"bytes"
	"fmt"
	"image"
	"image/color"

	"github.com/0verkilll/f5/encode"
)

// createExampleImage creates a simple test image for examples.
func createExampleImage(width, height int) *image.RGBA {
	img := image.NewRGBA(image.Rect(0, 0, width, height))
	for y := 0; y < height; y++ {
		for x := 0; x < width; x++ {
			img.Set(x, y, color.RGBA{
				R: uint8(x * 255 / width),
				G: uint8(y * 255 / height),
				B: 128,
				A: 255,
			})
		}
	}
	return img
}

func main() {
	var buf bytes.Buffer
	img := createExampleImage(32, 32)

	// Create encoder with quality 85
	enc, err := encode.NewWeeksEncoder(&buf, 85)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	// Encode the image
	if err := enc.Encode(img); err != nil {
		fmt.Println("Encode error:", err)
		return
	}

	fmt.Printf("Encoded successfully: %d bytes\n", buf.Len())

}
Output:
Encoded successfully: 815 bytes
Example (NewWeeksEncoderWithOptions)

Example_newWeeksEncoderWithOptions demonstrates using functional options. 4:4:4 is only available in standard mode (f5.jar is hardcoded for 4:2:0), so this example pairs WithSubsampling(444) with WithStandardMode().

package main

import (
	"bytes"
	"fmt"
	"image"
	"image/color"

	"github.com/0verkilll/jpeg"

	"github.com/0verkilll/f5/encode"
)

// createExampleImage creates a simple test image for examples.
func createExampleImage(width, height int) *image.RGBA {
	img := image.NewRGBA(image.Rect(0, 0, width, height))
	for y := 0; y < height; y++ {
		for x := 0; x < width; x++ {
			img.Set(x, y, color.RGBA{
				R: uint8(x * 255 / width),
				G: uint8(y * 255 / height),
				B: 128,
				A: 255,
			})
		}
	}
	return img
}

func main() {
	var buf bytes.Buffer
	img := createExampleImage(32, 32)

	// Create encoder with custom options:
	// - Quality 90
	// - Custom comment
	// - 4:4:4 subsampling (requires standard mode)
	enc, err := encode.NewWeeksEncoderWithOptions(&buf, 90,
		encode.WithComment("Custom F5 Encoder"),
		encode.WithSubsampling(jpeg.ChromaSubsampling444),
		encode.WithStandardMode(),
	)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	if err := enc.Encode(img); err != nil {
		fmt.Println("Encode error:", err)
		return
	}

	fmt.Printf("High-quality encode: %d bytes\n", buf.Len())

}
Output:
High-quality encode: 924 bytes
Example (SetSubsampling)

Example_setSubsampling demonstrates method chaining configuration. 4:2:2 is only available outside James-compatible mode (f5.jar itself is hardcoded for 4:2:0), so this example pairs SetSubsampling with WithStandardMode.

package main

import (
	"bytes"
	"fmt"
	"image"
	"image/color"

	"github.com/0verkilll/jpeg"

	"github.com/0verkilll/f5/encode"
)

// createExampleImage creates a simple test image for examples.
func createExampleImage(width, height int) *image.RGBA {
	img := image.NewRGBA(image.Rect(0, 0, width, height))
	for y := 0; y < height; y++ {
		for x := 0; x < width; x++ {
			img.Set(x, y, color.RGBA{
				R: uint8(x * 255 / width),
				G: uint8(y * 255 / height),
				B: 128,
				A: 255,
			})
		}
	}
	return img
}

func main() {
	var buf bytes.Buffer
	img := createExampleImage(32, 32)

	enc, err := encode.NewWeeksEncoderWithOptions(&buf, 75,
		encode.WithStandardMode(),
	)
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	enc.SetSubsampling(jpeg.ChromaSubsampling422).
		SetComment("Method chaining example")

	if err := enc.Encode(img); err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Printf("Encoded with 4:2:2: %d bytes\n", buf.Len())

}
Output:
Encoded with 4:2:2: 774 bytes
Example (WithQuantizer)

Example_withQuantizer demonstrates injecting a custom quantizer for testing. Note: Custom quantizers are only used in standard mode, not James-compatible mode.

package main

import (
	"bytes"
	"fmt"
	"image"
	"image/color"

	"github.com/0verkilll/f5/encode"
)

// createExampleImage creates a simple test image for examples.
func createExampleImage(width, height int) *image.RGBA {
	img := image.NewRGBA(image.Rect(0, 0, width, height))
	for y := 0; y < height; y++ {
		for x := 0; x < width; x++ {
			img.Set(x, y, color.RGBA{
				R: uint8(x * 255 / width),
				G: uint8(y * 255 / height),
				B: 128,
				A: 255,
			})
		}
	}
	return img
}

func main() {
	var buf bytes.Buffer
	img := createExampleImage(16, 16)

	// Create a mock quantizer for testing
	mockQuant := &encode.MockQuantizer{
		// Using all 1s produces minimal quantization (highest quality)
		LumTable:   [64]int{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
		ChromTable: [64]int{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
	}

	// Inject the mock quantizer with standard mode (required for custom quantizers)
	enc, err := encode.NewWeeksEncoderWithOptions(&buf, 75,
		encode.WithQuantizer(mockQuant),
		encode.WithStandardMode(),
	)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	if err := enc.Encode(img); err != nil {
		fmt.Println("Encode error:", err)
		return
	}

	// Verify the quantizer was called (6 blocks for 16x16 with 4:2:0)
	fmt.Printf("Quantizer called: %d times\n", mockQuant.QuantizeCalls)
	fmt.Printf("Encoded: %d bytes\n", buf.Len())

}
Output:
Quantizer called: 6 times
Encoded: 810 bytes
Example (WithSubsampling)

Example_withSubsampling demonstrates different chroma subsampling modes. Uses standard mode to produce Go-decodable output with proper subsampling.

package main

import (
	"bytes"
	"fmt"
	"image"
	"image/color"

	"github.com/0verkilll/jpeg"

	"github.com/0verkilll/f5/encode"
)

// createExampleImage creates a simple test image for examples.
func createExampleImage(width, height int) *image.RGBA {
	img := image.NewRGBA(image.Rect(0, 0, width, height))
	for y := 0; y < height; y++ {
		for x := 0; x < width; x++ {
			img.Set(x, y, color.RGBA{
				R: uint8(x * 255 / width),
				G: uint8(y * 255 / height),
				B: 128,
				A: 255,
			})
		}
	}
	return img
}

func main() {
	img := createExampleImage(32, 32)

	// Encode with 4:2:0 (default, smallest file) using standard mode
	var buf420 bytes.Buffer
	enc420, err := encode.NewWeeksEncoderWithOptions(&buf420, 75,
		encode.WithSubsampling(jpeg.ChromaSubsampling420),
		encode.WithStandardMode(),
	)
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	if err = enc420.Encode(img); err != nil {
		fmt.Println("error:", err)
		return
	}

	// Encode with 4:4:4 (largest file, highest quality) using standard mode
	var buf444 bytes.Buffer
	enc444, err := encode.NewWeeksEncoderWithOptions(&buf444, 75,
		encode.WithSubsampling(jpeg.ChromaSubsampling444),
		encode.WithStandardMode(),
	)
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	if err = enc444.Encode(img); err != nil {
		fmt.Println("error:", err)
		return
	}

	fmt.Printf("4:2:0 size: %d bytes\n", buf420.Len())
	fmt.Printf("4:4:4 size: %d bytes\n", buf444.Len())
	fmt.Printf("4:4:4 is larger: %v\n", buf444.Len() > buf420.Len())

}
Output:
4:2:0 size: 791 bytes
4:4:4 size: 856 bytes
4:4:4 is larger: true

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrInvalidICCProfile = errors.New("invalid ICC profile data")

ErrInvalidICCProfile is returned when ICC profile data is malformed.

View Source
var ErrInvalidJPEG = errors.New("invalid JPEG data")

ErrInvalidJPEG is returned when the source data is not valid JPEG.

View Source
var ErrNoEXIF = errors.New("no EXIF data found in source image")

ErrNoEXIF is returned when no EXIF data is found in the source image.

View Source
var ErrNoICC = errors.New("no ICC profile found in source image")

ErrNoICC is returned when no ICC profile is found in the source image.

Functions

func EmbedJPEGIdentical

func EmbedJPEGIdentical(img image.Image, quality int, password string, message []byte) ([]byte, error)

EmbedJPEGIdentical encodes img to a baseline JPEG and embeds message with the F5 algorithm, producing output that is byte-for-byte identical to the reference Westfeld f5.jar encoder for the same inputs.

The pipeline runs the James/Weeks encoder twice over the same pixels:

  1. Pass one taps every quantized 8x8 DCT block in MCU-interleaved encode order and concatenates the 64 zigzag-ordered coefficients of each block into a single flat slice. This is exactly the coefficient array f5.jar's modified JpegEncoder builds before embedding, so no de-zigzag or block re-ordering is needed here: embed.Embed maps each raw shuffled (raster) index to its zigzag slot internally.
  2. embed.Embed applies the F5 permutative-straddling matrix encoding in place, yielding f5.jar-identical modified coefficients.
  3. Pass two re-encodes the same pixels with an injection tap that overwrites each block with the modified coefficients, and captures the entropy-coded bytes.

Because both passes share the deterministic James-compatible encoder (4:2:0 chroma, canonical Huffman tables, the f5.jar JFIF/COM/DQT/SOF/DHT/SOS headers), the only image-dependent variation is in the coefficient data, which step two makes identical to f5.jar.

To reproduce f5.jar's reference output for verification, feed f5.jar a BMP written with WriteBMP24 from the same image.Image; identical RGB in both pipelines yields identical YCbCr, DCT, and quantized coefficients.

Parameters:

  • img: the cover image (any image.Image; converted to YCbCr 4:2:0)
  • quality: JPEG quality 1..100 (IJG scale, same as f5.jar's -q)
  • password: F5 PRNG seed (same as f5.jar's -p); must not be empty
  • message: the secret payload (max 8,388,607 bytes)

Returns the stego JPEG bytes, or an error if encoding or embedding fails (for example, the message exceeds the cover's F5 capacity).

func EmbedJPEGWithOptions

func EmbedJPEGWithOptions(img image.Image, quality int, password string, message []byte, opts ...Option) ([]byte, error)

EmbedJPEGWithOptions is EmbedJPEGIdentical with extra encoder options applied to the final (pass-two) encode. The F5 embedding into the DCT coefficients is unchanged — the options affect only how the output JPEG is framed. The intended use is WithPixelKnotHeaders (and/or WithoutAPP0 / WithoutComment) to produce PixelKnot-style stego JPEGs whose coefficients carry the same F5 payload as f5.jar but whose header omits the APP0 and COM markers. With no options it is byte-identical to EmbedJPEGIdentical.

Callers should not pass WithBlockTap here: pass two installs its own coefficient injection tap and an external tap would override it.

func EncodeBatch

func EncodeBatch(items []BatchItem, workers int) []error

EncodeBatch encodes many images concurrently across a pool of goroutines and returns a slice of errors parallel to items (errs[i] is the result for items[i], nil on success).

workers controls the pool size; a value <= 0 uses runtime.GOMAXPROCS(0). Each image is encoded on a single goroutine with intra-image parallelism disabled (so the cores are spent across images rather than oversubscribed within one image). This is the recommended path for high-throughput encoding of many images — it scales nearly linearly with core count.

Every item still produces byte-identical output to a standalone encode; only the wall-clock throughput changes. Items are independent, so one item's error does not stop the others.

Example:

items := make([]encode.BatchItem, len(images))
for i, img := range images {
    items[i] = encode.BatchItem{Image: img, Writer: outFiles[i], Quality: 75}
}
errs := encode.EncodeBatch(items, 0) // 0 = all cores
for i, err := range errs {
    if err != nil { log.Printf("image %d failed: %v", i, err) }
}

func GetJamesStyleHuffmanSpecs

func GetJamesStyleHuffmanSpecs() []jpeg.HuffmanSpec

GetJamesStyleHuffmanSpecs returns Huffman table specs in James Weeks order but with correct class/ID values for use with standard decoders.

The order is: DC_LUM, AC_LUM, DC_CHROM, AC_CHROM This matches the order written by writeJamesDHT.

func ParseEXIF

func ParseEXIF(r io.Reader) ([]byte, error)

ParseEXIF extracts EXIF data from a JPEG source.

It scans for the APP1 marker (0xFFE1) with EXIF signature ("Exif\x00\x00") and returns the raw EXIF segment data (excluding marker and length bytes but including the EXIF signature).

Returns ErrNoEXIF if no EXIF data is found. Returns ErrInvalidJPEG if the source is not valid JPEG data.

Example:

f, _ := os.Open("photo.jpg")
defer f.Close()
exifData, err := ParseEXIF(f)
if err == ErrNoEXIF {
    // No EXIF in source - this is normal
}

func ParseEXIFBytes

func ParseEXIFBytes(data []byte) ([]byte, error)

ParseEXIFBytes extracts EXIF data from JPEG bytes.

This is a convenience function that works with byte slices instead of readers. See ParseEXIF for full documentation.

func ParseICCProfile

func ParseICCProfile(r io.Reader) ([]byte, error)

ParseICCProfile extracts ICC profile data from a JPEG source.

It scans for APP2 markers (0xFFE2) with ICC_PROFILE signature and reassembles multi-segment profiles (profiles >64KB span multiple APP2 markers with sequence numbers).

Returns ErrNoICC if no ICC profile is found. Returns ErrInvalidJPEG if the source is not valid JPEG data. Returns ErrInvalidICCProfile if ICC segments are malformed or incomplete.

Example:

f, _ := os.Open("photo.jpg")
defer f.Close()
iccData, err := ParseICCProfile(f)
if err == ErrNoICC {
    // No ICC profile in source - this is normal
}

func ParseICCProfileBytes

func ParseICCProfileBytes(data []byte) ([]byte, error)

ParseICCProfileBytes extracts ICC profile data from JPEG bytes.

This is a convenience function that works with byte slices instead of readers. See ParseICCProfile for full documentation.

func WeeksEncodeToBytes

func WeeksEncodeToBytes(img image.Image, quality int) ([]byte, error)

WeeksEncodeToBytes encodes an image to JPEG bytes using the James R. Weeks encoder.

This is a convenience function that creates an encoder, encodes the image, and returns the result as a byte slice.

NOTE: This function uses James-compatible mode by default, which produces output that is byte-identical with the original James R. Weeks Java encoder but is NOT decodable by Go's standard image/jpeg decoder. Use WeeksEncodeToBytesStandard if you need Go-decodable output.

Parameters:

  • img: The image to encode
  • quality: Quality level from 1 to 100

Returns the encoded JPEG data or an error.

func WeeksEncodeToBytesStandard

func WeeksEncodeToBytesStandard(img image.Image, quality int) ([]byte, error)

WeeksEncodeToBytesStandard encodes an image to JPEG bytes in standard mode.

This is a convenience function that creates an encoder with WithStandardMode(), encodes the image, and returns the result as a byte slice.

Unlike WeeksEncodeToBytes, this function produces output that is decodable by Go's standard image/jpeg decoder and other standard JPEG decoders, but is NOT byte-identical with the original James R. Weeks Java encoder.

Parameters:

  • img: The image to encode
  • quality: Quality level from 1 to 100

Returns the encoded JPEG data or an error.

func WriteBMP24

func WriteBMP24(w io.Writer, img image.Image) error

WriteBMP24 writes img to w as a 24-bit, bottom-up, BITMAPINFOHEADER BMP in BGR pixel order with each row padded to a 4-byte boundary.

This is the exact shape net.f5.image.Bmp (the reference f5.jar's only BMP reader) accepts: a 24-bit true-colour file. macOS sips-produced BMPs use a different header/bit-depth and are rejected by that reader. Feeding f5.jar a BMP from WriteBMP24 and the same image.Image to EmbedJPEGIdentical guarantees both see identical RGB, hence identical YCbCr/DCT/coefficients.

Pixel colours are read via img.At and the standard color.RGBA conversion, taking the high byte of each 16-bit channel. The alpha channel is ignored (BMP is opaque).

Types

type BatchItem

type BatchItem struct {
	// Image is the source image to encode.
	Image image.Image
	// Writer receives the encoded JPEG bytes for this item.
	Writer io.Writer
	// Options are optional per-item encoder options (e.g. WithComment,
	// WithSubsampling). They are applied after the batch defaults, so a caller
	// can override anything — including re-enabling intra-image parallelism.
	Options []Option
	// Quality is the JPEG quality (1-100) for this item.
	Quality int
}

BatchItem describes one image to encode in EncodeBatch.

type BitWriter

type BitWriter interface {
	// WriteBits writes n bits from the least significant bits of the value.
	// The bits parameter contains the value, and nBits specifies how many
	// bits to write (1-32). Returns an error if the underlying write fails.
	WriteBits(bits uint32, nBits int) error

	// Flush writes any remaining bits, padding with 1-bits as per JPEG spec.
	// This should be called at the end of entropy-coded data to ensure
	// proper byte alignment.
	Flush() error
}

BitWriter abstracts bit-level writing for entropy coding.

This interface wraps the jpeg.EncoderBitWriter functionality, allowing injection of mock implementations for testing entropy encoding logic without writing actual bit streams.

The interface follows JPEG entropy coding requirements:

  • Bits are written most-significant bit first
  • Flush pads partial bytes with 1-bits per ITU-T T.81 spec

Example usage:

func encodeCoefficient(bw BitWriter, code uint16, size int) error {
    return bw.WriteBits(uint32(code), size)
}

type BitWriterAdapter

type BitWriterAdapter struct {
	// contains filtered or unexported fields
}

BitWriterAdapter wraps jpeg.EncoderBitWriter to satisfy the BitWriter interface.

func NewBitWriterAdapter

func NewBitWriterAdapter(bw *jpeg.EncoderBitWriter) *BitWriterAdapter

NewBitWriterAdapter creates a new BitWriterAdapter wrapping the given EncoderBitWriter.

func (*BitWriterAdapter) Flush

func (a *BitWriterAdapter) Flush() error

Flush writes any remaining bits, padding with 1-bits.

func (*BitWriterAdapter) WriteBits

func (a *BitWriterAdapter) WriteBits(bits uint32, nBits int) error

WriteBits writes n bits from the least significant bits of the value.

type BlockEncoder

type BlockEncoder interface {
	// EncodeBlock encodes a zigzag-ordered quantized block.
	// prevDC is the previous block's DC value for differential encoding.
	// isLuminance selects the appropriate Huffman tables:
	//   - true: luminance DC/AC tables
	//   - false: chrominance DC/AC tables
	// Returns the new DC value (block[0]) for use as prevDC in next call,
	// and any encoding error.
	EncodeBlock(block *[64]int, prevDC int, isLuminance bool) (newDC int, err error)

	// Flush finalizes the entropy-coded data.
	// Pads any partial byte with 1-bits per JPEG spec and writes to output.
	Flush() error
}

BlockEncoder performs entropy encoding of quantized DCT blocks.

Implementations handle the conversion of quantized coefficients into entropy-coded bit streams using Huffman coding, including:

  • DC differential encoding (each DC value encoded as difference from previous)
  • AC run-length encoding (zeros encoded as runs)
  • Special symbols: EOB (End of Block) and ZRL (Zero Run Length)

Per ITU-T T.81, the encoding process for each block:

  1. DC coefficient: encode difference from previous block's DC
  2. AC coefficients: run-length encode in zigzag order
  3. EOB marker: signal end of non-zero coefficients

Example usage:

encoder := NewHuffmanBlockEncoder(bitWriter, dcTable, acTable)
prevDC := 0
for _, block := range blocks {
    prevDC, _ = encoder.EncodeBlock(&block, prevDC, isLuminance)
}
encoder.Flush()

type BlockExtractor

type BlockExtractor interface {
	// ExtractBlock extracts an 8x8 block at the given position.
	// Parameters:
	//   - img: Source image (any image.Image implementation)
	//   - component: 0=Y (luminance), 1=Cb, 2=Cr
	//   - x, y: Top-left position of the block in image coordinates
	// Returns 64 float64 values in row-major order.
	// Values are NOT level-shifted (raw pixel values 0-255).
	ExtractBlock(img image.Image, component, x, y int) [64]float64
}

BlockExtractor extracts pixel blocks from images for DCT processing.

Implementations handle color space conversion and chroma subsampling, converting image pixels into 8x8 blocks of float64 values suitable for DCT transformation.

For YCbCr color space (standard JPEG):

  • Component 0: Y (luminance)
  • Component 1: Cb (blue-difference chroma)
  • Component 2: Cr (red-difference chroma)

Chroma subsampling modes affect how Cb/Cr blocks are extracted:

  • 4:4:4: Full resolution for all components
  • 4:2:2: Horizontal subsampling of chroma (2:1)
  • 4:2:0: Both horizontal and vertical subsampling (2:1 each)

Example usage:

extractor := NewYCbCrBlockExtractor(jpeg.ChromaSubsampling420)
yBlock := extractor.ExtractBlock(img, 0, blockX, blockY)  // luminance
cbBlock := extractor.ExtractBlock(img, 1, blockX, blockY) // chroma

type BlockTapFunc

type BlockTapFunc func(blockIndex int, isLuminance bool, block *[64]int)

BlockTapFunc is called for every zigzag-ordered quantized block immediately before it would be entropy-coded. The tap may mutate the block in place; the mutation is what gets passed to the inner encoder (if any).

blockIndex is a monotonically increasing counter starting at 0 across all components and MCUs in encode order. isLuminance is true for Y blocks and false for Cb/Cr.

Used by F5 steganalysis training pipelines to (a) extract cover-image quantized DCT coefficients without paying the entropy-coding cost, and (b) inject F5-modified coefficients in a second pass to produce a valid stego JPEG byte stream.

type CustomHuffmanTable

type CustomHuffmanTable struct {
	// contains filtered or unexported fields
}

CustomHuffmanTable represents a custom Huffman table for JPEG encoding. It stores the bits and values arrays as defined in ITU-T T.81, and implements the HuffmanTable interface for encoding.

func NewCustomHuffmanTable

func NewCustomHuffmanTable(tableClass, tableNum int, bits [16]byte, values []byte) (*CustomHuffmanTable, error)

NewCustomHuffmanTable creates a new custom Huffman table from bits and values.

Parameters:

  • tableClass: 0 for DC, 1 for AC
  • tableNum: 0 for luminance, 1 for chrominance
  • bits: Array of 16 bytes specifying code lengths (bits[i] = count of codes with length i+1)
  • values: Symbol values in order of increasing code length

Returns an error if validation fails:

  • tableClass must be 0 or 1
  • tableNum must be 0 or 1
  • values length must equal sum of bits array

func (*CustomHuffmanTable) Encode

func (t *CustomHuffmanTable) Encode(symbol byte) (code uint16, size uint8)

Encode returns the Huffman code and size for a given symbol. This implements the HuffmanTable interface.

func (*CustomHuffmanTable) GetBits

func (t *CustomHuffmanTable) GetBits() [16]byte

GetBits returns the bits array (counts of codes for each length).

func (*CustomHuffmanTable) GetEncoderTable

func (t *CustomHuffmanTable) GetEncoderTable() *jpeg.HuffmanEncoderTable

GetEncoderTable returns the underlying jpeg.HuffmanEncoderTable.

func (*CustomHuffmanTable) GetTableClass

func (t *CustomHuffmanTable) GetTableClass() int

GetTableClass returns the table class (0 for DC, 1 for AC).

func (*CustomHuffmanTable) GetTableNum

func (t *CustomHuffmanTable) GetTableNum() int

GetTableNum returns the table number (0 for luminance, 1 for chrominance).

func (*CustomHuffmanTable) GetValues

func (t *CustomHuffmanTable) GetValues() []byte

GetValues returns a copy of the values array.

type DCT

type DCT interface {
	// Forward performs forward DCT on an 8x8 block in-place.
	// Input values should be level-shifted (subtracted by 128 for 8-bit).
	// After transform, block[0] contains the DC coefficient and
	// blocks[1-63] contain AC coefficients.
	Forward(block *[64]float64)
}

DCT abstracts Discrete Cosine Transform operations for JPEG encoding.

This interface provides the forward DCT operation needed for JPEG encoding. It wraps the jpeg.DCTTransformer interface, allowing custom DCT implementations or mock transforms for testing block processing logic.

Per ITU-T T.81, the DCT transforms spatial domain pixels into frequency domain coefficients, with the DC coefficient at position 0 representing the average value and AC coefficients representing frequency components.

Example usage:

func processBlock(dct DCT, block *[64]float64) {
    // Level shift
    for i := range block {
        block[i] -= 128.0
    }
    // Forward DCT
    dct.Forward(block)
}

type DCTAdapter

type DCTAdapter struct {
	// contains filtered or unexported fields
}

DCTAdapter wraps jpeg.DCTTransformer to satisfy the DCT interface. The DCT interface only requires Forward, while jpeg.DCTTransformer has both Forward and Inverse. This adapter provides the encoding-focused interface.

func NewDCTAdapter

func NewDCTAdapter(dct jpeg.DCTTransformer) *DCTAdapter

NewDCTAdapter creates a new DCTAdapter wrapping the given DCTTransformer.

func (*DCTAdapter) Forward

func (a *DCTAdapter) Forward(block *[64]float64)

Forward performs forward DCT on an 8x8 block in-place.

type HuffmanBlockEncoder

type HuffmanBlockEncoder struct {
	// contains filtered or unexported fields
}

HuffmanBlockEncoder implements the BlockEncoder interface using Huffman coding.

This encoder handles the conversion of quantized DCT coefficients into entropy-coded bit streams, implementing:

  • DC differential encoding: Each DC value as difference from previous block
  • AC run-length encoding: Consecutive zeros encoded as runs
  • ZRL (Zero Run Length): Symbol 0xF0 for runs of 16 zeros
  • EOB (End of Block): Symbol 0x00 to signal remaining zeros

The encoder requires four Huffman tables for DC/AC luminance/chrominance.

func NewHuffmanBlockEncoder

func NewHuffmanBlockEncoder(bw BitWriter, dcLum, dcChrom, acLum, acChrom HuffmanTable) *HuffmanBlockEncoder

NewHuffmanBlockEncoder creates a new HuffmanBlockEncoder.

Parameters:

  • bw: BitWriter for writing entropy-coded bits
  • dcLum: Huffman table for luminance DC coefficients
  • dcChrom: Huffman table for chrominance DC coefficients
  • acLum: Huffman table for luminance AC coefficients
  • acChrom: Huffman table for chrominance AC coefficients

func (*HuffmanBlockEncoder) EncodeBlock

func (e *HuffmanBlockEncoder) EncodeBlock(block *[64]int, prevDC int, isLuminance bool) (newDC int, err error)

EncodeBlock encodes a zigzag-ordered quantized block using Huffman coding.

The encoding process follows ITU-T T.81 Section F.1.2:

  1. DC coefficient: Calculate diff from prevDC, encode category, write bits
  2. AC coefficients (1-63): Run-length encode in order
  3. Write ZRL (0xF0) for runs of 16 zeros
  4. Write EOB (0x00) when all remaining coefficients are zero

Returns the current block's DC value (block[0]) for use as prevDC in next call.

func (*HuffmanBlockEncoder) Flush

func (e *HuffmanBlockEncoder) Flush() error

Flush finalizes the entropy-coded data by flushing the underlying BitWriter. Per ITU-T T.81, this pads any partial byte with 1-bits.

type HuffmanTable

type HuffmanTable interface {
	// Encode returns the Huffman code and size for a given symbol.
	// For DC tables, symbol is the category (0-11).
	// For AC tables, symbol is the run/size byte (RRRRSSSS).
	// Returns the code bits and the number of bits in the code.
	Encode(symbol byte) (code uint16, size uint8)
}

HuffmanTable abstracts Huffman code lookup for JPEG encoding.

This interface wraps the jpeg.HuffmanEncoderTable functionality, allowing alternative Huffman table implementations and mock tables for testing.

In JPEG encoding, Huffman tables are used for:

  • DC coefficient categories (0-11 for differential DC values)
  • AC coefficient run/size symbols (RRRRSSSS format per ITU-T T.81)

Example usage:

func encodeDC(table HuffmanTable, bw BitWriter, category byte) error {
    code, size := table.Encode(category)
    return bw.WriteBits(uint32(code), int(size))
}

type HuffmanTableAdapter

type HuffmanTableAdapter struct {
	// contains filtered or unexported fields
}

HuffmanTableAdapter wraps jpeg.HuffmanEncoderTable to satisfy the HuffmanTable interface.

func NewHuffmanTableAdapter

func NewHuffmanTableAdapter(table *jpeg.HuffmanEncoderTable) *HuffmanTableAdapter

NewHuffmanTableAdapter creates a new HuffmanTableAdapter wrapping the given table.

func (*HuffmanTableAdapter) Encode

func (a *HuffmanTableAdapter) Encode(symbol byte) (code uint16, size uint8)

Encode returns the Huffman code and size for a given symbol.

type IJGQuantizer

type IJGQuantizer struct {
	// contains filtered or unexported fields
}

IJGQuantizer implements the Quantizer interface using IJG quality scaling.

The IJG quality formula scales the standard ITU-T T.81 quantization tables:

  • Quality < 50: scale = 5000 / quality (higher values = more compression)
  • Quality >= 50: scale = 200 - 2*quality (lower values = less compression)

Quality 50 produces the standard tables unchanged. Quality 100 produces the minimum quantization (highest quality), and quality 1 produces maximum quantization (lowest quality).

Example usage:

q, err := NewIJGQuantizer(75)
if err != nil {
    return err
}
quantized := q.QuantizeBlock(&dctBlock, true) // luminance
table := q.GetQuantTable(true)                // for DQT marker

func NewIJGQuantizer

func NewIJGQuantizer(quality int) (*IJGQuantizer, error)

NewIJGQuantizer creates a new IJGQuantizer with the specified quality.

Parameters:

  • quality: Quality level from 1 to 100 (same as libjpeg)
  • 1: Lowest quality, highest compression
  • 50: Standard quality (base tables unchanged)
  • 100: Highest quality, lowest compression

Returns an error if quality is outside the valid range [1, 100].

func (*IJGQuantizer) GetQuantTable

func (q *IJGQuantizer) GetQuantTable(isLuminance bool) [64]int

GetQuantTable returns the quantization table for the specified component.

Parameters:

  • isLuminance: true for luminance (Y) table, false for chrominance (Cb/Cr) table

Returns a copy of the 64-element quantization table. Values are in row-major order (not zigzag) and range from 1 to 255.

This method is used for:

  • Writing DQT (Define Quantization Table) markers
  • Debugging and inspection
  • Quality analysis

func (*IJGQuantizer) QuantizeBlock

func (q *IJGQuantizer) QuantizeBlock(block *[64]float64, isLuminance bool) [64]int

QuantizeBlock quantizes DCT coefficients in a block.

The quantization process divides each coefficient by its corresponding quantization table value and rounds to the nearest integer:

  • Positive values: int(value/qt + 0.5)
  • Negative values: int(value/qt - 0.5)

Parameters:

  • block: Pointer to 64 DCT coefficients (row-major, NOT zigzag order)
  • isLuminance: true for Y component, false for Cb/Cr components

Returns 64 quantized integer coefficients in the same order as input. The result is NOT zigzag reordered - that should be done separately.

type JamesBitWriter

type JamesBitWriter struct {
	// contains filtered or unexported fields
}

JamesBitWriter implements bit-level writing matching f5.jar's Huffman.java.

The Java encoder uses a specific bit buffer layout:

  • Bits are accumulated starting at bit position 23 and going downward.
  • Bytes are extracted from bits 16-23 (buffer >> 16).
  • This differs from typical implementations that extract from bits 24-31.

Two flush quirks of f5.jar that we replicate:

  • The final partial byte is padded with 0s (not 1s as in standard JPEG).
  • The final partial byte is NOT subject to 0xFF→0x00 byte stuffing (Huffman.flushBuffer in f5.jar omits the stuffing branch for the trailing-byte write).

func NewJamesBitWriter

func NewJamesBitWriter(w io.Writer) *JamesBitWriter

NewJamesBitWriter creates a new JamesBitWriter.

func (*JamesBitWriter) Flush

func (bw *JamesBitWriter) Flush() error

Flush writes any remaining bits. This matches Java's Huffman.flushBuffer which does NOT pad with 1s.

Java's flushBuffer:

while (putBits >= 8) { write byte from bits 16-23; shift << 8; putBits -= 8; }
if (putBits > 0) { write byte from bits 16-23; }

func (*JamesBitWriter) WriteBits

func (bw *JamesBitWriter) WriteBits(bits uint32, nBits int) error

WriteBits writes n bits matching Java's Huffman.bufferIt exactly. This is a direct port of the Java algorithm to ensure byte-identical output.

type JamesBlockExtractor

type JamesBlockExtractor struct {
	// Pre-converted YCbCr arrays (matching Java's JpegInfo layout)
	Y  [][]float32
	Cb [][]float32
	Cr [][]float32
	// contains filtered or unexported fields
}

JamesBlockExtractor implements the BlockExtractor interface matching the exact behavior of JpegInfo.java from the James R. Weeks encoder.

Key differences from the standard Go YCbCrBlockExtractor:

  1. Uses exact BT.601 coefficients as floating point (not Go's fixed-point)
  2. For 4:2:0 chroma, takes only the top-left pixel of each 2x2 block (no averaging)
  3. Pre-converts the entire image to YCbCr arrays like Java does

func NewJamesBlockExtractor

func NewJamesBlockExtractor(img image.Image, subsampling jpeg.ChromaSubsamplingMode) *JamesBlockExtractor

NewJamesBlockExtractor creates a new JamesBlockExtractor that pre-converts the image to YCbCr using the exact same algorithm as JpegInfo.java.

func (*JamesBlockExtractor) ExtractBlock

func (e *JamesBlockExtractor) ExtractBlock(img image.Image, component, x, y int) [64]float64

ExtractBlock extracts an 8x8 block at the given position. Parameters: img (ignored, uses pre-converted arrays), component (0=Y, 1=Cb, 2=Cr), x, y (top-left position in pixel coordinates). Returns 64 float64 values in row-major order, NOT level-shifted (0-255 range).

type JamesQuantizer

type JamesQuantizer struct {
	// contains filtered or unexported fields
}

JamesQuantizer implements the combined DCT+quantization approach used by the original James R. Weeks JpegEncoder.java.

The key difference from standard JPEG implementations is that the AAN scale factors are incorporated into the divisor tables, which means the DCT output is multiplied by the divisor (which includes both the quantization value and AAN descaling factors).

The standard JPEG level shift (subtract 128) is applied inside ForwardDCTAndQuantize, matching f5.jar's DCT.forwardDCT (DCT.java line 78).

func NewJamesQuantizer

func NewJamesQuantizer(quality int) (*JamesQuantizer, error)

NewJamesQuantizer creates a new James R. Weeks-compatible quantizer.

Parameters:

  • quality: Quality level from 1 to 100 (same as libjpeg)

This quantizer matches the exact quantization behavior of JpegEncoder.java, including the integrated AAN scale factors.

func (*JamesQuantizer) ForwardDCTAndQuantize

func (jq *JamesQuantizer) ForwardDCTAndQuantize(input *[64]float64, isLuminance bool) [64]int

ForwardDCTAndQuantize performs the forward DCT using the AAN algorithm and quantizes the result in one step, matching f5.jar's DCT.forwardDCT.

Parameters:

  • input: 64-element block in row-major order (raw pixel values in [0, 255])
  • isLuminance: true for Y component, false for Cb/Cr components

The function applies the standard JPEG level shift (subtract 128) internally before running AAN — matching f5.jar's DCT.java line 78.

Returns 64 quantized integer coefficients in row-major order (NOT zigzag).

Performance Note (DEC-005): CPU profiling shows this function accounts for ~13% of total encoding time. All local arrays (output, workspace, in, result) are stack-allocated and do NOT escape to heap. Further optimization would require SIMD which is out of scope. See decisions.md for full profiling data.

func (*JamesQuantizer) GetQuantTable

func (jq *JamesQuantizer) GetQuantTable(isLuminance bool) [64]int

GetQuantTable returns the quantization table for the specified component. Values are in row-major order (not zigzag).

func (*JamesQuantizer) QuantizeBlock

func (jq *JamesQuantizer) QuantizeBlock(block *[64]float64, isLuminance bool) [64]int

QuantizeBlock implements the Quantizer interface but should not be used directly when byte-identical output is needed. Use ForwardDCTAndQuantize instead to get the integrated DCT+quantization behavior.

type MockBitWriter

type MockBitWriter struct {
	WriteErr    error // Set to simulate write errors
	WrittenBits []struct {
		Bits  uint32
		NBits int
	}
	Flushed bool
}

MockBitWriter is a mock implementation of BitWriter for testing. It records all write operations for verification.

func (*MockBitWriter) Flush

func (m *MockBitWriter) Flush() error

Flush marks the mock as flushed.

func (*MockBitWriter) WriteBits

func (m *MockBitWriter) WriteBits(bits uint32, nBits int) error

WriteBits records the write operation.

type MockBlockEncoder

type MockBlockEncoder struct {
	EncodeErr error
	FlushErr  error
	// EncodedBlocks records all blocks passed to EncodeBlock
	EncodedBlocks []struct {
		Block       [64]int
		PrevDC      int
		IsLuminance bool
	}
	EncodeCalls int
	Flushed     bool
}

MockBlockEncoder is a mock implementation of BlockEncoder for testing.

func (*MockBlockEncoder) EncodeBlock

func (m *MockBlockEncoder) EncodeBlock(block *[64]int, prevDC int, isLuminance bool) (newDC int, err error)

EncodeBlock records the call and returns the DC value.

func (*MockBlockEncoder) Flush

func (m *MockBlockEncoder) Flush() error

Flush marks the mock as flushed.

type MockBlockExtractor

type MockBlockExtractor struct {
	// ExtractedPositions records all extraction positions
	ExtractedPositions []struct {
		Component int
		X, Y      int
	}
	ExtractCalls int
	FixedBlock   [64]float64
}

MockBlockExtractor is a mock implementation of BlockExtractor for testing.

func (*MockBlockExtractor) ExtractBlock

func (m *MockBlockExtractor) ExtractBlock(img image.Image, component, x, y int) [64]float64

ExtractBlock returns the fixed block and records the call.

type MockDCT

type MockDCT struct {
	// TransformFunc can be set to provide custom transform behavior
	TransformFunc func(block *[64]float64)
	ForwardCalls  int
}

MockDCT is a mock implementation of DCT for testing.

func (*MockDCT) Forward

func (m *MockDCT) Forward(block *[64]float64)

Forward records the call and optionally applies a transform.

type MockHuffmanTable

type MockHuffmanTable struct {
	Codes map[byte]struct {
		Code uint16
		Size uint8
	}
}

MockHuffmanTable is a mock implementation of HuffmanTable for testing.

func (*MockHuffmanTable) Encode

func (m *MockHuffmanTable) Encode(symbol byte) (code uint16, size uint8)

Encode returns the mock code for the symbol.

type MockQuantizer

type MockQuantizer struct {
	LumTable      [64]int
	ChromTable    [64]int
	QuantizeCalls int
}

MockQuantizer is a mock implementation of Quantizer for testing.

func (*MockQuantizer) GetQuantTable

func (m *MockQuantizer) GetQuantTable(isLuminance bool) [64]int

GetQuantTable returns the mock quantization table.

func (*MockQuantizer) QuantizeBlock

func (m *MockQuantizer) QuantizeBlock(block *[64]float64, isLuminance bool) [64]int

QuantizeBlock returns a quantized block using the mock tables.

type Option

type Option func(*WeeksEncoder)

Option is a functional option for configuring WeeksEncoder. Options are applied in order when creating an encoder with NewWeeksEncoderWithOptions.

func WithBlockEncoder

func WithBlockEncoder(be BlockEncoder) Option

WithBlockEncoder sets a custom BlockEncoder implementation. Use this to inject a mock encoder for testing or an alternative entropy coding strategy.

Example:

mockEncoder := &MockBlockEncoder{}
enc, _ := NewWeeksEncoderWithOptions(w, 75, WithBlockEncoder(mockEncoder))

func WithBlockExtractor

func WithBlockExtractor(bx BlockExtractor) Option

WithBlockExtractor sets a custom BlockExtractor implementation. Use this to inject a mock extractor for testing or an alternative color space handler.

Example:

customExtractor := NewYCbCrBlockExtractor(jpeg.ChromaSubsampling444)
enc, _ := NewWeeksEncoderWithOptions(w, 75, WithBlockExtractor(customExtractor))

func WithBlockTap

func WithBlockTap(tap BlockTapFunc) Option

WithBlockTap installs a callback invoked for every quantized DCT block immediately before entropy coding. The tap may mutate the block in place; the mutation is what gets encoded. Compose with WithBlockEncoder: the tap wraps whichever block encoder is in effect (default or custom).

Used by F5 steganalysis pipelines to extract cover coefficients on the fly without round-tripping through JPEG decode, and to inject embedded coefficients in a second-pass encode for round-trip validation.

func WithComment

func WithComment(comment string) Option

WithComment sets a custom COM marker comment. If not specified, the default James R. Weeks signature is used.

Example:

enc, _ := NewWeeksEncoderWithOptions(w, 75, WithComment("My Custom Comment"))

func WithDCT

func WithDCT(dct DCT) Option

WithDCT sets a custom DCT implementation. Use this to inject a mock DCT for testing or an alternative transform.

Example:

mockDCT := &MockDCT{}
enc, _ := NewWeeksEncoderWithOptions(w, 75, WithDCT(mockDCT))

func WithEXIF

func WithEXIF(exifData []byte) Option

WithEXIF directly sets EXIF data to be preserved in the output. The data should include the EXIF signature ("Exif\x00\x00") prefix.

This option is useful when you have already extracted EXIF data and want to apply it to multiple encodings without re-parsing.

Example:

exifData, _ := ParseEXIF(sourceReader)
enc1, _ := NewWeeksEncoderWithOptions(w1, 75, WithEXIF(exifData))
enc2, _ := NewWeeksEncoderWithOptions(w2, 90, WithEXIF(exifData))

func WithICCProfile

func WithICCProfile(iccProfile []byte) Option

WithICCProfile directly sets ICC profile data to be preserved in the output. The data should be the raw ICC profile (without APP2 marker overhead).

This option is useful when you have already extracted ICC profile data and want to apply it to multiple encodings without re-parsing.

Example:

iccData, _ := ParseICCProfile(sourceReader)
enc1, _ := NewWeeksEncoderWithOptions(w1, 75, WithICCProfile(iccData))
enc2, _ := NewWeeksEncoderWithOptions(w2, 90, WithICCProfile(iccData))

func WithMaxWorkers

func WithMaxWorkers(n int) Option

WithMaxWorkers caps the number of goroutines used for the parallel DCT+quantize phase. A value <= 0 means use runtime.GOMAXPROCS(0) (the default). The worker count never affects the output bytes — only throughput.

Example (limit a batch worker to 4 cores per image so other images get CPU):

enc, _ := NewWeeksEncoderWithOptions(w, 75, WithMaxWorkers(4))

func WithParallelEncoding

func WithParallelEncoding(enabled bool) Option

WithParallelEncoding enables or disables multi-core encoding for James-compatible mode. When enabled (the default), the per-block forward DCT and quantization run across multiple goroutines while the entropy (Huffman) stage stays sequential, so the output remains byte-identical to f5.jar regardless of the worker count. Small images automatically fall back to the sequential path where parallelism would not pay off.

Disable it for fully single-threaded, deterministic-scheduling encoding:

enc, _ := NewWeeksEncoderWithOptions(w, 75, WithParallelEncoding(false))

func WithPixelKnotHeaders

func WithPixelKnotHeaders() Option

WithPixelKnotHeaders configures the encoder to reproduce PixelKnot's header layout: both the JFIF APP0 segment and the COM comment marker are omitted, so the stream runs straight SOI -> DQT -> SOF0 -> DHT -> SOS. The DCT coefficient embedding, quantization tables, Huffman tables and entropy (0-bit) padding are unchanged, exactly mirroring guardianproject/F5Android, which differs from f5.jar only by commenting out those two WriteHeaders writes. Convenience for WithoutAPP0() + WithoutComment().

func WithQuantizer

func WithQuantizer(q Quantizer) Option

WithQuantizer sets a custom Quantizer implementation. Use this to inject a mock quantizer for testing or a custom quantization strategy.

Example:

customQuant, _ := NewIJGQuantizer(90)
enc, _ := NewWeeksEncoderWithOptions(w, 75, WithQuantizer(customQuant))

func WithSourceImage

func WithSourceImage(r io.Reader) Option

WithSourceImage extracts and preserves metadata (EXIF, ICC profile) from a source JPEG image. The metadata will be written to the output when encoding.

This option parses the source image's APP1 marker for EXIF data and APP2 markers for ICC profile data, storing both for preservation. If the source has no EXIF or ICC data, the corresponding APP markers will not be written to the output (graceful handling).

The source reader should provide valid JPEG data. If the source is not valid JPEG or cannot be read, the metadata fields will remain empty (no error during option application; encoding will proceed without metadata).

Note: This function reads all data from the reader, so it cannot be used with streaming readers that do not support seeking. Use WithSourceImageBytes for byte slice inputs if you need to parse metadata multiple times.

Example:

f, _ := os.Open("photo_with_metadata.jpg")
defer f.Close()
enc, _ := NewWeeksEncoderWithOptions(w, 75, WithSourceImage(f))

func WithSourceImageBytes

func WithSourceImageBytes(data []byte) Option

WithSourceImageBytes extracts and preserves metadata (EXIF, ICC profile) from JPEG bytes. This is a convenience wrapper that parses both EXIF and ICC profile data from the source bytes.

Example:

jpegData, _ := os.ReadFile("photo_with_metadata.jpg")
enc, _ := NewWeeksEncoderWithOptions(w, 75, WithSourceImageBytes(jpegData))

func WithStandardMode

func WithStandardMode() Option

WithStandardMode disables James-compatible mode and uses standard JPEG encoding. In standard mode, the encoder:

  • Applies level shift (subtracts 128) before DCT, as per ITU-T T.81
  • Uses standard bit buffer layout
  • Pads remaining bits with 1s (standard behavior)

This produces output that is decodable by Go's standard image/jpeg decoder and other standard JPEG decoders, but is NOT byte-identical with the James R. Weeks Java encoder.

Use this option when you need Go-decodable output for testing or when byte-compatibility with the James encoder is not required.

Example:

enc, _ := NewWeeksEncoderWithOptions(w, 75, WithStandardMode())
// Output will be decodable by image/jpeg.Decode

func WithSubsampling

func WithSubsampling(mode jpeg.ChromaSubsamplingMode) Option

WithSubsampling sets the chroma subsampling mode. This also updates the block extractor to use the new mode.

Supported modes:

  • jpeg.ChromaSubsampling420: 4:2:0 (default, most common)
  • jpeg.ChromaSubsampling422: 4:2:2 (horizontal-only subsampling)
  • jpeg.ChromaSubsampling444: 4:4:4 (no subsampling, highest quality)

Example:

enc, _ := NewWeeksEncoderWithOptions(w, 75, WithSubsampling(jpeg.ChromaSubsampling444))

func WithoutAPP0

func WithoutAPP0() Option

WithoutAPP0 suppresses the JFIF APP0 segment in the output. The classic f5.jar/James encoder always writes an APP0; PixelKnot's F5 engine (guardianproject/F5Android) comments the write out. Use this to produce PixelKnot-style stripped-header output. Output is no longer byte-identical to f5.jar.

func WithoutComment

func WithoutComment() Option

WithoutComment suppresses the COM comment marker in the output. The classic f5.jar/James encoder always writes the "James R. Weeks" copyright comment; PixelKnot comments the write out. Output is no longer byte-identical to f5.jar.

type Quantizer

type Quantizer interface {
	// QuantizeBlock quantizes DCT coefficients in a block.
	// isLuminance determines which quantization table to use:
	//   - true: luminance (Y component)
	//   - false: chrominance (Cb/Cr components)
	// Returns zigzag-ordered quantized coefficients suitable for entropy coding.
	QuantizeBlock(block *[64]float64, isLuminance bool) [64]int

	// GetQuantTable returns the quantization table for the specified component.
	// Used for writing DQT markers and for inspection/debugging.
	GetQuantTable(isLuminance bool) [64]int
}

Quantizer performs DCT coefficient quantization for JPEG encoding.

Implementations determine how DCT coefficients are divided by quantization table values to reduce precision and file size. The quantization step is lossy and primarily responsible for JPEG compression artifacts.

The IJG (Independent JPEG Group) quality scaling formula is commonly used:

  • Quality < 50: scale = 5000 / quality
  • Quality >= 50: scale = 200 - 2*quality

Example usage:

quantizer, _ := NewIJGQuantizer(75)
quantized := quantizer.QuantizeBlock(&dctBlock, true) // luminance

type TapBlockEncoder

type TapBlockEncoder struct {
	Inner BlockEncoder
	Tap   BlockTapFunc
	// contains filtered or unexported fields
}

TapBlockEncoder implements BlockEncoder by invoking Tap on each block, then forwarding the (possibly mutated) block to Inner.

If Inner is nil the block is discarded after the tap fires — useful for the extract-only path where the caller wants quantized coefficients but no JPEG output. In that mode the encoder driver still writes JPEG headers; pass io.Discard as the encoder writer if the bytes are not wanted.

func NewTapBlockEncoder

func NewTapBlockEncoder(inner BlockEncoder, tap BlockTapFunc) *TapBlockEncoder

NewTapBlockEncoder constructs a TapBlockEncoder. Either Inner or Tap (or both) may be set; both nil is a no-op.

func (*TapBlockEncoder) EncodeBlock

func (t *TapBlockEncoder) EncodeBlock(block *[64]int, prevDC int, isLuminance bool) (int, error)

EncodeBlock satisfies BlockEncoder. The tap fires first; if Inner is non-nil the (possibly mutated) block is forwarded to it.

func (*TapBlockEncoder) Flush

func (t *TapBlockEncoder) Flush() error

Flush satisfies BlockEncoder. No-op if Inner is nil.

func (*TapBlockEncoder) Reset

func (t *TapBlockEncoder) Reset()

Reset zeroes the block index counter so the same tap can be reused across multiple Encode calls.

type WeeksEncoder

type WeeksEncoder struct {
	// contains filtered or unexported fields
}

WeeksEncoder is a baseline JPEG encoder compatible with the James R. Weeks encoder. It produces standard baseline JPEG files (SOF0) with the James/BioElectroMech COM marker signature, making the output compatible with F5 steganography tools.

Usage:

var buf bytes.Buffer
enc, err := NewWeeksEncoder(&buf, 75)
if err != nil {
    return err
}
err = enc.Encode(img)

func NewWeeksEncoder

func NewWeeksEncoder(w io.Writer, quality int) (*WeeksEncoder, error)

NewWeeksEncoder creates a new James R. Weeks-compatible JPEG encoder.

Parameters:

  • w: Output writer for the JPEG data
  • quality: Quality level from 1 to 100 (same as libjpeg)

The encoder is initialized with:

  • Default James R. Weeks COM marker signature
  • 4:2:0 chroma subsampling (standard)
  • Scaled quantization tables based on quality
  • Standard Huffman tables from ITU-T T.81 Annex K

Returns an error if quality is outside the valid range [1, 100].

func NewWeeksEncoderWithOptions

func NewWeeksEncoderWithOptions(w io.Writer, quality int, opts ...Option) (*WeeksEncoder, error)

NewWeeksEncoderWithOptions creates a new James R. Weeks-compatible JPEG encoder with options.

Parameters:

  • w: Output writer for the JPEG data
  • quality: Quality level from 1 to 100 (same as libjpeg)
  • opts: Functional options to customize the encoder

The encoder is initialized with:

  • Default James R. Weeks COM marker signature
  • 4:2:0 chroma subsampling (standard)
  • Scaled quantization tables based on quality
  • Standard Huffman tables from ITU-T T.81 Annex K

Options can override these defaults. See WithQuantizer, WithBlockEncoder, WithBlockExtractor, WithDCT, WithComment, and WithSubsampling.

Returns an error if quality is outside the valid range [1, 100].

func (*WeeksEncoder) Encode

func (e *WeeksEncoder) Encode(img image.Image) error

Encode encodes an image to JPEG format and writes it to the output writer.

The encoding process:

  1. Convert image to YCbCr color space
  2. Apply chroma subsampling based on configured mode
  3. Write JPEG structure: SOI, APP0, APP1 (EXIF if present), APP2 (ICC if present), COM, DQT, SOF0, DHT, SOS
  4. Process image in MCUs (8x8 blocks, interleaved)
  5. For each block: forward DCT, quantize, zigzag, entropy encode
  6. Write EOI marker

Note: The James encoder does NOT level shift before DCT. This differs from standard JPEG implementations but is required for byte-identical output.

Returns an error if the image is nil or encoding fails.

func (*WeeksEncoder) GetHuffmanTableError

func (e *WeeksEncoder) GetHuffmanTableError() error

GetHuffmanTableError returns the last error from SetHuffmanTable, if any. Returns nil if the last SetHuffmanTable call succeeded.

func (*WeeksEncoder) HasCustomHuffmanTable

func (e *WeeksEncoder) HasCustomHuffmanTable(tableClass, tableNum int) bool

HasCustomHuffmanTable returns true if a custom Huffman table is set for the specified class and number.

func (*WeeksEncoder) SetComment

func (e *WeeksEncoder) SetComment(comment string) *WeeksEncoder

SetComment sets a custom COM marker comment. Returns the encoder for method chaining.

If not called, the default James R. Weeks signature is used: "JPEG Encoder Copyright 1998, James R. Weeks and BioElectroMech."

func (*WeeksEncoder) SetHuffmanTable

func (e *WeeksEncoder) SetHuffmanTable(tableClass, tableNum int, bits [16]byte, values []byte) *WeeksEncoder

SetHuffmanTable sets a custom Huffman table for encoding. Returns the encoder for method chaining.

Parameters:

  • tableClass: 0 for DC, 1 for AC
  • tableNum: 0 for luminance, 1 for chrominance
  • bits: Array of 16 bytes specifying code lengths (bits[i] = count of codes with length i+1)
  • values: Symbol values in order of increasing code length

Validation:

  • tableClass must be 0 or 1
  • tableNum must be 0 or 1
  • values length must equal sum of bits array

If validation fails, the table is not set and the error is stored in lastHuffmanTableError. Custom tables override the standard ITU-T T.81 Huffman tables when encoding.

Example:

// Set custom DC luminance table
bits := [16]byte{0, 1, 5, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0}
values := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}
enc.SetHuffmanTable(0, 0, bits, values)

func (*WeeksEncoder) SetQuantizationTable

func (e *WeeksEncoder) SetQuantizationTable(tableNum int, table [64]int) *WeeksEncoder

SetQuantizationTable sets a custom quantization table for the specified component. Returns the encoder for method chaining.

Parameters:

  • tableNum: 0 for luminance (Y component), 1 for chrominance (Cb/Cr components)
  • table: Array of exactly 64 quantization values in row-major order

Table values must be in the range 1-255 per ITU-T T.81 specification. Custom tables override the standard ITU-T T.81 Annex K base tables that are normally scaled by the quality parameter.

Any validation errors are stored internally and will cause Encode() to fail with a descriptive error message.

Example:

// Set a flat luminance table (minimal quantization)
var lumTable [64]int
for i := range lumTable {
    lumTable[i] = 1
}
enc.SetQuantizationTable(0, lumTable)

func (*WeeksEncoder) SetSubsampling

func (e *WeeksEncoder) SetSubsampling(mode jpeg.ChromaSubsamplingMode) *WeeksEncoder

SetSubsampling sets the chroma subsampling mode. Returns the encoder for method chaining.

Supported modes:

  • jpeg.ChromaSubsampling420: 4:2:0 (default, most common)
  • jpeg.ChromaSubsampling422: 4:2:2 (horizontal-only subsampling)
  • jpeg.ChromaSubsampling444: 4:4:4 (no subsampling, highest quality)

type YCbCrBlockExtractor

type YCbCrBlockExtractor struct {
	// contains filtered or unexported fields
}

YCbCrBlockExtractor implements the BlockExtractor interface for YCbCr color space.

This extractor handles color space conversion from any image.Image to YCbCr, chroma subsampling with neighborhood averaging for 4:2:0 and 4:2:2 modes, boundary clamping for edge blocks, and pixel replication at image boundaries.

The subsampling mode affects how chroma (Cb/Cr) components are extracted:

  • 4:4:4: No subsampling, each chroma pixel maps 1:1
  • 4:2:2: Horizontal 2:1 subsampling, chroma averaged over 2 horizontal pixels
  • 4:2:0: Both 2:1 subsampling, chroma averaged over 2x2 pixel blocks

func NewYCbCrBlockExtractor

func NewYCbCrBlockExtractor(subsampling jpeg.ChromaSubsamplingMode) *YCbCrBlockExtractor

NewYCbCrBlockExtractor creates a new YCbCrBlockExtractor with the specified chroma subsampling mode (ChromaSubsampling444, ChromaSubsampling422, or ChromaSubsampling420).

func (*YCbCrBlockExtractor) ExtractBlock

func (e *YCbCrBlockExtractor) ExtractBlock(img image.Image, component, x, y int) [64]float64

ExtractBlock extracts an 8x8 block at the given position. Parameters: img (source image), component (0=Y, 1=Cb, 2=Cr), x, y (top-left position). Returns 64 float64 values in row-major order, NOT level-shifted (0-255 range). For chroma components with subsampling, averages neighborhood pixels.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL