ConvolveWithEdgeOpTest.java

/*
 * Copyright (c) 2026, Harald Kuhr
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * * Redistributions of source code must retain the above copyright notice, this
 *   list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 *
 * * Neither the name of the copyright holder nor the names of its
 *   contributors may be used to endorse or promote products derived from
 *   this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package com.twelvemonkeys.image;

import org.junit.jupiter.api.Test;

import java.awt.*;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.*;

import static org.junit.jupiter.api.Assertions.*;

/**
 * ConvolveWithEdgeOpTest
 */
class ConvolveWithEdgeOpTest {

    private final Kernel kernel3x3 = new Kernel(3, 3, new float[]{
            0, 0, 0,
            0, 1, 0,
            0, 0, 0
    });

    @Test
    void testConstructor() {
        ConvolveWithEdgeOp op = new ConvolveWithEdgeOp(kernel3x3);
        assertEquals(ConvolveWithEdgeOp.EDGE_ZERO_FILL, op.getEdgeCondition());
        assertArrayEquals(kernel3x3.getKernelData(null), op.getKernel().getKernelData(null));
    }

    @Test
    void testConstructorFull() {
        RenderingHints hints = new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        ConvolveWithEdgeOp op = new ConvolveWithEdgeOp(kernel3x3, ConvolveWithEdgeOp.EDGE_REFLECT, hints);
        assertEquals(ConvolveWithEdgeOp.EDGE_REFLECT, op.getEdgeCondition());
        assertArrayEquals(kernel3x3.getKernelData(null), op.getKernel().getKernelData(null));
        assertEquals(hints, op.getRenderingHints());
    }

    @Test
    void testFilterNull() {
        ConvolveWithEdgeOp op = new ConvolveWithEdgeOp(kernel3x3);
        assertThrows(NullPointerException.class, () -> op.filter((BufferedImage) null, null));
    }

    @Test
    void testFilterSame() {
        ConvolveWithEdgeOp op = new ConvolveWithEdgeOp(kernel3x3);
        BufferedImage img = new BufferedImage(10, 10, BufferedImage.TYPE_INT_ARGB);
        assertThrows(IllegalArgumentException.class, () -> op.filter(img, img));
    }

    @Test
    void testGetBounds2D() {
        ConvolveWithEdgeOp op = new ConvolveWithEdgeOp(kernel3x3);
        BufferedImage img = new BufferedImage(10, 20, BufferedImage.TYPE_INT_ARGB);
        Rectangle2D bounds = op.getBounds2D(img);
        assertEquals(0, bounds.getX());
        assertEquals(0, bounds.getY());
        assertEquals(10, bounds.getWidth());
        assertEquals(20, bounds.getHeight());
    }

    @Test
    void testGetPoint2D() {
        ConvolveWithEdgeOp op = new ConvolveWithEdgeOp(kernel3x3);
        Point2D src = new Point2D.Double(5, 5);
        Point2D dst = op.getPoint2D(src, null);
        assertEquals(src.getX(), dst.getX());
        assertEquals(src.getY(), dst.getY());
    }

    @Test
    void testEdgeZeroFill() {
        // Use a kernel that averages all pixels
        Kernel kernel = new Kernel(3, 3, new float[]{
                1/9f, 1/9f, 1/9f,
                1/9f, 1/9f, 1/9f,
                1/9f, 1/9f, 1/9f
        });
        ConvolveWithEdgeOp op = new ConvolveWithEdgeOp(kernel, ConvolveWithEdgeOp.EDGE_ZERO_FILL, null);

        BufferedImage src = new BufferedImage(3, 3, BufferedImage.TYPE_INT_RGB);
        for (int y = 0; y < 3; y++) {
            for (int x = 0; x < 3; x++) {
                src.setRGB(x, y, 0xFFFFFF);
            }
        }

        BufferedImage dst = op.filter(src, null);

        // Center pixel should be white (all neighbors are white)
        // (9 * 255) / 9 = 255
        // We allow some tolerance for different JVM implementations of ConvolveOp
        int center = dst.getRGB(1, 1) & 0xFF;
        assertEquals(255, center, 1.0, "Center value " + center + " expected to be around 255");

        // Corner pixel (0,0) should be darker because of zero-fill
        // Neighbors: (0,0), (1,0), (0,1), (1,1) are white, others are zero
        // (4 * 255) / 9 = 113.33 -> 113
        int corner = dst.getRGB(0, 0) & 0xFF;
        assertTrue(corner < 255, "Corner value " + corner + " should be darker than 255");
    }

