Implementing JPEG Algorithm in Java

The JPEG (Joint Photographic Experts Group) compression standard is the most widely used format for digital photographs on the web. Unlike lossless codecs, JPEG achieves very high compression ratios by discarding perceptually insignificant information from the image. At its core, the algorithm transforms pixel data into the frequency domain, aggressively quantises the high-frequency components (which the human eye is less sensitive to), and then encodes the result efficiently.

This Java program demonstrates the key computational stages of JPEG compression applied to a single 8×8 pixel block: the Discrete Cosine Transform (DCT), quantisation, zigzag scan, and a simplified entropy encoding step.

This example provides an educational look at the key stages of JPEG compression.

JPEG Compression Pipeline

A real JPEG encoder processes an image in the following sequence. This program implements all four stages for one 8×8 luminance block:

  1. Input block — An 8×8 grid of pixel intensity values.
  2. DCT — Converts spatial pixel data into frequency-domain coefficients.
  3. Quantisation — Divides each DCT coefficient by a value from the standard luminance quantisation table and rounds to the nearest integer, discarding fine detail.
  4. Zigzag scan — Reorders the 8×8 quantised coefficients into a 1D array, placing the most significant low-frequency coefficients first.
  5. Entropy encoding — Encodes the 1D array using a simplified run-length + binary scheme similar to the actual JPEG Huffman coding step.

Code

package ajpeg;

import java.io.*;
import java.math.*;

public class Ajpeg {

    // Input 8x8 pixel block (sample luminance values)
    static double pixelBlock[][] = {
        {48,  39,  40,  68,  60,  38,  50,  121},
        {149, 82,  79,  101, 113, 106, 27,  62 },
        {58,  63,  77,  69,  124, 107, 74,  125},
        {80,  97,  74,  54,  59,  71,  91,  66 },
        {18,  34,  33,  46,  64,  61,  32,  37 },
        {149, 108, 80,  106, 116, 61,  73,  92 },
        {211, 233, 159, 88,  107, 158, 161, 109},
        {212, 104, 40,  44,  71,  136, 113, 66 }
    };

    // 1D array to hold the 64 zigzag-ordered quantised coefficients
    static int zigzagCoeffs[] = new int[64];

    // Buffer for the entropy-encoded bitstream
    static int entropyBuffer[] = new int[128];

    /*
     * Main method: drives the JPEG compression pipeline
     */
    public static void main(String args[]) throws IOException {

        getinput(); // Placeholder for loading real image data

        System.out.println("\n\n\n Input");
        display();

        // Step 1: Discrete Cosine Transform
        System.out.println("\n\n\n On DCT");
        dct();
        display();

        // Step 2: Quantisation
        System.out.println("\n\n\n On Quant");
        quant();
        display();

        // Step 3: Zigzag scan (reorders into 1D array)
        zigzag();

        // Step 4: Simplified entropy encoding
        se1();
    }

    /*
     * Placeholder for reading image data from external input.
     * Currently unused — the pixel block is hardcoded above.
     */
    public static void getinput() throws IOException {
        // To enable interactive input, uncomment the lines below:
        // BufferedReader obj = new BufferedReader(new InputStreamReader(System.in));
        // System.out.println("Enter input matrix");
        // pixelBlock = new double[8][8];
        // for (int i = 0; i < 8; i++)
        //     for (int j = 0; j < 8; j++)
        //         pixelBlock[i][j] = Integer.parseInt(obj.readLine());
    }

    /*
     * Prints the current 8x8 matrix (pixelBlock) to the console,
     * one row per line, values separated by tabs.
     */
    public static void display() {
        for (int i = 0; i < 8; i++) {
            for (int j = 0; j < 8; j++) {
                System.out.print(pixelBlock[i][j] + "\t");
                if (j == 7) { System.out.println(); }
            }
        }
    }

    /*
     * Computes the 2D Discrete Cosine Transform (DCT) of the 8x8 pixel block.
     * The standard JPEG DCT formula is:
     *   F(u,v) = (1/4) * C(u) * C(v) * sum_i sum_j f(i,j)
     *              * cos(PI*(2i+1)*u/16) * cos(PI*(2j+1)*v/16)
     * where C(k) = 1/sqrt(2) for k=0, and 1 otherwise.
     */
    public static void dct() {
        int i, j, u, v;
        double coeffValue;
        double dctOutput[][] = new double[8][8];

        for (u = 0; u < 8; u++) {
            for (v = 0; v < 8; v++) {
                coeffValue = 0;

                // Accumulate the weighted sum over all pixel positions
                for (i = 0; i < 8; i++) {
                    for (j = 0; j < 8; j++) {
                        coeffValue += pixelBlock[i][j]
                            * Math.cos(Math.PI * (2 * i + 1) * u / 16)
                            * Math.cos(Math.PI * (2 * j + 1) * v / 16);
                    }
                }

                // Apply the 1/4 scaling factor
                coeffValue *= 0.25;

                // Apply the 1/sqrt(2) normalisation for the DC row (u=0)
                if (u == 0) { coeffValue /= Math.sqrt(2); }

                // Apply the 1/sqrt(2) normalisation for the DC column (v=0)
                if (v == 0) { coeffValue /= Math.sqrt(2); }

                dctOutput[u][v] = coeffValue;
            }
        }

        // Replace the pixel block with the DCT coefficients
        pixelBlock = dctOutput;
    }

