Most JPEG workflows treat exposure (brightness) and contrast as inherently "lossy": decode pixels, apply curves, then re-encode. That approach works, but it always introduces an additional step of quantization error.
In this fork of the IJG JPEG-10 code, I added two options to jpegtran that operate directly on quantized DCT coefficients:
-exposure-comp EV-contrast DC LOW MID HIGH
Both are applied during transcoding, so they combine naturally with existing jpegtran operations such as rotation, flipping, cropping, marker copying, and progressive conversion.
jpegtran [standard options] [-exposure-comp EV] [-contrast DC LOW MID HIGH] input.jpg output.jpgExamples:
# Brighten by 1 stop
jpegtran -copy all -exposure-comp 1 input.jpg output.jpg
# Darken by 0.5 stops
jpegtran -copy all -exposure-comp -0.5 input.jpg output.jpg
# Contrast (uniform: DC=LOW=MID=HIGH)
jpegtran -copy all -contrast -1 -1 -1 -1 input.jpg out-contrast-u-1.jpg
jpegtran -copy all -contrast -0.5 -0.5 -0.5 -0.5 input.jpg out-contrast-u-0.5.jpg
jpegtran -copy all -contrast 0.5 0.5 0.5 0.5 input.jpg out-contrast-u+0.5.jpg
jpegtran -copy all -contrast 1 1 1 1 input.jpg out-contrast-u+1.jpg
# Contrast (band-specific examples)
jpegtran -copy all -contrast 0 0 0.6 0 input.jpg out-contrast-mid+0.6.jpg
jpegtran -copy all -contrast 0 0 0 0.4 input.jpg out-contrast-high+0.4.jpg
jpegtran -copy all -contrast 0 0.4 0 0 input.jpg out-contrast-low+0.4.jpg
# Combine: rotate 90°, brighten 0.5 EV, and add uniform contrast +0.5
jpegtran -copy all -rot 90 -exposure-comp 0.5 -contrast 0.5 0.5 0.5 0.5 input.jpg output.jpgBoth switches accept fractional values. Practical ranges:
| Option | Practical range | Neutral |
|-:-|:-:|:-:|
| -exposure-comp EV | -3 … +3 | 0 |
| -contrast DC LOW MID HIGH | -2 … +2 | 0 |
Integrated into cPicture with live preview:
A JPEG image is encoded as a grid of DCT blocks (with 8×8 Elements in size). Each block has one DC coefficient and 63 AC coefficients. But each MCU might have more than one block depending on the color subsampling.
-
DC[0] represents the (level-shifted) average sample value of the block. The relationship to pixel mean is:
$$\mu = \frac{DC_\text{unquant}}{N} + \text{center}$$ where
$N$ is the DCT block size of 8 and$\text{center} = 2^{\text{precision}-1}$ (e.g. 128 for 8-bit). -
AC[1..N²-1] represent spatial frequency components (texture, edges, contrast).
Both DC and AC are stored quantized: the actual stored integer is
Exposure compensation from -2EV to +2EV:

A photographic EV step corresponds to doubling (or halving) the amount of light. Applied in linear light:
Because JPEG samples are gamma-coded (sRGB), pixel values cannot be multiplied directly. Instead:
- Estimate a representative level from the DC blocks.
- Compute the equivalent additive pixel-domain offset by applying the gain in linear light at that reference level.
- Translate the offset into a quantized DC delta.
- Add the delta to every DC coefficient.
Only DC is modified. AC coefficients are not modified, so local contrast and texture are preserved.
A geometric mean (log-average) of all block mean levels is used as the exposure reference:
where
The gain is applied in linear light:
The sRGB transfer functions used:
Clamped to available headroom/shadow room to limit clipping, then converted to a quantized DC delta:
where
| Color space | Components adjusted |
|---|---|
| YCbCr, BG_YCC, YCCK | Luma only (component 0) |
| RGB/BG_RGB + subtract-green transform | Green/base only (component 1) |
| CMYK, all others | All components |
For CMYK and YCCK the delta is computed in an inverted intensity domain (
This option provides four separate controls (all in stops):
DCcontrols the DC coefficient (block mean)LOW,MID,HIGHcontrol the AC coefficients in frequency order
All controls are interpreted as log2 gains (stops). For a value
So +1 doubles, -1 halves.
DC is scaled by:
and applied as:
AC coefficients are processed in zigzag order (the JPEG natural order). Let
Define a normalized position:
Triangular weights:
- low weight fades out from low frequencies
- mid weight peaks in the middle
- high weight fades in toward high frequencies
Per-coefficient exponent and gain:
Applied to each AC coefficient:
If DC = LOW = MID = HIGH = X, then all coefficients are scaled by the same gain
Same as -exposure-comp:
- YCbCr/BG_YCC/YCCK: luma only
- RGB subtract-green: base/green only
- otherwise: all components
Both -exposure-comp and -contrast are applied as a post step after any geometric transform (-rot, -flip, -crop, …). The tonal operations work on the final output coefficient arrays, so the order of switches on the command line does not matter.
- Core implementation:
transupp.c:do_exposure_comp()anddo_contrast()transupp.h: adds new fields tojpeg_transform_info
- CLI parsing:
jpegtran.c
- Feature flags and parameters are stored in
jpeg_transform_infointransupp.h
-exposure-comp EVshifts brightness by changing only DC coefficients, with EV evaluated in linear light (sRGB transfer) at a log-average reference.-contrast DC LOW MID HIGHscales DC and AC coefficients, with AC gains varying smoothly over frequency order using low/mid/high controls.- Both run in the DCT domain and integrate naturally into the lossless-transformation workflow of
jpegtran.