    @Test
    void testEdgeNoOp() {
        Kernel kernel = new Kernel(3, 3, new float[]{
                1/9f, 1/9f, 1/9f,
                1/9f, 1/9f, 1/9f,
                1/9f, 1/9f, 1/9f
        });
        ConvolveWithEdgeOp op = new ConvolveWithEdgeOp(kernel, ConvolveWithEdgeOp.EDGE_NO_OP, null);

        BufferedImage src = new BufferedImage(3, 3, BufferedImage.TYPE_INT_RGB);
        for (int y = 0; y < 3; y++) {
            for (int x = 0; x < 3; x++) {
                src.setRGB(x, y, 0xFFFFFF);
            }
        }

        BufferedImage dst = op.filter(src, null);

        // Center pixel should be white
        assertEquals(0xFFFFFFFF, dst.getRGB(1, 1));

        // EDGE_NO_OP: edge pixels are copied from source
        assertEquals(0xFFFFFFFF, dst.getRGB(0, 0));
    }

    @Test
    void testEdgeReflect() {
        Kernel kernel = new Kernel(3, 3, new float[]{
                1/9f, 1/9f, 1/9f,
                1/9f, 1/9f, 1/9f,
                1/9f, 1/9f, 1/9f
        });
        ConvolveWithEdgeOp op = new ConvolveWithEdgeOp(kernel, ConvolveWithEdgeOp.EDGE_REFLECT, null);

        BufferedImage src = new BufferedImage(3, 3, BufferedImage.TYPE_INT_RGB);
        for (int y = 0; y < 3; y++) {
            for (int x = 0; x < 3; x++) {
                src.setRGB(x, y, 0xFFFFFF);
            }
        }
        // Make center white, surroundings black to see reflection effect
        // Wait, if I want to test reflection, I should have different colors at edges.
        src.setRGB(0, 0, 0xFF0000); // Red corner
        src.setRGB(1, 1, 0xFFFFFF); // White center

        BufferedImage dst = op.filter(src, null);

        assertNotNull(dst);
        assertEquals(src.getWidth(), dst.getWidth());
        assertEquals(src.getHeight(), dst.getHeight());
    }

    @Test
    void testEdgeWrap() {
        Kernel kernel = new Kernel(3, 3, new float[]{
                1/9f, 1/9f, 1/9f,
                1/9f, 1/9f, 1/9f,
                1/9f, 1/9f, 1/9f
        });
        ConvolveWithEdgeOp op = new ConvolveWithEdgeOp(kernel, ConvolveWithEdgeOp.EDGE_WRAP, null);

        BufferedImage src = new BufferedImage(3, 3, BufferedImage.TYPE_INT_RGB);
        src.setRGB(1, 1, 0xFFFFFF);

        BufferedImage dst = op.filter(src, null);

        assertNotNull(dst);
        assertEquals(src.getWidth(), dst.getWidth());
        assertEquals(src.getHeight(), dst.getHeight());
    }

    @Test
    void testRasterFilter() {
        ConvolveWithEdgeOp op = new ConvolveWithEdgeOp(kernel3x3);
        
        BufferedImage srcImg = new BufferedImage(10, 10, BufferedImage.TYPE_INT_RGB);
        srcImg.setRGB(5, 5, 0xFFFFFF);
        Raster src = srcImg.getRaster();
        
        WritableRaster dst = op.createCompatibleDestRaster(src);
        op.filter(src, dst);
        
        assertEquals(255, dst.getSample(5, 5, 0));
    }

    @Test
    void testCreateCompatibleDestImage() {
        ConvolveWithEdgeOp op = new ConvolveWithEdgeOp(kernel3x3);
        BufferedImage src = new BufferedImage(10, 10, BufferedImage.TYPE_INT_RGB);
        BufferedImage dst = op.createCompatibleDestImage(src, src.getColorModel());
        
        assertEquals(src.getWidth(), dst.getWidth());
        assertEquals(src.getHeight(), dst.getHeight());
        assertEquals(src.getType(), dst.getType());
    }

    @Test
    void testFilter3ByteBGR() {
        // ConvolveWithEdgeOp has a workaround for TYPE_3BYTE_BGR
        ConvolveWithEdgeOp op = new ConvolveWithEdgeOp(kernel3x3);
        BufferedImage src = new BufferedImage(10, 10, BufferedImage.TYPE_3BYTE_BGR);
        src.setRGB(5, 5, 0xFFFFFF);
        
        BufferedImage dst = op.filter(src, null);
        
        assertNotNull(dst);
        assertEquals(src.getWidth(), dst.getWidth());
        assertEquals(src.getHeight(), dst.getHeight());
        assertEquals(src.getType(), dst.getType());
        assertEquals(0xFFFFFFFF, dst.getRGB(5, 5));
    }

    @Test
    void testFilterIndexed() {
        byte[] gray = new byte[256];
        for (int i = 0; i < 256; i++) {
            gray[i] = (byte) i;
        }
        IndexColorModel cm = new IndexColorModel(8, 256, gray, gray, gray);
        BufferedImage src = new BufferedImage(10, 10, BufferedImage.TYPE_BYTE_INDEXED, cm);
        
        ConvolveWithEdgeOp op = new ConvolveWithEdgeOp(kernel3x3);
        // ConvolveOp might not support indexed directly, it usually converts or throws.
        // But ConvolveWithEdgeOp uses ConvolveOp.
        
        assertDoesNotThrow(() -> op.filter(src, null));
    }
}