    /*
     * Quantises the DCT coefficients using the standard JPEG luminance
     * quantisation table. Each coefficient is divided by the corresponding
     * table entry and rounded to the nearest integer.
     * Higher table values discard more information (higher compression).
     */
    public static void quant() {
        double quantTable[][] = {
            {16, 11, 12, 16,  24,  40,  51,  61 },
            {12, 12, 14, 19,  26,  58,  60,  55 },
            {14, 13, 16, 24,  40,  57,  69,  56 },
            {14, 17, 22, 29,  51,  87,  80,  62 },
            {18, 22, 37, 56,  68,  109, 103, 77 },
            {24, 35, 55, 64,  81,  104, 113, 92 },
            {49, 64, 78, 87,  103, 121, 120, 101},
            {72, 92, 95, 98,  112, 110, 103, 99 }
        };

        for (int i = 0; i < 8; i++) {
            for (int j = 0; j < 8; j++) {
                pixelBlock[i][j] = Math.round(pixelBlock[i][j] / quantTable[i][j]);
            }
        }
    }

    /*
     * Reorders the 8x8 quantised coefficient matrix into a 1D array
     * following the JPEG zigzag scan pattern. The zigzag ordering
     * clusters near-zero high-frequency coefficients together at the
     * end of the array, which improves the run-length encoding step.
     */
    public static void zigzag() {
        // Zigzag order: low-frequency top-left corner first
        zigzagCoeffs[0]  = (int)pixelBlock[0][0]; zigzagCoeffs[1]  = (int)pixelBlock[0][1];
        zigzagCoeffs[2]  = (int)pixelBlock[1][0]; zigzagCoeffs[3]  = (int)pixelBlock[2][0];
        zigzagCoeffs[4]  = (int)pixelBlock[1][1]; zigzagCoeffs[5]  = (int)pixelBlock[0][2];
        zigzagCoeffs[6]  = (int)pixelBlock[0][3]; zigzagCoeffs[7]  = (int)pixelBlock[1][2];
        zigzagCoeffs[8]  = (int)pixelBlock[2][1]; zigzagCoeffs[9]  = (int)pixelBlock[3][0];
        zigzagCoeffs[10] = (int)pixelBlock[4][0]; zigzagCoeffs[11] = (int)pixelBlock[3][1];
        zigzagCoeffs[12] = (int)pixelBlock[2][2]; zigzagCoeffs[13] = (int)pixelBlock[1][3];
        zigzagCoeffs[14] = (int)pixelBlock[0][4]; zigzagCoeffs[15] = (int)pixelBlock[0][5];
        zigzagCoeffs[16] = (int)pixelBlock[1][4]; zigzagCoeffs[17] = (int)pixelBlock[2][3];
        zigzagCoeffs[18] = (int)pixelBlock[3][2]; zigzagCoeffs[19] = (int)pixelBlock[4][1];
        zigzagCoeffs[20] = (int)pixelBlock[5][0]; zigzagCoeffs[21] = (int)pixelBlock[6][0];
        zigzagCoeffs[22] = (int)pixelBlock[5][1]; zigzagCoeffs[23] = (int)pixelBlock[4][2];
        zigzagCoeffs[24] = (int)pixelBlock[3][3]; zigzagCoeffs[25] = (int)pixelBlock[2][4];
        zigzagCoeffs[26] = (int)pixelBlock[1][5]; zigzagCoeffs[27] = (int)pixelBlock[0][6];
        zigzagCoeffs[28] = (int)pixelBlock[0][7]; zigzagCoeffs[29] = (int)pixelBlock[1][6];
        zigzagCoeffs[30] = (int)pixelBlock[2][5]; zigzagCoeffs[31] = (int)pixelBlock[3][4];
        zigzagCoeffs[32] = (int)pixelBlock[4][3]; zigzagCoeffs[33] = (int)pixelBlock[5][2];
        zigzagCoeffs[34] = (int)pixelBlock[6][1]; zigzagCoeffs[35] = (int)pixelBlock[7][0];
        zigzagCoeffs[36] = (int)pixelBlock[7][1]; zigzagCoeffs[37] = (int)pixelBlock[6][2];
        zigzagCoeffs[38] = (int)pixelBlock[5][3]; zigzagCoeffs[39] = (int)pixelBlock[4][4];
        zigzagCoeffs[40] = (int)pixelBlock[3][5]; zigzagCoeffs[41] = (int)pixelBlock[2][6];
        zigzagCoeffs[42] = (int)pixelBlock[1][7]; zigzagCoeffs[43] = (int)pixelBlock[2][7];
        zigzagCoeffs[44] = (int)pixelBlock[3][6]; zigzagCoeffs[45] = (int)pixelBlock[4][5];
        zigzagCoeffs[46] = (int)pixelBlock[5][4]; zigzagCoeffs[47] = (int)pixelBlock[6][3];
        zigzagCoeffs[48] = (int)pixelBlock[7][2]; zigzagCoeffs[49] = (int)pixelBlock[7][3];
        zigzagCoeffs[50] = (int)pixelBlock[6][4]; zigzagCoeffs[51] = (int)pixelBlock[5][5];
        zigzagCoeffs[52] = (int)pixelBlock[4][6]; zigzagCoeffs[53] = (int)pixelBlock[3][7];
        zigzagCoeffs[54] = (int)pixelBlock[4][7]; zigzagCoeffs[55] = (int)pixelBlock[5][6];
        zigzagCoeffs[56] = (int)pixelBlock[6][5]; zigzagCoeffs[57] = (int)pixelBlock[7][4];
        zigzagCoeffs[58] = (int)pixelBlock[7][5]; zigzagCoeffs[59] = (int)pixelBlock[6][6];
        zigzagCoeffs[60] = (int)pixelBlock[5][7]; zigzagCoeffs[61] = (int)pixelBlock[6][7];
        zigzagCoeffs[62] = (int)pixelBlock[7][6]; zigzagCoeffs[63] = (int)pixelBlock[7][7];

        System.out.println("\n\n\n On zigzag");
        for (int i = 0; i < 64; i++) {
            // Use absolute values; sign information is handled by entropy encoding
            zigzagCoeffs[i] = Math.abs(zigzagCoeffs[i]);
            System.out.print(zigzagCoeffs[i] + "\t");
        }
    }

    /*
     * Simplified entropy encoding.
     * For each non-zero coefficient, encodes:
     *   - The run of preceding zeros (in binary)
     *   - The bit-length of the coefficient value (in binary)
     *   - The coefficient value itself (in binary)
     * This mimics the structure of JPEG's actual Huffman-coded entropy block.
     */
    public static void se1() {
        int i, savedPos;
        StringBuffer bitStream = new StringBuffer();

        for (i = 0; i < 64; i++) {
            int zeroRunLength = 0;
            savedPos = i;

            // Count consecutive zeros following the current coefficient
            while (i + 1 < 64 && zigzagCoeffs[i + 1] == 0) {
                zeroRunLength++;
                i++;
            }

            // Encode the zero run length (skip for the very first coefficient)
            if (i != 0) {
                bitStream.append(Integer.toBinaryString(zeroRunLength));
            }

            // Encode the number of bits needed to represent the coefficient
            bitStream.append(
                Integer.toBinaryString(
                    Integer.toBinaryString(zigzagCoeffs[savedPos]).length()
                )
            );

            // Encode the coefficient value in binary
            bitStream.append(Integer.toBinaryString(zigzagCoeffs[savedPos]));
        }

        System.out.println("\n\n\n" + bitStream.toString());
    }
}

How the Code Works

1. Input Block

The 8×8 array pixelBlock holds sample luminance (brightness) values for a single image block, ranging from 0 (black) to 255 (white). In a real encoder these values would come from a JPEG file or image buffer.

2. Discrete Cosine Transform (dct)

The dct() method applies the 2D DCT formula to the pixel block. The result is a new 8×8 matrix of frequency-domain coefficients. The top-left value (F[0][0]) is the DC coefficient representing the average brightness. All other values are AC coefficients representing progressively finer spatial frequencies. The four nested loops run in O(N⁴) time, which is acceptable for an 8×8 block.

3. Quantisation (quant)

Each DCT coefficient is divided by the corresponding value in the standard JPEG luminance quantisation table and rounded. The quantisation table has larger values at high-frequency positions, so fine detail is rounded away more aggressively than coarse structure. This is where JPEG becomes lossy — the rounding step is irreversible.

4. Zigzag Scan (zigzag)

The zigzag() method manually maps the 64 matrix positions into a 1D array in JPEG’s standard zigzag order. Starting from the DC coefficient at [0][0], it traverses the matrix diagonally, ending at the highest-frequency corner at [7][7]. Because quantisation typically zeroes out many high-frequency coefficients, this ordering clusters the non-zero values at the start of the array and produces long runs of zeros at the end — exactly what run-length encoding thrives on.

5. Entropy Encoding (se1)

The se1() method encodes each non-zero coefficient in the zigzag array as a compact binary token consisting of three parts: the binary count of preceding zeros (run length), the binary length of the coefficient’s binary representation (bit length), and the coefficient’s binary value. Concatenating all tokens produces the final compressed bitstream. In a production JPEG encoder this would be Huffman-coded; here a simplified binary scheme is used for clarity.

Sample Output

Input
48.0    39.0    40.0    68.0    60.0    38.0    50.0    121.0
149.0   82.0    79.0    101.0   113.0   106.0   27.0    62.0
58.0    63.0    77.0    69.0    124.0   107.0   74.0    125.0
80.0    97.0    74.0    54.0    59.0    71.0    91.0    66.0
18.0    34.0    33.0    46.0    64.0    61.0    32.0    37.0
149.0   108.0   80.0    106.0   116.0   61.0    73.0    92.0
211.0   233.0   159.0   88.0    107.0   158.0   161.0   109.0
212.0   104.0   40.0    44.0    71.0    136.0   113.0   66.0

On DCT
699.25   43.18   55.25   72.11   24.0    -25.51  11.21   -4.14
-129.78  -71.50  -70.26  -73.35  59.43   -24.02  22.61   -2.05
85.71    30.32   61.78   44.87   14.84   17.35   15.51   -13.19
-40.81   10.17   -17.53  -55.81  30.50   -2.28   -21.00  -1.26
-157.50  -49.39  13.27   -1.78   -8.75   22.47   -8.47   -9.23
92.49    -9.03   45.72   -48.13  -58.51  -9.01   -28.54  10.38
-53.09   -62.97  -3.49   -19.62  56.09   -2.25   -3.28   11.91
-20.54   -55.90  -20.59  -18.19  -26.58  -27.07  8.47    0.31

On Quant
44.0     4.0    5.0    5.0    1.0   -1.0   0.0    0.0
-11.0   -6.0   -5.0   -4.0   2.0    0.0   0.0    0.0
  6.0    2.0    4.0    2.0   0.0    0.0   0.0    0.0
 -3.0    1.0   -1.0   -2.0   1.0    0.0   0.0    0.0
 -9.0   -2.0    0.0    0.0   0.0    0.0   0.0    0.0
  4.0    0.0    1.0   -1.0  -1.0    0.0   0.0    0.0
 -1.0   -1.0    0.0    0.0   1.0    0.0   0.0    0.0
  0.0   -1.0    0.0    0.0   0.0    0.0   0.0    0.0

On zigzag
44  4  11  6  6  5  5  5  2  3  9  1  4  4  1  1
 2  2   1  2  4  1  0  0  2  0  0  0  0  0  0  1
 0  1   1  0  1  0  1  0  0  0  0  0  0  0  1  0
 0  0   1  0  0  0  0  0  0  0  0  0  0  0  0  0

1101011000111000100101101111001111001110101110101110101010010110100100101101110001110001101101010010100110101001110010111101010111011111111111111111110111

Output Explanation

  1. Input block — The 8×8 matrix of raw pixel values is printed as floating-point numbers. The wide range (18–233) reflects significant contrast in the image block.
  2. After DCT — The matrix now contains frequency coefficients. The top-left DC value (699.25) dominates — it represents the average block brightness. The remaining AC values are much smaller in magnitude.
  3. After quantisation — The coefficients are divided by the quantisation table and rounded. Many values in the bottom-right of the matrix have become 0, especially the high-frequency corners. The DC coefficient has been reduced from 699 to 44.
  4. Zigzag scan — The 64 values are printed in 1D zigzag order. The significant coefficients (44, 11, 9, etc.) appear at the start, while the trailing portion consists almost entirely of zeros, demonstrating why zigzag ordering enables effective run-length compression.
  5. Entropy encoded bitstream — The final binary string encodes all non-zero coefficients and their preceding zero runs. This compact representation is what would be written into the JPEG file’s data stream.

See Also

Conclusion

This program walks through the four essential stages of JPEG compression in clean, readable Java. Starting from a raw pixel block, it shows how the DCT transforms spatial data into frequency space, how quantisation introduces controlled lossiness, how the zigzag scan organises coefficients to maximise compressibility, and how entropy encoding converts the result into a compact bitstream. While a production-grade JPEG encoder adds colour space conversion, chroma subsampling, and proper Huffman coding, the core mathematics demonstrated here are identical to what runs inside every JPEG encoder on the planet.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.